altium-toolkit 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -14,6 +14,8 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
14
14
|
static #EDGE_GLYPH_CENTER_TOLERANCE = 1.5
|
|
15
15
|
static #EDGE_GLYPH_PROXIMITY_RATIO = 0.2
|
|
16
16
|
static #MARKER_PROXIMITY_MULTIPLIER = 3
|
|
17
|
+
static #SPATIAL_INDEX_MIN_CELL_SIZE = 32
|
|
18
|
+
static #SPATIAL_INDEX_MAX_CELLS_PER_ITEM = 256
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Normalizes repeated edge-facing documentation glyphs so their opening
|
|
@@ -107,6 +109,8 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
107
109
|
)
|
|
108
110
|
]
|
|
109
111
|
const visited = new Array(items.length).fill(false)
|
|
112
|
+
const spatialIndex =
|
|
113
|
+
PcbEdgeFacingGlyphNormalizer.#buildSpatialIndex(items)
|
|
110
114
|
const groups = []
|
|
111
115
|
|
|
112
116
|
for (let index = 0; index < items.length; index += 1) {
|
|
@@ -137,11 +141,10 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
137
141
|
arcIndexes.push(currentItem.arcIndex)
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
for (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
) {
|
|
144
|
+
for (const nextIndex of PcbEdgeFacingGlyphNormalizer.#collectCandidateIndexes(
|
|
145
|
+
currentItem.bounds,
|
|
146
|
+
spatialIndex
|
|
147
|
+
)) {
|
|
145
148
|
if (visited[nextIndex]) {
|
|
146
149
|
continue
|
|
147
150
|
}
|
|
@@ -171,6 +174,153 @@ export class PcbEdgeFacingGlyphNormalizer {
|
|
|
171
174
|
return groups
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Builds a spatial lookup for candidate bounds intersections.
|
|
179
|
+
* @param {{ bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} items
|
|
180
|
+
* @returns {{ cellSize: number, cells: Map<string, number[]>, overflowIndexes: number[], marks: Uint32Array, mark: number }}
|
|
181
|
+
*/
|
|
182
|
+
static #buildSpatialIndex(items) {
|
|
183
|
+
const cellSize =
|
|
184
|
+
PcbEdgeFacingGlyphNormalizer.#resolveSpatialCellSize(items)
|
|
185
|
+
const cells = new Map()
|
|
186
|
+
const overflowIndexes = []
|
|
187
|
+
|
|
188
|
+
items.forEach((item, index) => {
|
|
189
|
+
const range = PcbEdgeFacingGlyphNormalizer.#resolveCellRange(
|
|
190
|
+
item.bounds,
|
|
191
|
+
cellSize
|
|
192
|
+
)
|
|
193
|
+
const cellCount =
|
|
194
|
+
(range.maxX - range.minX + 1) * (range.maxY - range.minY + 1)
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
cellCount >
|
|
198
|
+
PcbEdgeFacingGlyphNormalizer.#SPATIAL_INDEX_MAX_CELLS_PER_ITEM
|
|
199
|
+
) {
|
|
200
|
+
overflowIndexes.push(index)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
|
|
205
|
+
for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
|
|
206
|
+
const key = PcbEdgeFacingGlyphNormalizer.#cellKey(
|
|
207
|
+
cellX,
|
|
208
|
+
cellY
|
|
209
|
+
)
|
|
210
|
+
const bucket = cells.get(key)
|
|
211
|
+
|
|
212
|
+
if (bucket) {
|
|
213
|
+
bucket.push(index)
|
|
214
|
+
} else {
|
|
215
|
+
cells.set(key, [index])
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
cellSize,
|
|
223
|
+
cells,
|
|
224
|
+
overflowIndexes,
|
|
225
|
+
marks: new Uint32Array(items.length),
|
|
226
|
+
mark: 0
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Collects unique candidate item indexes that could intersect one bounds box.
|
|
232
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
|
|
233
|
+
* @param {{ cellSize: number, cells: Map<string, number[]>, overflowIndexes: number[], marks: Uint32Array, mark: number }} spatialIndex
|
|
234
|
+
* @returns {number[]}
|
|
235
|
+
*/
|
|
236
|
+
static #collectCandidateIndexes(bounds, spatialIndex) {
|
|
237
|
+
const candidates = []
|
|
238
|
+
const range = PcbEdgeFacingGlyphNormalizer.#resolveCellRange(
|
|
239
|
+
bounds,
|
|
240
|
+
spatialIndex.cellSize
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
spatialIndex.mark += 1
|
|
244
|
+
if (spatialIndex.mark >= 0xffffffff) {
|
|
245
|
+
spatialIndex.marks.fill(0)
|
|
246
|
+
spatialIndex.mark = 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const appendCandidate = (index) => {
|
|
250
|
+
if (spatialIndex.marks[index] === spatialIndex.mark) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
spatialIndex.marks[index] = spatialIndex.mark
|
|
255
|
+
candidates.push(index)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
|
|
259
|
+
for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
|
|
260
|
+
const bucket = spatialIndex.cells.get(
|
|
261
|
+
PcbEdgeFacingGlyphNormalizer.#cellKey(cellX, cellY)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if (bucket) {
|
|
265
|
+
bucket.forEach(appendCandidate)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
spatialIndex.overflowIndexes.forEach(appendCandidate)
|
|
271
|
+
return candidates
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resolves a spatial cell size from typical primitive bounds spans.
|
|
276
|
+
* @param {{ bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} items
|
|
277
|
+
* @returns {number}
|
|
278
|
+
*/
|
|
279
|
+
static #resolveSpatialCellSize(items) {
|
|
280
|
+
const spans = items
|
|
281
|
+
.map((item) =>
|
|
282
|
+
Math.max(
|
|
283
|
+
Number(item.bounds.maxX) - Number(item.bounds.minX),
|
|
284
|
+
Number(item.bounds.maxY) - Number(item.bounds.minY),
|
|
285
|
+
0
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
.filter((span) => Number.isFinite(span))
|
|
289
|
+
.sort((left, right) => left - right)
|
|
290
|
+
const medianSpan = spans[Math.floor(spans.length / 2)] || 0
|
|
291
|
+
|
|
292
|
+
return Math.max(
|
|
293
|
+
medianSpan * 4,
|
|
294
|
+
PcbEdgeFacingGlyphNormalizer.#EDGE_GLYPH_CONNECTION_TOLERANCE * 8,
|
|
295
|
+
PcbEdgeFacingGlyphNormalizer.#SPATIAL_INDEX_MIN_CELL_SIZE
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolves the inclusive grid-cell range for one bounds box.
|
|
301
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
|
|
302
|
+
* @param {number} cellSize
|
|
303
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
|
|
304
|
+
*/
|
|
305
|
+
static #resolveCellRange(bounds, cellSize) {
|
|
306
|
+
return {
|
|
307
|
+
minX: Math.floor(Number(bounds.minX) / cellSize),
|
|
308
|
+
maxX: Math.floor(Number(bounds.maxX) / cellSize),
|
|
309
|
+
minY: Math.floor(Number(bounds.minY) / cellSize),
|
|
310
|
+
maxY: Math.floor(Number(bounds.maxY) / cellSize)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Builds one deterministic spatial index key.
|
|
316
|
+
* @param {number} cellX
|
|
317
|
+
* @param {number} cellY
|
|
318
|
+
* @returns {string}
|
|
319
|
+
*/
|
|
320
|
+
static #cellKey(cellX, cellY) {
|
|
321
|
+
return `${cellX}:${cellY}`
|
|
322
|
+
}
|
|
323
|
+
|
|
174
324
|
/**
|
|
175
325
|
* Resolves whether one connected screw glyph needs its semicircular head
|
|
176
326
|
* moved onto the same side as the screw tip while keeping the authored
|