altium-toolkit 1.0.9 → 1.1.0

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.
Files changed (79) hide show
  1. package/docs/api.md +6 -2
  2. package/docs/model-format.md +29 -4
  3. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
  4. package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
  5. package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
  6. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
  7. package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
  8. package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
  9. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
  10. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
  11. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
  12. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  13. package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
  14. package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
  15. package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
  16. package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
  17. package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
  18. package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
  19. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
  20. package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
  21. package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
  22. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
  23. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  24. package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
  25. package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
  26. package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
  27. package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
  28. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  29. package/package.json +1 -1
  30. package/src/core/altium/AltiumParser.mjs +12 -2
  31. package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
  32. package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
  33. package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
  34. package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
  35. package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
  36. package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
  37. package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
  38. package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
  39. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  40. package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
  41. package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
  42. package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
  43. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
  44. package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
  45. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
  46. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
  47. package/src/core/altium/PcbLibModelParser.mjs +2 -0
  48. package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
  49. package/src/core/altium/PcbModelParser.mjs +211 -22
  50. package/src/core/altium/PcbPadStackParser.mjs +171 -2
  51. package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
  52. package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
  53. package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
  54. package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
  55. package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
  56. package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
  57. package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
  58. package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
  59. package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
  60. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  61. package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
  62. package/src/core/altium/PrintableTextDecoder.mjs +70 -6
  63. package/src/core/altium/PrjPcbModelParser.mjs +69 -2
  64. package/src/core/altium/PrjScrModelParser.mjs +386 -0
  65. package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
  66. package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
  67. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  68. package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
  69. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
  70. package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
  71. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
  72. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  73. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  74. package/src/parser.mjs +21 -0
  75. package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
  76. package/src/ui/PcbScene3dBuilder.mjs +26 -4
  77. package/src/ui/PcbSvgRenderer.mjs +65 -0
  78. package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +48 -2
