circuitjson-toolkit 1.0.16 → 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 (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -14,7 +14,16 @@ export class CircuitJsonPcbHolePrimitiveModel {
14
14
  static build(element, center) {
15
15
  const shape = CircuitJsonPcbHolePrimitiveModel.#outerShape(element)
16
16
  const points = CircuitJsonPcbHolePrimitiveModel.#outerPoints(element)
17
- const size = CircuitJsonPcbHolePrimitiveModel.#outerSize(element, shape)
17
+ const rotation =
18
+ CircuitJsonPcbHolePrimitiveModel.#outerRotation(element)
19
+ const holeRotation =
20
+ CircuitJsonPcbHolePrimitiveModel.#holeRotation(element)
21
+ const size =
22
+ CircuitJsonPcbHolePrimitiveModel.#polygonOuterSize(
23
+ points,
24
+ center,
25
+ rotation
26
+ ) || CircuitJsonPcbHolePrimitiveModel.#outerSize(element, shape)
18
27
  const hole = CircuitJsonPcbHolePrimitiveModel.#holeSize(
19
28
  element,
20
29
  shape,
@@ -37,10 +46,8 @@ export class CircuitJsonPcbHolePrimitiveModel {
37
46
  holeDiameter: hole.diameter,
38
47
  holeWidth: hole.width,
39
48
  holeHeight: hole.height,
40
- rotation: CircuitJsonUnits.angle(
41
- element.ccw_rotation ?? element.rotation,
42
- 0
43
- ),
49
+ holeRotation,
50
+ rotation,
44
51
  points,
45
52
  bounds
46
53
  }
@@ -53,10 +60,10 @@ export class CircuitJsonPcbHolePrimitiveModel {
53
60
  */
54
61
  static #outerShape(element) {
55
62
  const raw = CircuitJsonPcbHolePrimitiveModel.#shapeText(
56
- element.shape || element.hole_shape
63
+ element.pad_shape || element.shape || element.hole_shape
57
64
  )
58
65
  if (raw.includes('polygon')) return 'polygon'
59
- if (raw.includes('rect')) return 'rect'
66
+ if (raw.includes('rect') || raw.includes('square')) return 'rect'
60
67
  if (
61
68
  raw.includes('pill') ||
62
69
  raw.includes('slot') ||
@@ -73,16 +80,40 @@ export class CircuitJsonPcbHolePrimitiveModel {
73
80
  * @returns {{ width: number, height: number }}
74
81
  */
75
82
  static #outerSize(element, shape) {
83
+ if (element.type === 'pcb_hole') {
84
+ const holeDiameter = CircuitJsonUnits.length(
85
+ element.hole_diameter,
86
+ 0.6
87
+ )
88
+ const holeWidth = CircuitJsonUnits.length(
89
+ element.hole_width,
90
+ holeDiameter
91
+ )
92
+ const holeHeight = CircuitJsonUnits.length(
93
+ element.hole_height,
94
+ shape === 'circle' ? holeWidth : holeDiameter
95
+ )
96
+ return {
97
+ width: holeWidth,
98
+ height: shape === 'circle' ? holeWidth : holeHeight
99
+ }
100
+ }
76
101
  const diameter = CircuitJsonUnits.length(
77
102
  element.diameter ?? element.outer_diameter,
78
103
  0.6
79
104
  )
80
105
  const width = CircuitJsonUnits.length(
81
- element.rect_pad_width ?? element.pad_width ?? element.width,
106
+ element.outer_width ??
107
+ element.rect_pad_width ??
108
+ element.pad_width ??
109
+ element.width,
82
110
  diameter
83
111
  )
84
112
  const height = CircuitJsonUnits.length(
85
- element.rect_pad_height ?? element.pad_height ?? element.height,
113
+ element.outer_height ??
114
+ element.rect_pad_height ??
115
+ element.pad_height ??
116
+ element.height,
86
117
  shape === 'circle' ? width : diameter
87
118
  )
88
119
 
@@ -103,6 +134,41 @@ export class CircuitJsonPcbHolePrimitiveModel {
103
134
  .filter(Boolean)
104
135
  }
105
136
 
137
+ /**
138
+ * Resolves polygon extents in the pad's rotation-local coordinate system.
139
+ * @param {{ x: number, y: number }[]} points Polygon points.
140
+ * @param {{ x: number, y: number }} center Pad center.
141
+ * @param {number} rotation Rotation in degrees.
142
+ * @returns {{ width: number, height: number } | null} Local polygon size.
143
+ */
144
+ static #polygonOuterSize(points, center, rotation) {
145
+ if (points.length < 3) return null
146
+ const centerX = Number(center?.x)
147
+ const centerY = Number(center?.y)
148
+ if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) return null
149
+
150
+ const angle = (-Number(rotation || 0) * Math.PI) / 180
151
+ const cosine = Math.cos(angle)
152
+ const sine = Math.sin(angle)
153
+ let minX = Infinity
154
+ let minY = Infinity
155
+ let maxX = -Infinity
156
+ let maxY = -Infinity
157
+ for (const point of points) {
158
+ const deltaX = point.x - centerX
159
+ const deltaY = point.y - centerY
160
+ const x = deltaX * cosine - deltaY * sine
161
+ const y = deltaX * sine + deltaY * cosine
162
+ minX = Math.min(minX, x)
163
+ minY = Math.min(minY, y)
164
+ maxX = Math.max(maxX, x)
165
+ maxY = Math.max(maxY, y)
166
+ }
167
+ const width = maxX - minX
168
+ const height = maxY - minY
169
+ return width > 0 && height > 0 ? { width, height } : null
170
+ }
171
+
106
172
  /**
107
173
  * Resolves the drilled opening size.
108
174
  * @param {object} element Drilled PCB element.
@@ -148,7 +214,11 @@ export class CircuitJsonPcbHolePrimitiveModel {
148
214
  element.hole_shape || element.shape
149
215
  )
150
216
  if (raw === 'round') return 'circle'
151
- if (raw.includes('rect') && !raw.includes('rect_pad')) return 'rect'
217
+ if (
218
+ raw.includes('square') ||
219
+ (raw.includes('rect') && !raw.includes('rect_pad'))
220
+ )
221
+ return 'rect'
152
222
  if (
153
223
  raw.includes('pill') ||
154
224
  raw.includes('slot') ||
@@ -159,6 +229,34 @@ export class CircuitJsonPcbHolePrimitiveModel {
159
229
  return element.type === 'pcb_hole' ? outerShape : 'circle'
160
230
  }
161
231
 
232
+ /**
233
+ * Resolves the board-space outer pad rotation independently from its drill.
234
+ * @param {object} element Drilled PCB element.
235
+ * @returns {number}
236
+ */
237
+ static #outerRotation(element) {
238
+ return CircuitJsonUnits.angle(
239
+ element.rect_ccw_rotation ??
240
+ element.ccw_rotation ??
241
+ element.rotation,
242
+ 0
243
+ )
244
+ }
245
+
246
+ /**
247
+ * Resolves the board-space drill rotation independently from its outer pad.
248
+ * @param {object} element Drilled PCB element.
249
+ * @returns {number}
250
+ */
251
+ static #holeRotation(element) {
252
+ return CircuitJsonUnits.angle(
253
+ element.hole_ccw_rotation ??
254
+ element.ccw_rotation ??
255
+ element.rotation,
256
+ 0
257
+ )
258
+ }
259
+
162
260
  /**
163
261
  * Normalizes a shape string.
164
262
  * @param {unknown} value Shape value.
@@ -10,7 +10,7 @@ export class CircuitJsonPcbPadPrimitiveModel {
10
10
  * @returns {string}
11
11
  */
