altium-toolkit 1.4.16 → 1.4.17

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.
@@ -7,8 +7,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
7
7
  # Library Scope
8
8
 
9
9
  Altium Toolkit provides reusable native Altium parsing behind the common ECAD
10
- toolkit API. Canonical results use CircuitJSON; source-only facts and all
11
- historical 1.1.41 contracts remain explicit Altium extensions.
10
+ toolkit API. Canonical results use CircuitJSON; source-only facts and historical
11
+ 1.1.41 APIs remain explicit Altium extensions with maintained implementations.
12
12
 
13
13
  ## In Scope
14
14
 
@@ -18,7 +18,8 @@ historical 1.1.41 contracts remain explicit Altium extensions.
18
18
  reusable document contexts
19
19
  - Shared CircuitJSON render, interaction, query, manufacturing, simulation,
20
20
  and data-only 3D scene services
21
- - Exact `/extensions` preservation of audited Altium 1.1.41 behavior
21
+ - Preservation of audited Altium 1.1.41 `/extensions` API signatures and public
22
+ asset entrypoints and targets, with tested bug fixes and performance improvements
22
23
  - `.SchDoc`, `.PcbDoc`, `.PCBDwf`, `.SchLib`, `.PcbLib`, `.PrjPcb`, `.PrjScr`,
23
24
  and `.IntLib` parsing from `ArrayBuffer`
24
25
  - OLE and binary stream helpers needed by parser recovery
@@ -74,18 +74,9 @@ export class AltiumWorkerClient {
74
74
  */
75
75
  static #client() {
76
76
  if (!client) {
77
- client = new ParserWorkerClient({
78
- createWorker: () => {
79
- const WorkerConstructor = globalThis.Worker
80
- return Reflect.construct(WorkerConstructor, [
81
- new URL(
82
- '../workers/parser.worker.mjs',
83
- import.meta.url
84
- ),
85
- { type: 'module' }
86
- ])
87
- }
88
- })
77
+ client = ParserWorkerClient.fromWorkerUrl(
78
+ new URL('../workers/parser.worker.mjs', import.meta.url)
79
+ )
89
80
  }
90
81
  return client
91
82
  }
@@ -0,0 +1,95 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ /** Matches placements to authored bodies within one adapter invocation. */
5
+ export class AltiumScene3dBodyPlacementIndex {
6
+ #cells = new Map()
7
+
8
+ /**
9
+ * Groups authored body positions into one-mil cells in source order.
10
+ * @param {object[]} componentBodies Source component bodies.
11
+ */
12
+ constructor(componentBodies) {
13
+ componentBodies.forEach((componentBody, order) => {
14
+ const x = Number(componentBody?.positionMil?.x || 0)
15
+ const y = Number(componentBody?.positionMil?.y || 0)
16
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return
17
+ const key = `${Math.floor(x)}:${Math.floor(y)}`
18
+ const cell = this.#cells.get(key) || []
19
+ cell.push({ componentBody, x, y, order })
20
+ this.#cells.set(key, cell)
21
+ })
22
+ }
23
+
24
+ /**
25
+ * Selects by identity affinity, distance, then original source order.
26
+ * @param {object} placement Built external placement.
27
+ * @returns {object | null}
28
+ */
29
+ resolve(placement) {
30
+ const x = Number(placement?.bodyPositionMil?.x || 0)
31
+ const y = Number(placement?.bodyPositionMil?.y || 0)
32
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
33
+ const placementText = AltiumScene3dBodyPlacementIndex.#identityText([
34
+ placement?.designator,
35
+ placement?.externalModel?.name
36
+ ])
37
+ let best = null
38
+ let bestScore = -1
39
+ let bestDistance = Infinity
40
+ for (const dx of [-1, 0, 1]) {
41
+ for (const dy of [-1, 0, 1]) {
42
+ const cell =
43
+ this.#cells.get(
44
+ `${Math.floor(x) + dx}:${Math.floor(y) + dy}`
45
+ ) || []
46
+ for (const candidate of cell) {
47
+ const distance = Math.hypot(
48
+ x - candidate.x,
49
+ y - candidate.y
50
+ )
51
+ if (!(distance <= 0.01)) continue
52
+ const bodyText =
53
+ AltiumScene3dBodyPlacementIndex.#identityText([
54
+ candidate.componentBody?.identifier,
55
+ candidate.componentBody?.name
56
+ ])
57
+ const score =
58
+ placementText &&
59
+ bodyText &&
60
+ placementText.includes(bodyText)
61
+ ? bodyText.length
62
+ : 0
63
+ if (
64
+ best &&
65
+ !(
66
+ score > bestScore ||
67
+ (score === bestScore &&
68
+ (distance < bestDistance ||
69
+ (distance === bestDistance &&
70
+ candidate.order < best.order)))
71
+ )
72
+ )
73
+ continue
74
+ best = candidate
75
+ bestScore = score
76
+ bestDistance = distance
77
+ }
78
+ }
79
+ }
80
+ return best?.componentBody || null
81
+ }
82
+
83
+ /**
84
+ * Normalizes metadata with the placement adapter's identity convention.
85
+ * @param {unknown[]} values Source identity fields.
86
+ * @returns {string}
87
+ */
88
+ static #identityText(values) {
89
+ return values
90
+ .map((value) => String(value || '').toLowerCase())
91
+ .join(' ')
92
+ .replace(/\.[a-z0-9]+\\b/g, '')
93
+ .replace(/[^a-z0-9]+/g, '')
94
+ }
95
+ }
@@ -1,3 +1,4 @@
1
+ import { PcbScene3dComponentGeometryResolver } from './PcbScene3dComponentGeometryResolver.mjs'
1
2
  import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
