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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -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
- let nextIndex = 0;
142
- nextIndex < items.length;
143
- nextIndex += 1
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