12
12
  static shape(element) {
13
- return String(element.shape || 'rect')
13
+ return String(element.legacy_shape || element.shape || 'rect')
14
14
  .trim()
15
15
  .toLowerCase()
16
16
  }
@@ -5,12 +5,9 @@ import { CircuitJsonPcbPrimitiveFields } from './CircuitJsonPcbPrimitiveFields.m
5
5
  import { CircuitJsonPcbTraceLengthModel } from './CircuitJsonPcbTraceLengthModel.mjs'
6
6
  import { CircuitJsonPcbDrawingStyle } from './CircuitJsonPcbDrawingStyle.mjs'
7
7
 
8
- /**
9
- * Builds derived and documentation PCB primitives from CircuitJSON rows.
10
- */
8
+ /** Builds derived and documentation PCB primitives from CircuitJSON rows. */
11
9
  export class CircuitJsonPcbPrimitiveArtwork {
12
- /**
13
- * Builds supplemental primitive rows.
10
+ /** Builds supplemental primitive rows.
14
11
  * @param {{ elementsByType: Map<string, object[]> }} index Element index.
15
12
  * @param {Map<string, object>} componentsByPcbId Component lookup.
16
13
  * @returns {object[]}
@@ -40,8 +37,7 @@ export class CircuitJsonPcbPrimitiveArtwork {
40
37
  ].filter(Boolean)
41
38
  }
42
39
 
43
- /**
44
- * Builds board detail primitives.
40
+ /** Builds board detail primitives.
45
41
  * @param {{ elementsByType: Map<string, object[]> }} index Element index.
46
42
  * @returns {object[]}
47
43
  */
@@ -69,8 +65,7 @@ export class CircuitJsonPcbPrimitiveArtwork {
69
65
  .filter(Boolean)
70
66
  }
71
67
 
72
- /**
73
- * Builds total routed trace length labels.
68
+ /** Builds total routed trace length labels.
74
69
  * @param {object[]} primitives Primitive rows.
75
70
  * @param {{ elementsByType: Map<string, object[]> }} [index] Element index.
76
71
  * @returns {object[]}
@@ -79,8 +74,7 @@ export class CircuitJsonPcbPrimitiveArtwork {
79
74
  return CircuitJsonPcbTraceLengthModel.build(primitives, index)
80
75
  }
81
76
 
82
- /**
83
- * Builds generated copper clearance diagnostics.
77
+ /** Builds generated copper clearance diagnostics.
84
78
  * @param {{ elementsByType: Map<string, object[]> }} index Element index.
85
79
  * @param {object[]} primitives Primitive rows.
86
80
  * @returns {object[]}
@@ -448,7 +442,9 @@ export class CircuitJsonPcbPrimitiveArtwork {
448
442
  id: CircuitJsonPcbPrimitiveFields.derivedId(element),
449
443
  kind,
450
444
  shape: String(
451
- element.shape || (points.length ? 'polygon' : 'rect')
445
+ element.legacy_shape ||
446
+ element.shape ||
447
+ (points.length ? 'polygon' : 'rect')
452
448
  ),
453
449
  x: center?.x ?? bounds.minX + bounds.width / 2,
454
450
  y: center?.y ?? bounds.minY + bounds.height / 2,
@@ -525,21 +521,25 @@ export class CircuitJsonPcbPrimitiveArtwork {
525
521
  * @returns {object | null}
526
522
  */
527
523
  static #linePrimitive(element, kind, index = 0) {
528
- const start = CircuitJsonPcbPrimitiveFields.point({
529
- x: element.x1 ?? element.start?.x,
530
- y: element.y1 ?? element.start?.y
531
- })
532
- const end = CircuitJsonPcbPrimitiveFields.point({
533
- x: element.x2 ?? element.end?.x,
534
- y: element.y2 ?? element.end?.y
535
- })
524
+ const start = CircuitJsonPcbPrimitiveFields.point(
525
+ element.from || {
526
+ x: element.x1 ?? element.start?.x,
527
+ y: element.y1 ?? element.start?.y
528
+ }
529
+ )
530
+ const end = CircuitJsonPcbPrimitiveFields.point(
531
+ element.to || {
532
+ x: element.x2 ?? element.end?.x,
533
+ y: element.y2 ?? element.end?.y
534
+ }
535
+ )
536
536
  if (!start || !end) return null
537
537
  const width = CircuitJsonUnits.length(
538
538
  element.width ??
539
539
  element.stroke_width ??
540
540
  element.strokeWidth ??
541
541
  element.line_width,
542
- 0.08
542
+ kind === 'courtyard' ? 0.05 : 0.08
543
543
  )
544
544
  return CircuitJsonPcbPrimitiveArtwork.#primitive({
545
545
  id: CircuitJsonIndexer.getElementId(element) + ':' + index,
@@ -562,17 +562,18 @@ export class CircuitJsonPcbPrimitiveArtwork {
562
562
  })