2
3
 
3
4
  const REFINABLE_FAMILIES = new Set(['chip', 'diode', 'generic', 'ic', 'sot'])
@@ -33,6 +34,10 @@ export class AltiumScene3dComponentBodyAdapter {
33
34
  return sceneDescription
34
35
  }
35
36
 
37
+ const componentGeometry = new PcbScene3dComponentGeometryResolver(
38
+ sourceComponents,
39
+ pads
40
+ )
36
41
  const sourceByDesignator = new Map(
37
42
  sourceComponents.map((component) => [
38
43
  String(component?.designator || ''),
@@ -46,7 +51,7 @@ export class AltiumScene3dComponentBodyAdapter {
46
51
  AltiumScene3dComponentBodyAdapter.#refineComponent(
47
52
  component,
48
53
  sourceByDesignator.get(String(component?.designator || '')),
49
- pads
54
+ componentGeometry
50
55
  )
51
56
  )
52
57
  }
@@ -56,10 +61,10 @@ export class AltiumScene3dComponentBodyAdapter {
56
61
  * Refines one procedural component body when nearby pads overinflated it.
57
62
  * @param {object} component Scene component.
58
63
  * @param {object | undefined} sourceComponent Source PCB component.
59
- * @param {object[]} pads Source PCB pads.
64
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped geometry.
60
65
  * @returns {object}
61
66
  */
62
- static #refineComponent(component, sourceComponent, pads) {
67
+ static #refineComponent(component, sourceComponent, componentGeometry) {
63
68
  const family = String(component?.body?.family || '')
64
69
  if (
65
70
  component?.externalModel ||
@@ -72,7 +77,7 @@ export class AltiumScene3dComponentBodyAdapter {
72
77
  const span = AltiumScene3dComponentBodyAdapter.#ownedPadSpan(
73
78
  sourceComponent,
74
79
  component.mountSide,
75
- pads
80
+ componentGeometry
76
81
  )
77
82
  const size = component?.body?.sizeMil || {}
78
83
  if (
@@ -99,42 +104,17 @@ export class AltiumScene3dComponentBodyAdapter {
99
104
  * Resolves the owned surface-pad span for one source component.
100
105
  * @param {object} component Source component.
101
106
  * @param {string} mountSide Component mount side.
102
- * @param {object[]} pads Source PCB pads.
107
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped geometry.
103
108
  * @returns {{ width: number, depth: number } | null}
104
109
  */
105
- static #ownedPadSpan(component, mountSide, pads) {
106
- const componentIndex = Number(component?.componentIndex)
107
- if (!Number.isFinite(componentIndex)) {
108
- return null
109
- }
110
-
111
- const ownedPads = pads.filter(
112
- (pad) => Number(pad?.componentIndex) === componentIndex
113
- )
114
- const surfacePads = ownedPads.filter((pad) =>
115
- AltiumScene3dComponentBodyAdapter.#isSurfacePad(pad, mountSide)
116
- )
117
- const spanPads = surfacePads.length ? surfacePads : ownedPads
118
-
110
+ static #ownedPadSpan(component, mountSide, componentGeometry) {
119
111
  return PcbScene3dPadLocalSpanResolver.resolve(
120
112
  component,
121
- spanPads,
113
+ componentGeometry.componentPads(component, mountSide),
122
114
  mountSide
123
115
  )
124
116
  }
125
117
 
126
- /**
127
- * Checks whether one pad belongs to the component's mounted surface.
128
- * @param {object} pad Source pad.
129
- * @param {string} mountSide Component mount side.
130
- * @returns {boolean}
131
- */
132
- static #isSurfacePad(pad, mountSide) {
133
- return String(mountSide || '').toLowerCase() === 'bottom'
134
- ? Boolean(pad?.hasBottomPasteMaskOpening)
135
- : Boolean(pad?.hasTopPasteMaskOpening)
136
- }
137
-
138
118
  /**
139
119
  * Checks whether the current body is clearly larger than owned pads.
140
120
  * @param {object} size Current body size.
@@ -1,3 +1,4 @@
1
+ import { AltiumScene3dBodyPlacementIndex } from './AltiumScene3dBodyPlacementIndex.mjs'
1
2
  import { AltiumScene3dIdentityTokens } from './AltiumScene3dIdentityTokens.mjs'
2
3
  import { AltiumScene3dAuthoredConnectorYawPolicy } from './AltiumScene3dAuthoredConnectorYawPolicy.mjs'
3
4
  import { AltiumScene3dPlacementRotationPolicy } from './AltiumScene3dPlacementRotationPolicy.mjs'
@@ -59,6 +60,7 @@ export class AltiumScene3dExternalPlacementAdapter {
59
60
  ])
60
61
  )
61
62
 
63
+ const bodyIndex = new AltiumScene3dBodyPlacementIndex(componentBodies)
62
64
  const repairedScene = {
63
65
  ...sceneDescription,
64
66
  externalPlacements: sceneDescription.externalPlacements
@@ -67,7 +69,7 @@ export class AltiumScene3dExternalPlacementAdapter {
67
69
  placement,
68
70
  components,
69
71
  componentByDesignator,
70
- componentBodies,
72
+ bodyIndex,
71
73
  pads,
72
74
  sceneDescription?.board
73
75
  )
@@ -86,7 +88,7 @@ export class AltiumScene3dExternalPlacementAdapter {
86
88
  placement,
87
89
  components,
88
90
  componentByDesignator,
89
- componentBodies,
91
+ bodyIndex,
90
92
  pads,
91
93
  board
92
94
  ) {
@@ -101,11 +103,7 @@ export class AltiumScene3dExternalPlacementAdapter {
101
103
  return placement
102
104
  }
103
105
 
104
- const componentBody =
105
- AltiumScene3dExternalPlacementAdapter.#resolveComponentBody(
106
- placement,
107
- componentBodies
108
- )
106
+ const componentBody = bodyIndex.resolve(placement)
109
107
  const currentComponent = componentByDesignator.get(
110
108
  String(placement?.designator || '')
111
109
  )
@@ -1542,60 +1540,6 @@ export class AltiumScene3dExternalPlacementAdapter {
1542
1540
  ]
1543
1541
  }
1544
1542
 
1545
- /**
1546
- * Resolves the source component body row for one placement.
1547
- * @param {object} placement External model placement.
1548
- * @param {object[]} componentBodies Source component body rows.
1549
- * @returns {object | null}
1550
- */
1551
- static #resolveComponentBody(placement, componentBodies) {
1552
- const candidates = componentBodies
1553
- .map((componentBody) => ({
1554
- componentBody,
1555
- distance:
1556
- AltiumScene3dExternalPlacementAdapter.#distanceBetweenPoints(
1557
- placement?.bodyPositionMil,
1558
- componentBody?.positionMil
1559
- ),
1560
- identityScore:
1561
- AltiumScene3dExternalPlacementAdapter.#bodyPlacementIdentityScore(
1562
- placement,
1563
- componentBody
1564
- )
1565
- }))
1566
- .filter((candidate) => candidate.distance <= 0.01)
1567
- .sort(
1568
- (left, right) =>
1569
- right.identityScore - left.identityScore ||
1570
- left.distance - right.distance
1571
- )
1572
-
1573
- return candidates[0]?.componentBody || null
1574
- }
1575
-
1576
- /**
1577
- * Scores whether a source body row belongs to one placement.
1578
- * @param {object} placement External model placement.
1579
- * @param {object} componentBody Source component body.
1580
- * @returns {number}
1581
- */
1582
- static #bodyPlacementIdentityScore(placement, componentBody) {
1583
- const placementText =
1584
- AltiumScene3dExternalPlacementAdapter.#normalizeIdentityText([
1585
- placement?.designator,
1586
- placement?.externalModel?.name
1587
- ])
1588
- const bodyText =
1589
- AltiumScene3dExternalPlacementAdapter.#normalizeIdentityText([
1590
- componentBody?.identifier,
1591
- componentBody?.name
1592
- ])
1593
-
1594
- return placementText && bodyText && placementText.includes(bodyText)
1595
- ? bodyText.length
1596
- : 0
1597
- }
1598
-
1599
1543
  /**
1600
1544
  * Repairs model-local Altium rotation signs for embedded body transforms.
1601
1545
  * @param {object | null | undefined} modelTransform Placement transform.
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { PcbInteractionGeometry } from './PcbInteractionGeometry.mjs'
6
6
  import { PcbInteractionItemRegistry } from './PcbInteractionItemRegistry.mjs'
7
+ import { PcbInteractionSourceMetadata } from './PcbInteractionSourceMetadata.mjs'
7
8
 
8
9
  const TYPE_PRIORITY = {
9
10
  track: 100,
@@ -67,15 +68,18 @@ export class PcbInteractionIndex {
67
68
  * @returns {object[]}
68
69
  */