@@ -0,0 +1,244 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
6
+
7
+ const Primitives = CircuitJsonModelAdapterPrimitives
8
+
9
+ /**
10
+ * Builds upstream-compatible Circuit JSON PCB element records.
11
+ */
12
+ export class CircuitJsonModelAdapterPcbElements {
13
+ /**
14
+ * Builds an upstream-compatible SMT pad element.
15
+ * @param {string} idScope
16
+ * @param {Record<string, unknown>} pad
17
+ * @param {number} padIndex
18
+ * @param {string} pcbComponentId
19
+ * @param {string} pcbPortId
20
+ * @param {{ x: number, y: number }} center
21
+ * @param {string} layer
22
+ * @param {string[]} portHints
23
+ * @returns {object}
24
+ */
25
+ static smtPad(
26
+ idScope,
27
+ pad,
28
+ padIndex,
29
+ pcbComponentId,
30
+ pcbPortId,
31
+ center,
32
+ layer,
33
+ portHints
34
+ ) {
35
+ const shape = Primitives.padShape(pad)
36
+ const width = Primitives.milNumber(
37
+ pad.sizeTopX || pad.sizeX || pad.width,
38
+ 0
39
+ )
40
+ const height = Primitives.milNumber(
41
+ pad.sizeTopY || pad.sizeY || pad.height,
42
+ 0
43
+ )
44
+ const rotation = Primitives.number(pad.rotation || pad.holeRotation, 0)
45
+ const base = {
46
+ type: 'pcb_smtpad',
47
+ pcb_smtpad_id: Primitives.id(idScope, ['pcb_smtpad', padIndex]),
48
+ pcb_component_id: pcbComponentId,
49
+ pcb_port_id: pcbPortId,
50
+ x: center.x,
51
+ y: center.y,
52
+ layer,
53
+ port_hints: portHints
54
+ }
55
+
56
+ if (shape === 'circle') {
57
+ return {
58
+ ...base,
59
+ shape,
60
+ radius: Primitives.round(Math.max(width, height) / 2)
61
+ }
62
+ }
63
+
64
+ if (shape === 'pill') {
65
+ return {
66
+ ...base,
67
+ shape: CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
68
+ ? 'rotated_pill'
69
+ : 'pill',
70
+ width,
71
+ height,
72
+ radius: Primitives.round(Math.min(width, height) / 2),
73
+ ...CircuitJsonModelAdapterPcbElements.#ccwRotationField(
74
+ rotation
75
+ )
76
+ }
77
+ }
78
+
79
+ return {
80
+ ...base,
81
+ shape: CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
82
+ ? 'rotated_rect'
83
+ : 'rect',
84
+ width,
85
+ height,
86
+ ...CircuitJsonModelAdapterPcbElements.#ccwRotationField(rotation)
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Builds an upstream-compatible non-plated hole element.
92
+ * @param {string} idScope
93
+ * @param {Record<string, unknown>} pad
94
+ * @param {number} padIndex
95
+ * @param {string} pcbComponentId
96
+ * @param {{ x: number, y: number }} center
97
+ * @returns {object}
98
+ */
99
+ static hole(idScope, pad, padIndex, pcbComponentId, center) {
100
+ const shape = Primitives.padShape(pad)
101
+ const width = Primitives.milNumber(
102
+ pad.sizeTopX || pad.sizeX || pad.width || pad.diameter,
103
+ 0
104
+ )
105
+ const height = Primitives.milNumber(
106
+ pad.sizeTopY || pad.sizeY || pad.height || pad.diameter,
107
+ 0
108
+ )
109
+ const holeDiameter = Primitives.milNumber(pad.holeDiameter, 0)
110
+ const base = {
111
+ type: 'pcb_hole',
112
+ pcb_hole_id: Primitives.id(idScope, ['pcb_hole', padIndex]),
113
+ pcb_component_id: pcbComponentId,
114
+ x: center.x,
115
+ y: center.y
116
+ }
117
+
118
+ if (shape === 'circle') {
119
+ return {
120
+ ...base,
121
+ hole_shape: 'circle',
122
+ hole_diameter: holeDiameter
123
+ }
124
+ }
125
+
126
+ return {
127
+ ...base,
128
+ hole_shape: shape === 'pill' ? 'pill' : 'rect',
129
+ hole_width: holeDiameter || width,
130
+ hole_height: holeDiameter || height
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Builds an upstream-compatible plated hole element.
136
+ * @param {string} idScope
137
+ * @param {Record<string, unknown>} pad
138
+ * @param {number} padIndex
139
+ * @param {string} pcbComponentId
140
+ * @param {string} pcbPortId
141
+ * @param {{ x: number, y: number }} center
142
+ * @param {string[]} portHints
143
+ * @returns {object}
144
+ */
145
+ static platedHole(
146
+ idScope,
147
+ pad,
148
+ padIndex,
149
+ pcbComponentId,
150
+ pcbPortId,
151
+ center,
152
+ portHints
153
+ ) {
154
+ const shape = Primitives.padShape(pad)
155
+ const width = Primitives.milNumber(
156
+ pad.sizeTopX || pad.sizeX || pad.width || pad.diameter,
157
+ 0
158
+ )
159
+ const height = Primitives.milNumber(
160
+ pad.sizeTopY || pad.sizeY || pad.height || pad.diameter,
161
+ 0
162
+ )
163
+ const holeDiameter = Primitives.milNumber(pad.holeDiameter, 0)
164
+ const rotation = Primitives.number(pad.rotation || pad.holeRotation, 0)
165
+ const base = {
166
+ type: 'pcb_plated_hole',
167
+ pcb_plated_hole_id: Primitives.id(idScope, [
168
+ 'pcb_plated_hole',
169
+ padIndex
170
+ ]),
171
+ pcb_component_id: pcbComponentId,
172
+ pcb_port_id: pcbPortId,
173
+ x: center.x,
174
+ y: center.y,
175
+ layers: ['top', 'bottom'],
176
+ port_hints: portHints
177
+ }
178
+
179
+ if (shape === 'circle') {
180
+ return {
181
+ ...base,
182
+ shape,
183
+ outer_diameter: Primitives.round(Math.max(width, height)),
184
+ hole_diameter: holeDiameter
185
+ }
186
+ }
187
+
188
+ if (shape === 'pill') {
189
+ return {
190
+ ...base,
191
+ shape,
192
+ outer_width: width,
193
+ outer_height: height,
194
+ hole_width: holeDiameter,
195
+ hole_height: holeDiameter,
196
+ ccw_rotation: rotation
197
+ }
198
+ }
199
+
200
+ return {
201
+ ...base,
202
+ shape: 'circular_hole_with_rect_pad',
203
+ hole_shape: 'circle',
204
+ pad_shape: 'rect',
205
+ hole_diameter: holeDiameter,
206
+ rect_pad_width: width,
207
+ rect_pad_height: height,
208
+ ...CircuitJsonModelAdapterPcbElements.#rectCcwRotationField(
209
+ rotation
210
+ )
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returns true when a rotation value should use a rotated pad shape.
216
+ * @param {number | null} rotation
217
+ * @returns {boolean}
218
+ */
219
+ static #hasRotation(rotation) {
220
+ return Math.abs(rotation || 0) > 0.000001
221
+ }
222
+
223
+ /**
224
+ * Returns an optional counter-clockwise rotation field.
225
+ * @param {number | null} rotation
226
+ * @returns {{ ccw_rotation?: number }}
227
+ */
228
+ static #ccwRotationField(rotation) {
229
+ return CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
230
+ ? { ccw_rotation: rotation || 0 }
231
+ : {}
232
+ }
233
+
234
+ /**
235
+ * Returns an optional rectangular pad rotation field.
236
+ * @param {number | null} rotation
237
+ * @returns {{ rect_ccw_rotation?: number }}
238
+ */
239
+ static #rectCcwRotationField(rotation) {
240
+ return CircuitJsonModelAdapterPcbElements.#hasRotation(rotation)
241
+ ? { rect_ccw_rotation: rotation || 0 }
242
+ : {}
243
+ }
244
+ }
@@ -8,7 +8,7 @@
8
8
  export class CircuitJsonModelSchema {
9
9
  static CURRENT_SCHEMA_ID = 'https://github.com/tscircuit/circuit-json'
10
10
 
11
- static CURRENT_SCHEMA_VERSION = '0.0.431'
11
+ static CURRENT_SCHEMA_VERSION = '0.0.433'
12
12
 
13
13
  static FORMAT_NAME = 'circuit-json'
14
14
 
package/src/parser.mjs CHANGED
@@ -9,21 +9,31 @@ export { OleDirectoryEntry } from './core/ole/OleDirectoryEntry.mjs'
9
9
  export { AltiumParser } from './core/altium/AltiumParser.mjs'
10
10
  export { AltiumLayoutParser } from './core/altium/AltiumLayoutParser.mjs'
11
11
  export { AsciiRecordParser } from './core/altium/AsciiRecordParser.mjs'
12
+ export { HostCapabilityDiagnosticsBuilder } from './core/altium/HostCapabilityDiagnosticsBuilder.mjs'
12
13
  export { IntLibModelParser } from './core/altium/IntLibModelParser.mjs'
13
14
  export { IntLibStreamExtractor } from './core/altium/IntLibStreamExtractor.mjs'
14
15
  export { EmbeddedFileInventoryBuilder } from './core/altium/EmbeddedFileInventoryBuilder.mjs'
16
+ export { CiArtifactBundleBuilder } from './core/altium/CiArtifactBundleBuilder.mjs'
17
+ export { ContractGateReportBuilder } from './core/altium/ContractGateReportBuilder.mjs'
18
+ export { DraftsmanBoardViewMetadataBuilder } from './core/altium/DraftsmanBoardViewMetadataBuilder.mjs'
19
+ export { DraftsmanDigestParser } from './core/altium/DraftsmanDigestParser.mjs'
15
20
  export { LibraryRenderManifestBuilder } from './core/altium/LibraryRenderManifestBuilder.mjs'
21
+ export { LibraryQaReportBuilder } from './core/altium/LibraryQaReportBuilder.mjs'
16
22
  export { LibrarySearchIndex } from './core/altium/LibrarySearchIndex.mjs'
17
23
  export { ParserUtils } from './core/altium/ParserUtils.mjs'
24
+ export { ParserCompatibilityFuzzer } from './core/altium/ParserCompatibilityFuzzer.mjs'
18
25
  export { NormalizedModelSchema } from './core/altium/NormalizedModelSchema.mjs'
19
26
  export { ProjectDesignBundleBuilder } from './core/altium/ProjectDesignBundleBuilder.mjs'
27
+ export { ProjectDocumentGraphBuilder } from './core/altium/ProjectDocumentGraphBuilder.mjs'
20
28
  export { ProjectAnnotationParser } from './core/altium/ProjectAnnotationParser.mjs'
21
29
  export { ProjectNetlistExporter } from './core/altium/ProjectNetlistExporter.mjs'
22
30
  export { ProjectVariantViewBuilder } from './core/altium/ProjectVariantViewBuilder.mjs'
23
31
  export { CircuitJsonModelSchema } from './core/circuit-json/CircuitJsonModelSchema.mjs'
24
32
  export { CircuitJsonModelAdapter } from './core/circuit-json/CircuitJsonModelAdapter.mjs'
25
33
  export { PcbBinaryPrimitiveParser } from './core/altium/PcbBinaryPrimitiveParser.mjs'
34
+ export { PcbBomProfileBuilder } from './core/altium/PcbBomProfileBuilder.mjs'
26
35
  export { PcbBoardRegionSemanticsParser } from './core/altium/PcbBoardRegionSemanticsParser.mjs'
36
+ export { PcbComponentKindPolicy } from './core/altium/PcbComponentKindPolicy.mjs'
27
37
  export { PcbComponentPrimitiveIndexer } from './core/altium/PcbComponentPrimitiveIndexer.mjs'
28
38
  export { PcbCustomPadShapeParser } from './core/altium/PcbCustomPadShapeParser.mjs'
29
39
  export { PcbDimensionParser } from './core/altium/PcbDimensionParser.mjs'
@@ -32,7 +42,11 @@ export { PcbEmbeddedModelExtractor } from './core/altium/PcbEmbeddedModelExtract
32
42
  export { PcbExtendedPrimitiveInformationParser } from './core/altium/PcbExtendedPrimitiveInformationParser.mjs'
33
43
  export { PcbFontMetricsParser } from './core/altium/PcbFontMetricsParser.mjs'
34
44
  export { PcbLayerIdCodec } from './core/altium/PcbLayerIdCodec.mjs'
45
+ export { PcbLayerStackFidelityReportBuilder } from './core/altium/PcbLayerStackFidelityReportBuilder.mjs'
46
+ export { PcbLayerStackInterchangeParser } from './core/altium/PcbLayerStackInterchangeParser.mjs'
47
+ export { PcbLayerStackQueryHelper } from './core/altium/PcbLayerStackQueryHelper.mjs'
35
48
  export { PcbLibModelParser } from './core/altium/PcbLibModelParser.mjs'
49
+ export { PcbLibParityReportBuilder } from './core/altium/PcbLibParityReportBuilder.mjs'
36
50
  export { PcbLibStreamExtractor } from './core/altium/PcbLibStreamExtractor.mjs'
37
51
  export { PcbMechanicalLayerPairParser } from './core/altium/PcbMechanicalLayerPairParser.mjs'
38
52
  export { PcbModelParser } from './core/altium/PcbModelParser.mjs'
@@ -40,8 +54,12 @@ export { PcbOutlineRasterizer } from './core/altium/PcbOutlineRasterizer.mjs'
40
54
  export { PcbOutlineRecovery } from './core/altium/PcbOutlineRecovery.mjs'
41
55
  export { PcbOwnershipGraphBuilder } from './core/altium/PcbOwnershipGraphBuilder.mjs'
42
56
  export { PcbPadStackParser } from './core/altium/PcbPadStackParser.mjs'
57
+ export { PcbPlacedFootprintManifestBuilder } from './core/altium/PcbPlacedFootprintManifestBuilder.mjs'
43
58
  export { PcbPickPlacePositionResolver } from './core/altium/PcbPickPlacePositionResolver.mjs'
59
+ export { PcbPolygonRecordParser } from './core/altium/PcbPolygonRecordParser.mjs'
44
60
  export { PcbRawRecordRegistry } from './core/altium/PcbRawRecordRegistry.mjs'
61
+ export { PcbReviewMetadataBuilder } from './core/altium/PcbReviewMetadataBuilder.mjs'
62
+ export { PcbRouteAnalysisBuilder } from './core/altium/PcbRouteAnalysisBuilder.mjs'
45
63
  export { PcbRuleParser } from './core/altium/PcbRuleParser.mjs'
46
64
  export { PcbSidecarRecordParser } from './core/altium/PcbSidecarRecordParser.mjs'
47
65
  export { PcbSpecialStringResolver } from './core/altium/PcbSpecialStringResolver.mjs'
@@ -52,6 +70,8 @@ export { PcbViaStackParser } from './core/altium/PcbViaStackParser.mjs'
52
70
  export { PcbViaStructureParser } from './core/altium/PcbViaStructureParser.mjs'
53
71
  export { PrintableTextDecoder } from './core/altium/PrintableTextDecoder.mjs'
54
72
  export { PrjPcbModelParser } from './core/altium/PrjPcbModelParser.mjs'
73
+ export { PrjScrModelParser } from './core/altium/PrjScrModelParser.mjs'
74
+ export { ProjectBomPnpReconciliationBuilder } from './core/altium/ProjectBomPnpReconciliationBuilder.mjs'
55
75
  export { SchematicMultipartOwnerMatcher } from './core/altium/SchematicMultipartOwnerMatcher.mjs'
56
76
  export { SchematicCrossSheetConnectorParser } from './core/altium/SchematicCrossSheetConnectorParser.mjs'
57
77
  export { SchematicHarnessParser } from './core/altium/SchematicHarnessParser.mjs'
@@ -63,3 +83,4 @@ export { SchematicRecordTypeRegistry } from './core/altium/SchematicRecordTypeRe
63
83
  export { SchematicRepeatedChannelParser } from './core/altium/SchematicRepeatedChannelParser.mjs'
64
84
  export { SchematicTemplateParser } from './core/altium/SchematicTemplateParser.mjs'
65
85
  export { SchematicStandaloneCalloutNormalizer } from './core/altium/SchematicStandaloneCalloutNormalizer.mjs'
86
+ export { SvgModelCrossLinkValidator } from './core/altium/SvgModelCrossLinkValidator.mjs'
@@ -141,9 +141,21 @@ export class PcbFootprintPrimitiveSelector {
141
141
  * @returns {boolean}
142
142
  */
143
143
  static #includesLayerName(layerName, needle) {
144
+ return PcbFootprintPrimitiveSelector.#compactLayerName(
145
+ layerName
146
+ ).includes(PcbFootprintPrimitiveSelector.#compactLayerName(needle))
147
+ }
148
+
149
+ /**
150
+ * Normalizes layer labels so compact names such as TopOverlay match
151
+ * spaced Altium labels such as Top Overlay.
152
+ * @param {string} layerName
153
+ * @returns {string}
154
+ */
155
+ static #compactLayerName(layerName) {
144
156
  return String(layerName || '')
145
157
  .trim()
146
158
  .toUpperCase()
147
- .includes(needle)
159
+ .replace(/[^A-Z0-9]/g, '')
148
160
  }
149
161
  }