563
563
  }
564
564
 
565
- /**
566
- * Returns true when an element should render as open line segments.
567
- * @param {object} element Element row.
568
- * @returns {boolean}
569
- */
565
+ /** Returns whether an element renders as open line segments. */
570
566
  static #isOpenPath(element) {
571
567
  const type = String(element.type || '')
572
- const shape = String(element.shape || '').toLowerCase()
568
+ const shape = String(
569
+ element.legacy_shape || element.shape || ''
570
+ ).toLowerCase()
573
571
  return (
574
572
  type.endsWith('_line') ||
575
573
  type.endsWith('_path') ||
574
+ (type === 'pcb_courtyard_outline' &&
575
+ CircuitJsonPcbPrimitiveFields.linePoints(element).length ===
576
+ 2) ||
576
577
  shape === 'line' ||
577
578
  shape === 'path'
578
579
  )
@@ -651,7 +652,9 @@ export class CircuitJsonPcbPrimitiveArtwork {
651
652
  id: CircuitJsonIndexer.getElementId(element),
652
653
  kind,
653
654
  shape: String(
654
- element.shape || (points.length ? 'polygon' : 'rect')
655
+ element.legacy_shape ||
656
+ element.shape ||
657
+ (points.length ? 'polygon' : 'rect')
655
658
  ),
656
659
  x: center?.x ?? bounds.minX + bounds.width / 2,
657
660
  y: center?.y ?? bounds.minY + bounds.height / 2,
@@ -703,10 +706,15 @@ export class CircuitJsonPcbPrimitiveArtwork {
703
706
  */
704
707
  static #isBoardDetailPath(element) {
705
708
  const type = String(element.type || '')
706
- const shape = String(element.shape || '').toLowerCase()
709
+ const shape = String(
710
+ element.legacy_shape || element.shape || ''
711
+ ).toLowerCase()
707
712
  return (
708
713
  type.endsWith('_line') ||
709
714
  type.endsWith('_path') ||
715
+ (type === 'pcb_courtyard_outline' &&
716
+ CircuitJsonPcbPrimitiveFields.linePoints(element).length ===
717
+ 2) ||
710
718
  shape === 'line' ||
711
719
  shape === 'path'
712
720
  )
@@ -752,7 +760,7 @@ export class CircuitJsonPcbPrimitiveArtwork {
752
760
  element.stroke_width ??
753
761
  element.strokeWidth ??
754
762
  element.line_width,
755
- 0.08
763
+ kind === 'courtyard' ? 0.05 : 0.08
756
764
  )
757
765
 
758
766
  return {
@@ -903,9 +911,11 @@ export class CircuitJsonPcbPrimitiveArtwork {
903
911
  if (kind === 'cutout') return 'cutouts'
904
912
  if (kind === 'keepout') return 'keepouts'
905
913
  if (kind === 'courtyard') {
906
- return CircuitJsonPcbPrimitiveFields.layer(
907
- element.layer || 'top_courtyard'
908
- )
914
+ const side =
915
+ CircuitJsonPcbPrimitiveFields.side(
916
+ CircuitJsonPcbPrimitiveFields.layer(element.layer)
917
+ ) || 'top'
918
+ return side + '_courtyard'
909
919
  }
910
920
  return CircuitJsonPcbPrimitiveFields.layer(element.layer || 'board')
911
921
  }
@@ -981,11 +991,7 @@ export class CircuitJsonPcbPrimitiveArtwork {
981
991
  }
982
992
  }
983
993
 
984
- /**
985
- * Rounds a numeric value for deterministic model rows.
986
- * @param {number} value Numeric value.
987
- * @returns {number}
988
- */
994
+ /** Returns a deterministic rounded numeric value. */
989
995
  static #rounded(value) {
990
996
  return Number(Number(value).toFixed(6))
991
997
  }
@@ -22,10 +22,22 @@ export class CircuitJsonPcbPrimitiveBuilder {
22
22
  * @returns {{ bounds: object, layers: object[], virtualLayers: object[], components: object[], nets: object[], primitives: object[], anchors: object[], diagnostics: object[], airwires: object[], traceLengths: object[], groups: object[], anchorOffsets: object[] }}
23
23
  */
24
24
  static build(documentModel) {
25
+ return CircuitJsonPcbPrimitiveBuilder.buildComplete(
26
+ documentModel,
27
+ CircuitJsonPcbPrimitiveBuilder.buildInteraction(documentModel)
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Builds only geometry and selection data required for interaction. It
33
+ * intentionally omits clearance diagnostics, airwires, and report layers.
34
+ * @param {object | object[]} documentModel Parsed document model.
35
+ * @returns {{ bounds: object, layers: object[], virtualLayers: object[], components: object[], nets: object[], primitives: object[], anchors: object[], diagnostics: object[], airwires: object[], traceLengths: object[], groups: object[], anchorOffsets: object[] }} Interaction primitive model.
36
+ */
37
+ static buildInteraction(documentModel) {
25
38
  const elements = CircuitJsonPcbPrimitiveBuilder.elements(documentModel)
26
39
  const index = CircuitJsonPcbPrimitiveIndex.build(elements)
27
40
  const boards = CircuitJsonPcbPrimitiveBuilder.#all(index, 'pcb_board')
28
- const board = boards[0] || null
29
41
  const components =
30
42
  CircuitJsonPcbPrimitiveBuilder.#componentLookups(index)
31
43
  const areaModel = CircuitJsonPcbZonePrimitiveBuilder.build(
@@ -67,36 +79,65 @@ export class CircuitJsonPcbPrimitiveBuilder {
67
79
  CircuitJsonPcbPrimitiveFields.mergedBoardBounds(boards) ||
68
80
  CircuitJsonPcbPrimitiveGeometry.mergedPrimitiveBounds(primitives) ||
69
81
  CircuitJsonPcbPrimitiveGeometry.bounds(0, 0, 1, 1)
82
+
83
+ return {
84
+ bounds,
85
+ layers: CircuitJsonPcbPrimitiveFields.layers(boards, primitives),
86
+ virtualLayers: [],
87
+ components: components.rows,
88
+ nets: CircuitJsonPcbNetMetadata.nets(primitives, index),
89
+ primitives,
90
+ anchors: primitives.flatMap((primitive) =>
91
+ primitive.anchors.map((anchor) => ({ ...anchor, primitive }))
92
+ ),
93
+ diagnostics: [],
94
+ airwires: [],
95
+ traceLengths: [],
96
+ groups: groupModel.groups,
97
+ anchorOffsets: groupModel.anchorOffsets
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Adds legacy report and overlay data to an interaction primitive model
103
+ * without rebuilding its renderer-neutral geometry.
104
+ * @param {object | object[]} documentModel Parsed document model.
105
+ * @param {Record<string, any>} interactionModel Prepared interaction model.
106
+ * @returns {{ bounds: object, layers: object[], virtualLayers: object[], components: object[], nets: object[], primitives: object[], anchors: object[], diagnostics: object[], airwires: object[], traceLengths: object[], groups: object[], anchorOffsets: object[] }} Complete primitive model.
107
+ */
108
+ static buildComplete(documentModel, interactionModel) {
109
+ const elements = CircuitJsonPcbPrimitiveBuilder.elements(documentModel)
110
+ const index = CircuitJsonPcbPrimitiveIndex.build(elements)
111
+ const components =
112
+ CircuitJsonPcbPrimitiveBuilder.#componentLookups(index)
113
+ const areaDiagnostics = CircuitJsonPcbZonePrimitiveBuilder.build(
114
+ index,
115
+ components.byPcbId
116
+ ).diagnostics
70
117
  const overlays = CircuitJsonPcbPrimitiveOverlays.build(
71
118
  index,
72
119
  components.byPcbId,
73
- primitives,
74
- bounds,
75
- groupModel,
120
+ interactionModel.primitives,
121
+ interactionModel.bounds,
122
+ {
123
+ groups: interactionModel.groups,
124
+ anchorOffsets: interactionModel.anchorOffsets
125
+ },
76
126
  [
77
127
  ...CircuitJsonPcbPrimitiveBuilder.#generatedDiagnostics(index),
78
- ...areaModel.diagnostics
128
+ ...areaDiagnostics
79
129
  ]
80
130
  )
81
131
 
82
132
  return {
83
- bounds,
84
- layers: CircuitJsonPcbPrimitiveFields.layers(board),
133
+ ...interactionModel,
85
134
  virtualLayers: overlays.virtualLayers,
86
- components: components.rows,
87
- nets: CircuitJsonPcbNetMetadata.nets(primitives, index),
88
- primitives,
89
- anchors: primitives.flatMap((primitive) =>
90
- primitive.anchors.map((anchor) => ({ ...anchor, primitive }))
91
- ),
92
135
  diagnostics: overlays.diagnostics,
93
136
  airwires: overlays.airwires,
94
137
  traceLengths: CircuitJsonPcbPrimitiveArtwork.traceLengths(
95
- primitives,
138
+ interactionModel.primitives,
96
139
  index
97
- ),
98
- groups: groupModel.groups,
99
- anchorOffsets: groupModel.anchorOffsets
140
+ )
100
141
  }
101
142
  }
102
143
 
@@ -106,14 +147,74 @@ export class CircuitJsonPcbPrimitiveBuilder {
106
147
  * @returns {object[]}
107
148
  */
108
149
  static elements(documentModel) {
109
- if (Array.isArray(documentModel)) return documentModel
110
- if (Array.isArray(documentModel?.elements))
111
- return documentModel.elements
112
- if (Array.isArray(documentModel?.circuitJson))
113
- return documentModel.circuitJson
150
+ if (Array.isArray(documentModel)) {
151
+ return CircuitJsonPcbPrimitiveBuilder.#elementSlots(documentModel)
152
+ }
153
+ if (!documentModel || typeof documentModel !== 'object') return []
154
+ let descriptors
155
+ try {
156
+ descriptors = Object.getOwnPropertyDescriptors(documentModel)
157
+ } catch {
158
+ return []
159
+ }
160
+ for (const field of ['elements', 'circuitJson']) {
161
+ const descriptor = descriptors[field]
162
+ if (
163
+ descriptor &&
164
+ Object.hasOwn(descriptor, 'value') &&
165
+ Array.isArray(descriptor.value)
166
+ ) {
167
+ return CircuitJsonPcbPrimitiveBuilder.#elementSlots(
168
+ descriptor.value
169
+ )
170
+ }
171
+ }
114
172
  return []
115
173
  }
116
174
 
175
+ /**
176
+ * Returns an exact dense element array, dropping only legacy root metadata.
177
+ * @param {object[]} model Element-array or legacy hybrid-array candidate.
178
+ * @returns {object[]} Original exact array or intrinsic dense slot copy.
179
+ */
180
+ static #elementSlots(model) {
181
+ let prototype
182
+ let descriptors
183
+ try {
184
+ prototype = Object.getPrototypeOf(model)
185
+ descriptors = Object.getOwnPropertyDescriptors(model)
186
+ } catch {
187
+ return model
188
+ }
189
+ const lengthDescriptor = descriptors.length
190
+ const length =
191
+ lengthDescriptor && Object.hasOwn(lengthDescriptor, 'value')
192
+ ? lengthDescriptor.value
193
+ : null
194
+ if (
195
+ prototype !== Array.prototype ||
196
+ !Number.isSafeInteger(length) ||
197
+ length < 0
198
+ ) {
199
+ return model
200
+ }
201
+ const slots = new Array(length)
202
+ for (let index = 0; index < length; index += 1) {
203
+ const descriptor = descriptors[String(index)]
204
+ if (
205
+ !descriptor ||
206
+ !Object.hasOwn(descriptor, 'value') ||
207
+ descriptor.enumerable !== true
208
+ ) {
209
+ return model
210
+ }
211
+ slots[index] = descriptor.value
212
+ }
213
+ return Reflect.ownKeys(descriptors).length === length + 1
214
+ ? model
215
+ : slots
216
+ }
217
+
117
218
  /**
118
219
  * Returns indexed element rows by type.
119
220
  * @param {{ elementsByType: Map<string, object[]> }} index Element index.
@@ -678,7 +779,10 @@ export class CircuitJsonPcbPrimitiveBuilder {
678
779
  element.isKnockout === true,
679
780
  rotation: CircuitJsonUnits.angle(element.ccw_rotation, 0),
680
781
  bounds,
681
- layer: CircuitJsonPcbPrimitiveFields.layer(element.layer),
782
+ layer: CircuitJsonPcbPrimitiveBuilder.#detailLayer(
783
+ element,
784
+ CircuitJsonPcbPrimitiveBuilder.#textKind(element)
785
+ ),
682
786
  component,
683
787
  netName: CircuitJsonPcbPrimitiveFields.netName(
684
788
  element,
@@ -815,13 +919,27 @@ export class CircuitJsonPcbPrimitiveBuilder {
815
919
  * @returns {string}
816
920
  */
817
921
  static #lineLayer(element) {
818
- if (element.layer) {
922
+ return CircuitJsonPcbPrimitiveBuilder.#detailLayer(
923
+ element,
924
+ CircuitJsonPcbPrimitiveBuilder.#lineKind(element)
925
+ )
926
+ }
927
+
928
+ /**
929
+ * Resolves a standards-side layer into its drawing-specific virtual layer.
930
+ * @param {object} element Source drawing element.
931
+ * @param {string} kind Primitive kind.
932
+ * @returns {string}
933
+ */
934
+ static #detailLayer(element, kind) {
935
+ if (kind === 'copper-text') {
819
936
  return CircuitJsonPcbPrimitiveFields.layer(element.layer)
820
937
  }
821
- return CircuitJsonPcbPrimitiveBuilder.#lineKind(element) ===
822
- 'fabrication'
823
- ? 'top_fabrication'
824
- : 'top_silkscreen'
938
+ const side =
939
+ CircuitJsonPcbPrimitiveFields.side(
940
+ CircuitJsonPcbPrimitiveFields.layer(element.layer)
941
+ ) || 'top'
942
+ return side + (kind === 'fabrication' ? '_fabrication' : '_silkscreen')
825
943
  }
826
944
 
827
945
  /**
@@ -2,6 +2,15 @@ import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
2
2
  import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
3
3
  import { CircuitJsonPcbPrimitiveGeometry } from './CircuitJsonPcbPrimitiveGeometry.mjs'
4
4
 
5
+ const PHYSICAL_COPPER_KINDS = new Set([
6
+ 'copper-text',
7
+ 'pad',
8
+ 'thermal-spoke',
9
+ 'track',
10
+ 'via',
11
+ 'zone'
12
+ ])
13
+
5
14
  /**
6
15
  * Resolves shared CircuitJSON PCB primitive fields from common element shapes.
7
16
  */
@@ -47,13 +56,42 @@ export class CircuitJsonPcbPrimitiveFields {
47
56
 
48
57
  /**
49
58
  * Builds copper layer rows from board metadata.
50
- * @param {object | null} board Board element.
59
+ * @param {object | object[] | null} boardOrBoards Board element or rows.
60
+ * @param {object[]} [primitives] Prepared primitives for observed layer union.
51
61
  * @returns {object[]}
52
62
  */
53
- static layers(board) {
63
+ static layers(boardOrBoards, primitives = []) {
64
+ const boards = Array.isArray(boardOrBoards)
65
+ ? boardOrBoards
66
+ : boardOrBoards
67
+ ? [boardOrBoards]
68
+ : []
69
+ let declaredCount = boards.length ? 1 : 2
70
+ for (const board of boards) {
71
+ declaredCount = Math.max(
72
+ declaredCount,
73
+ Math.max(
74
+ 1,
75
+ Math.min(
76
+ 8,
77
+ Math.round(
78
+ CircuitJsonUnits.length(board?.num_layers, 4)
79
+ )
80
+ )
81
+ )
82
+ )
83
+ if (declaredCount === 8) break
84
+ }
85
+ const observed =
86
+ CircuitJsonPcbPrimitiveFields.#observedCopperLayers(primitives)
87
+ const maximumInner = [...observed].reduce((maximum, layer) => {
88
+ const match = /^inner([1-6])$/u.exec(layer)
89
+ return match ? Math.max(maximum, Number(match[1])) : maximum
90
+ }, 0)
54
91
  const count = Math.max(
55
- 1,
56
- Math.round(CircuitJsonUnits.length(board?.num_layers, 2))
92
+ declaredCount,
93
+ maximumInner ? maximumInner + 2 : 1,
94
+ observed.has('bottom') ? 2 : 1
57
95
  )
58
96
  const keys =
59
97
  count === 1
@@ -79,6 +117,34 @@ export class CircuitJsonPcbPrimitiveFields {
79
117
  }))
80
118
  }
81
119
 
120
+ /**
121
+ * Resolves every structurally physical layer referenced by copper geometry.
122
+ * @param {object[]} primitives Prepared PCB primitives.
123
+ * @returns {Set<string>} Observed physical layer ids.
124
+ */
125
+ static #observedCopperLayers(primitives) {
126
+ const result = new Set()
127
+ for (const primitive of primitives || []) {
128
+ if (!PHYSICAL_COPPER_KINDS.has(primitive.kind)) continue
129
+ const source = primitive.sourceRoute || primitive.source || {}
130
+ const candidates = [
131
+ primitive.layer,
132
+ source.from_layer,
133
+ source.fromLayer,
134
+ source.to_layer,
135
+ source.toLayer,
136
+ ...(Array.isArray(source.layers) ? source.layers : [])
137
+ ]
138
+ for (const candidate of candidates) {
139
+ const layer = CircuitJsonPcbPrimitiveFields.layer(candidate)
140
+ if (/^(?:top|bottom|inner[1-6])$/u.test(layer)) {
141
+ result.add(layer)
142
+ }
143
+ }
144
+ }
145
+ return result
146
+ }
147
+
82
148
  /**
83
149
  * Resolves a center point.
84
150
  * @param {object} element Element row.