69
70
  static hitTestItems(items, point, options = {}) {
71
+ const visibility = PcbInteractionIndex.#visibilityContext(options)
72
+ const tolerance = Number(options?.tolerance) || 0
73
+
70
74
  return (Array.isArray(items) ? items : [])
71
75
  .filter((item) =>
72
- PcbInteractionIndex.#isVisibleCandidate(item, options)
76
+ PcbInteractionIndex.#isVisibleCandidate(item, visibility)
73
77
  )
74
78
  .filter((item) =>
75
79
  PcbInteractionGeometry.containsPoint(
76
80
  item.geometry,
77
81
  point,
78
- Number(options?.tolerance) || 0
82
+ tolerance
79
83
  )
80
84
  )
81
85
  .sort(PcbInteractionIndex.#compareCandidates)
@@ -101,9 +105,23 @@ export class PcbInteractionIndex {
101
105
  * @returns {object}
102
106
  */
103
107
  static #context(pcb) {
108
+ const components = Array.isArray(pcb.components) ? pcb.components : []
109
+ const componentsById = new Map()
110
+ for (const component of components) {
111
+ const componentIndex = Number(component?.componentIndex)
112
+ if (
113
+ Number.isInteger(componentIndex) &&
114
+ !componentsById.has(componentIndex)
115
+ ) {
116
+ // Explicit identities take precedence, retaining the first row.
117
+ componentsById.set(componentIndex, component)
118
+ }
119
+ }
120
+
104
121
  return {
105
- components: Array.isArray(pcb.components) ? pcb.components : [],
106
- layerNameFor: PcbInteractionIndex.#layerNameResolver(pcb)
122
+ components,
123
+ componentsById,
124
+ layerNameFor: PcbInteractionSourceMetadata.layerNameResolver(pcb)
107
125
  }
108
126
  }
109
127
 
@@ -129,13 +147,7 @@ export class PcbInteractionIndex {
129
147
  */
130
148
  static #extractZones(documentModel, context) {
131
149
  const pcb = documentModel?.pcb || {}
132
- const zones = [
133
- ...(Array.isArray(pcb.regions) ? pcb.regions : []),
134
- ...(Array.isArray(pcb.shapeBasedRegions)
135
- ? pcb.shapeBasedRegions
136
- : []),
137
- ...(Array.isArray(pcb.polygons) ? pcb.polygons : [])
138
- ]
150
+ const zones = PcbInteractionSourceMetadata.zoneSources(pcb)
139
151
 
140
152
  return zones
141
153
  .map((zone, index) => {
@@ -326,10 +338,11 @@ export class PcbInteractionIndex {
326
338
  * @returns {object | null}
327
339
  */
328
340
  static #zoneGeometry(zone) {
329
- if (Array.isArray(zone?.points) && zone.points.length >= 3) {
341
+ const kind = PcbInteractionSourceMetadata.zoneGeometryKind(zone)
342
+ if (kind === 'points') {
330
343
  return PcbInteractionGeometry.polygon(zone.points)
331
344
  }
332
- if (Array.isArray(zone?.segments) && zone.segments.length >= 3) {
345
+ if (kind === 'segments') {
333
346
  return PcbInteractionGeometry.polygon(
334
347
  zone.segments.map((segment) => ({
335
348
  x: segment.x1,
@@ -337,12 +350,7 @@ export class PcbInteractionIndex {
337
350
  }))
338
351
  )
339
352
  }
340
- if (
341
- Number.isFinite(Number(zone?.x1)) &&
342
- Number.isFinite(Number(zone?.y1)) &&
343
- Number.isFinite(Number(zone?.x2)) &&
344
- Number.isFinite(Number(zone?.y2))
345
- ) {
353
+ if (kind === 'bounds') {
346
354
  return PcbInteractionGeometry.bounds({
347
355
  minX: Math.min(Number(zone.x1), Number(zone.x2)),
348
356
  minY: Math.min(Number(zone.y1), Number(zone.y2)),
@@ -372,21 +380,38 @@ export class PcbInteractionIndex {
372
380
  }
373
381
 
374
382
  /**
375
- * Returns whether an item is visible under hit-test filters.
376
- * @param {object} item Interaction item.
383
+ * Normalizes visibility filters once for the current hit test.
377
384
  * @param {object} options Hit-test options.
385
+ * @returns {object}
386
+ */
387
+ static #visibilityContext(options) {
388
+ return {
389
+ side: PcbInteractionIndex.#normalizeSide(options?.side),
390
+ hiddenObjects: new Set(
391
+ (Array.isArray(options?.hiddenObjects)
392
+ ? options.hiddenObjects
393
+ : []
394
+ ).map(String)
395
+ ),
396
+ hiddenLayers: new Set(
397
+ (Array.isArray(options?.hiddenLayers)
398
+ ? options.hiddenLayers
399
+ : []
400
+ ).map(String)
401
+ )
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Returns whether an item is visible under normalized hit-test filters.
407
+ * @param {object} item Interaction item.
408
+ * @param {object} visibility Normalized visibility filters.
378
409
  * @returns {boolean}
379
410
  */
380
- static #isVisibleCandidate(item, options) {
381
- const side = PcbInteractionIndex.#normalizeSide(options?.side)
411
+ static #isVisibleCandidate(item, visibility) {
412
+ const { side, hiddenObjects, hiddenLayers } = visibility
382
413
  if (item.side !== 'both' && item.side !== side) return false
383
414
 
384
- const hiddenObjects = new Set(
385
- (Array.isArray(options?.hiddenObjects)
386
- ? options.hiddenObjects
387
- : []
388
- ).map(String)
389
- )
390
415
  if (
391
416
  hiddenObjects.has(item.objectKey) ||
392
417
  hiddenObjects.has(PLURAL_TYPE_KEYS[item.type] || item.type)
@@ -394,12 +419,6 @@ export class PcbInteractionIndex {
394
419
  return false
395
420
  }
396
421
 
397
- const hiddenLayers = new Set(
398
- (Array.isArray(options?.hiddenLayers)
399
- ? options.hiddenLayers
400
- : []
401
- ).map(String)
402
- )
403
422
  return (
404
423
  !item.layerKeys?.length ||
405
424
  item.layerKeys.some((layerKey) => !hiddenLayers.has(layerKey))
@@ -416,39 +435,6 @@ export class PcbInteractionIndex {
416
435
  return second.priority - first.priority || first.order - second.order
417
436
  }
418
437
 
419
- /**
420
- * Creates a layer-name resolver for layer-id based primitives.
421
- * @param {object} pcb PCB model.
422
- * @returns {(item: object) => string[]}
423
- */
424
- static #layerNameResolver(pcb) {
425
- const byId = new Map()
426
- const layers = [
427
- ...(Array.isArray(pcb?.layers) ? pcb.layers : []),
428
- ...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
429
- ]
430
-
431
- for (const layer of layers) {
432
- const layerId = Number(layer?.layerId)
433
- const name = String(layer?.name || '').trim()
434
- if (Number.isInteger(layerId) && name) {
435
- byId.set(layerId, name)
436
- }
437
- }
438
-
439
- return (item) => {
440
- const directLayer = String(item?.layer || '').trim()
441
- if (directLayer) return [directLayer]
442
-
443
- const layerId = Number(item?.layerId ?? item?.layerCode)
444
- if (Number.isInteger(layerId) && byId.has(layerId)) {
445
- return [byId.get(layerId)]
446
- }
447
-
448
- return []
449
- }
450
- }
451
-
452
438
  /**
453
439
  * Resolves physical layer keys for an item.
454
440
  * @param {object} item Source item.
@@ -484,18 +470,13 @@ export class PcbInteractionIndex {
484
470
  const componentIndex = Number(primitive?.componentIndex)
485
471
  if (!Number.isInteger(componentIndex)) return null
486
472
 
487
- const explicitMatch =
488
- context.components.find(
489
- (component) =>
490
- Number(component?.componentIndex) === componentIndex
491
- ) || null
473
+ const explicitMatch = context.componentsById.get(componentIndex)
492
474
  if (explicitMatch) return explicitMatch
493
475
 
494
- return (
495
- context.components.find((component, index) => {
496
- return index === componentIndex
497
- }) || null
498
- )
476
+ if (componentIndex < 0 || componentIndex >= context.components.length) {
477
+ return null
478
+ }
479
+ return context.components[componentIndex] || null
499
480
  }
500
481
 
501
482
  /**
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- import { PcbInteractionIndex } from './PcbInteractionIndex.mjs'
5
+ import { PcbInteractionSourceMetadata } from './PcbInteractionSourceMetadata.mjs'
6
6
 
7
7
  const VIRTUAL_LAYER_DEFINITIONS = [
8
8
  { key: 'tracks', label: 'Tracks' },
@@ -25,8 +25,7 @@ export class PcbInteractionLayerModel {
25
25
  static resolve(documentModel) {
26
26
  const pcb = documentModel?.pcb || {}
27
27
  const physicalLayers = PcbInteractionLayerModel.#physicalLayers(pcb)
28
- const items = PcbInteractionIndex.build(documentModel)
29
- const layersByObject = PcbInteractionLayerModel.#layersByObject(items)
28
+ const layersByObject = PcbInteractionSourceMetadata.layersByObject(pcb)
30
29
 
31
30
  return {
32
31
  physicalLayers,
@@ -71,33 +70,4 @@ export class PcbInteractionLayerModel {
71
70
 
72
71
  return layers
73
72
  }
74
-
75
- /**
76
- * Collects referenced physical layer keys by virtual object key.
77
- * @param {object[]} items Interaction items.
78
- * @returns {Map<string, Set<string>>}
79
- */
80
- static #layersByObject(items) {
81
- const layersByObject = new Map()
82
-
83
- for (const item of items) {
84
- if (!layersByObject.has(item.objectKey)) {
85
- layersByObject.set(item.objectKey, new Set())
86
- }
87
- const layerSet = layersByObject.get(item.objectKey)
88
- for (const layerKey of item.layerKeys || []) {
89
- layerSet.add(layerKey)
90
- }
91
- if (item.type === 'pad' || item.type === 'via') {
92
- if (!layersByObject.has('holes')) {
93
- layersByObject.set('holes', new Set())
94
- }
95
- for (const layerKey of item.layerKeys || []) {
96
- layersByObject.get('holes').add(layerKey)
97
- }
98
- }
99
- }
100
-
101
- return layersByObject
102
- }
103
73
  }