altium-toolkit 1.4.15 → 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.
@@ -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
  }
@@ -0,0 +1,122 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Shares primitive eligibility and physical layer metadata across PCB views.
7
+ */
8
+ export class PcbInteractionSourceMetadata {
9
+ /**
10
+ * Returns zone sources in selectable-item extraction order.
11
+ * @param {object} pcb PCB model.
12
+ * @returns {object[]}
13
+ */
14
+ static zoneSources(pcb) {
15
+ return [
16
+ ...(Array.isArray(pcb?.regions) ? pcb.regions : []),
17
+ ...(Array.isArray(pcb?.shapeBasedRegions)
18
+ ? pcb.shapeBasedRegions
19
+ : []),
20
+ ...(Array.isArray(pcb?.polygons) ? pcb.polygons : [])
21
+ ]
22
+ }
23
+
24
+ /**
25
+ * Identifies selectable zone geometry without constructing its points.
26
+ * @param {object} zone Zone-like source.
27
+ * @returns {'points' | 'segments' | 'bounds' | null}
28
+ */
29
+ static zoneGeometryKind(zone) {
30
+ if (Array.isArray(zone?.points) && zone.points.length >= 3) {
31
+ return 'points'
32
+ }
33
+ if (Array.isArray(zone?.segments) && zone.segments.length >= 3) {
34
+ return 'segments'
35
+ }
36
+ if (
37
+ Number.isFinite(Number(zone?.x1)) &&
38
+ Number.isFinite(Number(zone?.y1)) &&
39
+ Number.isFinite(Number(zone?.x2)) &&
40
+ Number.isFinite(Number(zone?.y2))
41
+ ) {
42
+ return 'bounds'
43
+ }
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * Creates a layer-name resolver for layer-id based primitives.
49
+ * @param {object} pcb PCB model.
50
+ * @returns {(item: object) => string[]}
51
+ */
52
+ static layerNameResolver(pcb) {
53
+ const byId = new Map()
54
+ const layers = [
55
+ ...(Array.isArray(pcb?.layers) ? pcb.layers : []),
56
+ ...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
57
+ ]
58
+
59
+ for (const layer of layers) {
60
+ const layerId = Number(layer?.layerId)
61
+ const name = String(layer?.name || '').trim()
62
+ if (Number.isInteger(layerId) && name) {
63
+ byId.set(layerId, name)
64
+ }
65
+ }
66
+
67
+ return (item) => {
68
+ const directLayer = String(item?.layer || '').trim()
69
+ if (directLayer) return [directLayer]
70
+
71
+ const layerId = Number(item?.layerId ?? item?.layerCode)
72
+ if (Number.isInteger(layerId) && byId.has(layerId)) {
73
+ return [byId.get(layerId)]
74
+ }
75
+
76
+ return []
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Collects ordered physical layer keys without building selectable geometry.
82
+ * @param {object} pcb PCB model.
83
+ * @returns {Map<string, Set<string>>}
84
+ */
85
+ static layersByObject(pcb) {
86
+ const layerNameFor = PcbInteractionSourceMetadata.layerNameResolver(pcb)
87
+ const layersByObject = new Map()
88
+ const groups = [
89
+ ['zones', PcbInteractionSourceMetadata.zoneSources(pcb)],
90
+ ['tracks', Array.isArray(pcb?.tracks) ? pcb.tracks : []],
91
+ ['pads', Array.isArray(pcb?.pads) ? pcb.pads : []],
92
+ ['footprint-text', Array.isArray(pcb?.texts) ? pcb.texts : []]
93
+ ]
94
+
95
+ // Vias and component bodies have no physical layer keys in the index.
96
+ for (const [objectKey, sources] of groups) {
97
+ const layerSet = new Set()
98
+ layersByObject.set(objectKey, layerSet)
99
+ if (objectKey === 'pads') layersByObject.set('holes', layerSet)
100
+
101
+ for (const source of sources) {
102
+ if (
103
+ objectKey === 'zones' &&
104
+ !PcbInteractionSourceMetadata.zoneGeometryKind(source)
105
+ ) {
106
+ continue
107
+ }
108
+ if (
109
+ objectKey === 'footprint-text' &&
110
+ source?.visible === false
111
+ ) {
112
+ continue
113
+ }
114
+ for (const layerKey of layerNameFor(source)) {
115
+ layerSet.add(layerKey)
116
+ }
117
+ }
118
+ }
119
+
120
+ return layersByObject
121
+ }
122
+ }