@@ -986,16 +986,38 @@ export class PcbScene3dBuilder {
986
986
  return new Set(
987
987
  (Array.isArray(primitiveLayers) ? primitiveLayers : [])
988
988
  .filter((layer) =>
989
- String(layer?.name || '')
990
- .trim()
991
- .toUpperCase()
992
- .includes(needle)
989
+ PcbScene3dBuilder.#includesLayerName(layer?.name, needle)
993
990
  )
994
991
  .map((layer) => Number(layer.layerId))
995
992
  .filter((layerId) => Number.isInteger(layerId))
996
993
  )
997
994
  }
998
995
 
996
+ /**
997
+ * Returns true when one layer name matches a spaced or compact target.
998
+ * @param {string} layerName
999
+ * @param {string} needle
1000
+ * @returns {boolean}
1001
+ */
1002
+ static #includesLayerName(layerName, needle) {
1003
+ return PcbScene3dBuilder.#compactLayerName(layerName).includes(
1004
+ PcbScene3dBuilder.#compactLayerName(needle)
1005
+ )
1006
+ }
1007
+
1008
+ /**
1009
+ * Normalizes layer labels so compact names such as TopOverlay match
1010
+ * spaced Altium labels such as Top Overlay.
1011
+ * @param {string} layerName
1012
+ * @returns {string}
1013
+ */
1014
+ static #compactLayerName(layerName) {
1015
+ return String(layerName || '')
1016
+ .trim()
1017
+ .toUpperCase()
1018
+ .replace(/[^A-Z0-9]/g, '')
1019
+ }
1020
+
999
1021
  /**
1000
1022
  * Normalizes one Altium overlay text into the runtime stroke-text shape.
1001
1023
  * @param {{ text?: string, value?: string, x?: number, y?: number, height?: number, strokeWidth?: number, rotation?: number, mirrored?: boolean | number | string, isMirrored?: boolean | number | string, mirrorFlag?: boolean | number | string, Mirrored?: boolean | number | string, IsMirrored?: boolean | number | string, MirrorFlag?: boolean | number | string, layerId?: number }} text
@@ -829,6 +829,7 @@ export class PcbSvgRenderer {
829
829
  return {
830
830
  schema: PcbSvgRenderer.#SEMANTIC_SCHEMA,
831
831
  view: PcbSvgRenderer.#buildViewMetadata(pcb, semanticContext),
832
+ lookups: PcbSvgRenderer.#buildSemanticLookups(pcb, semanticContext),
832
833
  boardOutline: {
833
834
  feature: 'board-outline',
834
835
  elementKeys: ['pcb-board-outline', 'pcb-board-outline-stroke']
@@ -887,6 +888,70 @@ export class PcbSvgRenderer {
887
888
  }
888
889
  }
889
890
 
891
+ /**
892
+ * Builds stable lookup maps for semantic SVG consumers.
893
+ * @param {object} pcb Normalized PCB model.
894
+ * @param {object} semanticContext Semantic lookup context.
895
+ * @returns {object}
896
+ */
897
+ static #buildSemanticLookups(pcb, semanticContext) {
898
+ const netsByIndex = {}
899
+ const netIndexByName = {}
900
+ const netClassesByName = {}
901
+ const componentsByIndex = {}
902
+ const componentIndexByDesignator = {}
903
+ const layersByKey = {}
904
+ const layerKeyByDisplayName = {}
905
+
906
+ for (const net of pcb?.nets || []) {
907
+ const netIndex = Number(net?.netIndex)
908
+ if (Number.isInteger(netIndex) && net?.name) {
909
+ netsByIndex[netIndex] = net.name
910
+ netIndexByName[net.name] = netIndex
911
+ }
912
+ }
913
+
914
+ for (const [
915
+ netName,
916
+ classNames
917
+ ] of semanticContext.netClassNamesByNetName) {
918
+ netClassesByName[netName] = [...classNames].sort((left, right) =>
919
+ left.localeCompare(right, undefined, { numeric: true })
920
+ )
921
+ }
922
+
923
+ for (const [
924
+ componentIndex,
925
+ component
926
+ ] of semanticContext.componentsByIndex) {
927
+ componentsByIndex[componentIndex] =
928
+ PcbSvgRenderer.#stripEmptySemanticObject({
929
+ designator: component.designator,
930
+ uniqueId: component.uniqueId,
931
+ pattern: component.pattern
932
+ })
933
+ if (component.designator) {
934
+ componentIndexByDesignator[component.designator] =
935
+ componentIndex
936
+ }
937
+ }
938
+
939
+ for (const layer of semanticContext.layerDescriptors) {
940
+ layersByKey[layer.layerKey] = layer
941
+ layerKeyByDisplayName[layer.displayName] = layer.layerKey
942
+ }
943
+
944
+ return {
945
+ netsByIndex,
946
+ netIndexByName,
947
+ netClassesByName,
948
+ componentsByIndex,
949
+ componentIndexByDesignator,
950
+ layersByKey,
951
+ layerKeyByDisplayName
952
+ }
953
+ }
954
+
890
955
  /**
891
956
  * Builds metadata for the rendered PCB view.
892
957
  * @param {object} pcb Normalized PCB model.