circuitjson-toolkit 1.0.17 → 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 (154) hide show
  1. package/NOTICE.md +31 -0
  2. package/README.md +267 -107
  3. package/docs/api.md +501 -96
  4. package/docs/capabilities.md +70 -0
  5. package/docs/migration/behaviors.md +45 -0
  6. package/docs/migration/parser.md +60 -0
  7. package/docs/migration/renderers.md +515 -0
  8. package/docs/migration/root.md +740 -0
  9. package/docs/migration.md +120 -0
  10. package/docs/model-format.md +175 -57
  11. package/docs/provenance.md +206 -0
  12. package/docs/release-notes-v1.1.0.md +154 -0
  13. package/docs/testing.md +117 -7
  14. package/package.json +31 -5
  15. package/spec/api-baseline-v1.0.17.json +1 -0
  16. package/spec/baseline-provenance-v1.0.17.json +7 -0
  17. package/spec/circuitjson-schema-snapshot.json +321 -0
  18. package/spec/circuitjson-schema-source.json +28 -0
  19. package/spec/feature-preservation.json +1 -0
  20. package/spec/library-scope.md +27 -20
  21. package/src/capabilities.mjs +1 -0
  22. package/src/core/ArchiveEntryPath.mjs +93 -0
  23. package/src/core/ArchiveLimits.mjs +31 -0
  24. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  25. package/src/core/AsyncInputOwnership.mjs +56 -0
  26. package/src/core/AttachedValueLimits.mjs +67 -0
  27. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  28. package/src/core/CircuitJsonDocument.mjs +19 -61
  29. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  30. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  31. package/src/core/CircuitJsonIndexer.mjs +274 -194
  32. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  33. package/src/core/CircuitJsonParser.mjs +75 -13
  34. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  35. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  36. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  37. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  38. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  39. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  40. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  41. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  42. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  43. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  44. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  45. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  46. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  47. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  48. package/src/core/CircuitJsonUnits.mjs +13 -87
  49. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  50. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  51. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  52. package/src/core/ManufacturingService.mjs +323 -0
  53. package/src/core/Parser.mjs +343 -0
  54. package/src/core/ParserOptions.mjs +333 -0
  55. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  56. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  57. package/src/core/PcbInteractionIndex.mjs +368 -0
  58. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  59. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  60. package/src/core/ProjectLoader.mjs +975 -0
  61. package/src/core/SimulationService.mjs +790 -0
  62. package/src/core/ToolkitCapabilities.mjs +130 -0
  63. package/src/core/ZipArchiveInspector.mjs +649 -0
  64. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  65. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  66. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  67. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  68. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  69. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  70. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  71. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  72. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  73. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  74. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  75. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  76. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  77. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  78. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  79. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  80. package/src/core/contracts/DocumentResult.mjs +198 -0
  81. package/src/core/contracts/ProjectResult.mjs +96 -0
  82. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  83. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  84. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  85. package/src/core/contracts/ToolkitError.mjs +176 -0
  86. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  87. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  88. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  89. package/src/core/query/CircuitTraversal.mjs +343 -0
  90. package/src/core/query/ComponentGrouping.mjs +275 -0
  91. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  92. package/src/core/query/QueryService.mjs +435 -0
  93. package/src/core/query/RegexPattern.mjs +75 -0
  94. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  95. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  96. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  97. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  98. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  99. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  100. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  101. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  102. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  103. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  104. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  105. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  106. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  107. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  108. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  109. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  110. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  111. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  112. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  113. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  114. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  115. package/src/core/worker/WorkerRequestData.mjs +623 -0
  116. package/src/core/worker/WorkerResponseData.mjs +905 -0
  117. package/src/extensions.mjs +37 -0
  118. package/src/index.mjs +14 -9
  119. package/src/interaction.mjs +2 -0
  120. package/src/manufacturing.mjs +1 -0
  121. package/src/parser.mjs +12 -2
  122. package/src/project.mjs +5 -0
  123. package/src/query.mjs +1 -0
  124. package/src/renderers.mjs +3 -29
  125. package/src/scene3d.mjs +3 -0
  126. package/src/simulation.mjs +1 -0
  127. package/src/styles/renderers.css +24 -0
  128. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  129. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  130. package/src/testing/runToolkitContract.mjs +705 -0
  131. package/src/testing.mjs +3 -0
  132. package/src/ui/BomTableRenderer.mjs +304 -0
  133. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  134. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  135. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  136. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  137. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  138. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  139. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  140. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  141. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  142. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  143. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  144. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  145. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  146. package/src/ui/PcbSvgRenderer.mjs +41 -0
  147. package/src/ui/SafeSvgPaint.mjs +26 -0
  148. package/src/ui/SafeXmlText.mjs +60 -0
  149. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  150. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  151. package/src/ui/SchematicTableGeometry.mjs +319 -0
  152. package/src/ui/SchematicTextAnchor.mjs +55 -0
  153. package/src/ui/SchematicTextBounds.mjs +98 -0
  154. package/src/workers/parser.worker.mjs +59 -0
@@ -0,0 +1,847 @@
1
+ import { CircuitJsonUnits } from '../CircuitJsonUnits.mjs'
2
+ import { CircuitJsonSchematicTableNormalizer } from './CircuitJsonSchematicTableNormalizer.mjs'
3
+
4
+ const VISIBLE_LAYERS = new Set([
5
+ 'top',
6
+ 'bottom',
7
+ 'inner1',
8
+ 'inner2',
9
+ 'inner3',
10
+ 'inner4',
11
+ 'inner5',
12
+ 'inner6'
13
+ ])
14
+ const EMPTY_LIST = Object.freeze([])
15
+ const UPSTREAM_SUPPLIERS = new Set([
16
+ 'jlcpcb',
17
+ 'macrofab',
18
+ 'pcbway',
19
+ 'digikey',
20
+ 'mouser',
21
+ 'lcsc'
22
+ ])
23
+ const SIDE_LAYER_TYPES = new Set([
24
+ 'pcb_copper_text',
25
+ 'pcb_fabrication_note_dimension',
26
+ 'pcb_fabrication_note_path',
27
+ 'pcb_fabrication_note_rect',
28
+ 'pcb_fabrication_note_text',
29
+ 'pcb_note_dimension',
30
+ 'pcb_note_line',
31
+ 'pcb_note_path',
32
+ 'pcb_note_rect',
33
+ 'pcb_note_text',
34
+ 'pcb_silkscreen_circle',
35
+ 'pcb_silkscreen_graphic',
36
+ 'pcb_silkscreen_line',
37
+ 'pcb_silkscreen_oval',
38
+ 'pcb_silkscreen_path',
39
+ 'pcb_silkscreen_pill',
40
+ 'pcb_silkscreen_rect',
41
+ 'pcb_silkscreen_text'
42
+ ])
43
+ const COMPONENT_ARTWORK_TYPES = new Set([
44
+ 'pcb_copper_text',
45
+ 'pcb_fabrication_note_dimension',
46
+ 'pcb_fabrication_note_path',
47
+ 'pcb_fabrication_note_rect',
48
+ 'pcb_fabrication_note_text',
49
+ 'pcb_silkscreen_circle',
50
+ 'pcb_silkscreen_graphic',
51
+ 'pcb_silkscreen_line',
52
+ 'pcb_silkscreen_oval',
53
+ 'pcb_silkscreen_path',
54
+ 'pcb_silkscreen_pill',
55
+ 'pcb_silkscreen_rect',
56
+ 'pcb_silkscreen_text'
57
+ ])
58
+ const COURTYARD_TYPES = new Set([
59
+ 'pcb_courtyard_circle',
60
+ 'pcb_courtyard_outline',
61
+ 'pcb_courtyard_pill',
62
+ 'pcb_courtyard_polygon',
63
+ 'pcb_courtyard_rect'
64
+ ])
65
+ const PCB_ROUTE_PATH_TYPES = new Set([
66
+ 'pcb_fabrication_note_path',
67
+ 'pcb_note_path',
68
+ 'pcb_silkscreen_path'
69
+ ])
70
+
71
+ /**
72
+ * Projects explicitly supported pre-union shapes onto valid CircuitJSON.
73
+ */
74
+ export class CircuitJsonLegacyNormalizer {
75
+ /**
76
+ * Normalizes a dense safe model while preserving already canonical rows.
77
+ * @param {unknown} model CircuitJSON model.
78
+ * @param {{ owned?: boolean }} [options] Ownership options.
79
+ * @returns {unknown} Original model or a standards-shaped replacement.
80
+ */
81
+ static normalize(model, options = {}) {
82
+ if (options.owned === true) {
83
+ if (!Array.isArray(model)) return model
84
+ let hasSchematicTable = false
85
+ for (let index = 0; index < model.length; index += 1) {
86
+ const fields = model[index]
87
+ const type = fields?.type
88
+ if (
89
+ !fields ||
90
+ typeof fields !== 'object' ||
91
+ typeof type !== 'string'
92
+ ) {
93
+ continue
94
+ }
95
+ if (type === 'schematic_table') hasSchematicTable = true
96
+ CircuitJsonLegacyNormalizer.#fields(fields, type)
97
+ }
98
+ if (hasSchematicTable) {
99
+ CircuitJsonSchematicTableNormalizer.normalize(model, true)
100
+ }
101
+ return model
102
+ }
103
+ const descriptors = CircuitJsonLegacyNormalizer.#array(model)
104
+ if (!descriptors) return model
105
+ const length = descriptors.length.value
106
+ const rows = options.owned === true ? model : new Array(length)
107
+ let changed = false
108
+ for (let index = 0; index < length; index += 1) {
109
+ const original = descriptors[String(index)].value
110
+ const normalized = CircuitJsonLegacyNormalizer.#element(
111
+ original,
112
+ options.owned === true
113
+ )
114
+ rows[index] = normalized
115
+ if (normalized !== original) changed = true
116
+ }
117
+ if (CircuitJsonSchematicTableNormalizer.normalize(rows, false)) {
118
+ changed = true
119
+ }
120
+ return changed || options.owned === true ? rows : model
121
+ }
122
+
123
+ /** @param {unknown} element Element candidate. @param {boolean} owned Owned input. @returns {unknown} */
124
+ static #element(element, owned) {
125
+ const descriptors = CircuitJsonLegacyNormalizer.#record(element)
126
+ const type = descriptors?.type?.value
127
+ if (!descriptors || typeof type !== 'string') return element
128
+ const fields = Object.fromEntries(
129
+ Object.entries(descriptors).map(([key, descriptor]) => [
130
+ key,
131
+ descriptor.value
132
+ ])
133
+ )
134
+ CircuitJsonLegacyNormalizer.#fields(fields, type)
135
+ const keys = Object.keys(fields)
136
+ const changed =
137
+ keys.length !== Object.keys(descriptors).length ||
138
+ keys.some(
139
+ (key) =>
140
+ !descriptors[key] || fields[key] !== descriptors[key].value
141
+ )
142
+ if (!changed) return element
143
+ if (owned) {
144
+ for (const key of Object.keys(element)) delete element[key]
145
+ Object.assign(element, fields)
146
+ return element
147
+ }
148
+ return fields
149
+ }
150
+
151
+ /** @param {Record<string, any>} fields Element fields. @param {string} type Type. @returns {void} */
152
+ static #fields(fields, type) {
153
+ CircuitJsonLegacyNormalizer.#common(fields, type)
154
+ if (type.startsWith('pcb_')) {
155
+ CircuitJsonLegacyNormalizer.#pcb(fields, type)
156
+ } else if (type.startsWith('schematic_')) {
157
+ CircuitJsonLegacyNormalizer.#schematic(fields, type)
158
+ }
159
+ }
160
+
161
+ /** @param {Record<string, any>} fields Element fields. @param {string} type Type. @returns {void} */
162
+ static #common(fields, type) {
163
+ if (type === 'source_component') {
164
+ fields.ftype ??=
165
+ CircuitJsonLegacyNormalizer.#sourceComponentFtype(fields)
166
+ CircuitJsonLegacyNormalizer.#supplierNumbers(fields)
167
+ }
168
+ if (
169
+ type === 'source_net' &&
170
+ fields.member_source_group_ids === undefined
171
+ ) {
172
+ fields.member_source_group_ids = EMPTY_LIST
173
+ }
174
+ if (type === 'source_trace') {
175
+ fields.connected_source_port_ids ??= EMPTY_LIST
176
+ fields.connected_source_net_ids ??= EMPTY_LIST
177
+ }
178
+ if (type === 'source_port') fields.name ??= ''
179
+ if (type === 'source_component_pins_underspecified_warning') {
180
+ fields.source_port_ids ??=
181
+ fields.source_port_id === undefined
182
+ ? EMPTY_LIST
183
+ : [fields.source_port_id]
184
+ }
185
+ if (type === 'cad_component') {
186
+ fields.pcb_component_id ??= ''
187
+ fields.source_component_id ??= ''
188
+ fields.position ??= { x: 0, y: 0, z: 0 }
189
+ }
190
+ }
191
+
192
+ /** @param {Record<string, any>} fields Element fields. @param {string} type Type. @returns {void} */
193
+ static #pcb(fields, type) {
194
+ CircuitJsonLegacyNormalizer.#courtyard(fields, type)
195
+ const normalizedType = fields.type
196
+ CircuitJsonLegacyNormalizer.#padClearanceDiagnostic(
197
+ fields,
198
+ normalizedType
199
+ )
200
+ if (normalizedType === 'pcb_board' && !Array.isArray(fields.outline)) {
201
+ if (Array.isArray(fields.outline?.points)) {
202
+ fields.outline = fields.outline.points
203
+ }
204
+ }
205
+ if (normalizedType === 'pcb_component') {
206
+ fields.rotation ??= 0
207
+ fields.width ??= 0
208
+ fields.height ??= 0
209
+ }
210
+ if (normalizedType === 'pcb_group') {
211
+ fields.pcb_component_ids ??= EMPTY_LIST
212
+ }
213
+ if (normalizedType === 'pcb_smtpad') {
214
+ CircuitJsonLegacyNormalizer.#padLengths(fields)
215
+ if (fields.shape === 'rounded_rect') {
216
+ fields.legacy_shape = fields.shape
217
+ fields.shape = 'rect'
218
+ }
219
+ if (fields.shape === 'circle' && fields.radius === undefined) {
220
+ fields.radius =
221
+ fields.diameter !== undefined
222
+ ? Number(fields.diameter) / 2
223
+ : Math.min(
224
+ Number(fields.width),
225
+ Number(fields.height)
226
+ ) / 2
227
+ }
228
+ if (
229
+ ['pill', 'rotated_pill'].includes(fields.shape) &&
230
+ fields.radius === undefined
231
+ ) {
232
+ fields.radius =
233
+ Math.min(Number(fields.width), Number(fields.height)) / 2
234
+ }
235
+ }
236
+ if (normalizedType === 'pcb_plated_hole') {
237
+ fields.layers ??= fields.layer ? [fields.layer] : ['top', 'bottom']
238
+ if (fields.shape === 'circular_hole_with_rect_pad') {
239
+ fields.hole_shape ??= 'circle'
240
+ fields.pad_shape ??= 'rect'
241
+ }
242
+ }
243
+ if (normalizedType === 'pcb_via') {
244
+ fields.layers = CircuitJsonLegacyNormalizer.#layers(fields.layers, [
245
+ fields.from_layer ?? 'top',
246
+ fields.to_layer ?? 'bottom'
247
+ ])
248
+ if (fields.from_layer !== undefined) {
249
+ fields.from_layer = CircuitJsonLegacyNormalizer.#side(
250
+ fields.from_layer
251
+ )
252
+ }
253
+ if (fields.to_layer !== undefined) {
254
+ fields.to_layer = CircuitJsonLegacyNormalizer.#side(
255
+ fields.to_layer
256
+ )
257
+ }
258
+ }
259
+ if (normalizedType === 'pcb_hole') {
260
+ if (fields.hole_shape === 'round') fields.hole_shape = 'circle'
261
+ if (fields.hole_shape === 'circle_or_square') {
262
+ fields.hole_shape = 'circle'
263
+ }
264
+ if (['circle', 'square'].includes(fields.hole_shape)) {
265
+ fields.hole_diameter ??=
266
+ fields.diameter ?? fields.width ?? fields.height ?? 0
267
+ } else {
268
+ fields.hole_width ??= fields.width ?? fields.diameter ?? 0
269
+ fields.hole_height ??= fields.height ?? fields.diameter ?? 0
270
+ }
271
+ }
272
+ if (normalizedType === 'pcb_keepout') {
273
+ fields.shape ??= fields.radius !== undefined ? 'circle' : 'rect'
274
+ fields.layers ??= fields.layer ? [fields.layer] : ['top']
275
+ if (fields.shape === 'polygon' && Array.isArray(fields.points)) {
276
+ const bounds = CircuitJsonLegacyNormalizer.#pointBounds(
277
+ fields.points
278
+ )
279
+ if (bounds) {
280
+ fields.legacy_shape = 'polygon'
281
+ fields.shape = 'rect'
282
+ fields.center = {
283
+ x: (bounds.minX + bounds.maxX) / 2,
284
+ y: (bounds.minY + bounds.maxY) / 2
285
+ }
286
+ fields.width = bounds.maxX - bounds.minX
287
+ fields.height = bounds.maxY - bounds.minY
288
+ }
289
+ }
290
+ }
291
+ if (normalizedType === 'pcb_cutout') {
292
+ fields.shape ??= Array.isArray(fields.points)
293
+ ? 'polygon'
294
+ : Array.isArray(fields.route)
295
+ ? 'path'
296
+ : fields.radius !== undefined
297
+ ? 'circle'
298
+ : 'rect'
299
+ if (fields.shape === 'path') {
300
+ fields.slot_width ??= fields.width ?? 0
301
+ }
302
+ }
303
+ if (normalizedType === 'pcb_port') {
304
+ fields.layers ??= fields.layer ? [fields.layer] : ['top']
305
+ }
306
+ if (normalizedType === 'pcb_copper_pour' && fields.shape === 'brep') {
307
+ CircuitJsonLegacyNormalizer.#brep(fields)
308
+ }
309
+ if (normalizedType === 'pcb_copper_pour') {
310
+ fields.layer = CircuitJsonLegacyNormalizer.#side(fields.layer)
311
+ }
312
+ if (normalizedType === 'pcb_solder_paste') {
313
+ fields.x ??= fields.center?.x
314
+ fields.y ??= fields.center?.y
315
+ fields.shape ??= fields.radius !== undefined ? 'circle' : 'rect'
316
+ if (fields.shape === 'pill') {
317
+ fields.radius ??=
318
+ Math.min(Number(fields.width), Number(fields.height)) / 2
319
+ }
320
+ }
321
+ if (normalizedType === 'pcb_breakout_point') {
322
+ fields.pcb_group_id ??= ''
323
+ fields.x ??= fields.center?.x ?? 0
324
+ fields.y ??= fields.center?.y ?? 0
325
+ }
326
+ if (normalizedType === 'pcb_thermal_spoke') {
327
+ fields.pcb_ground_plane_id ??= ''
328
+ fields.shape ??= 'spokes'
329
+ fields.spoke_count ??= 4
330
+ fields.spoke_thickness ??= fields.width ?? 0
331
+ fields.spoke_inner_diameter ??= 0
332
+ fields.spoke_outer_diameter ??= 0
333
+ }
334
+ if (normalizedType === 'pcb_trace_error') {
335
+ fields.error_type = 'pcb_trace_error'
336
+ fields.pcb_trace_id ??= ''
337
+ fields.source_trace_id ??= ''
338
+ fields.pcb_component_ids ??=
339
+ fields.pcb_component_id === undefined
340
+ ? []
341
+ : [fields.pcb_component_id]
342
+ fields.pcb_port_ids ??= []
343
+ }
344
+ if (normalizedType === 'pcb_trace') {
345
+ fields.route = CircuitJsonLegacyNormalizer.#traceRoute(fields.route)
346
+ }
347
+ if (
348
+ normalizedType === 'pcb_silkscreen_circle' &&
349
+ fields.center === undefined &&
350
+ (fields.x !== undefined || fields.y !== undefined)
351
+ ) {
352
+ fields.center = { x: fields.x ?? 0, y: fields.y ?? 0 }
353
+ }
354
+ if (normalizedType === 'pcb_silkscreen_oval') {
355
+ if (fields.radius_x === undefined && fields.width !== undefined) {
356
+ fields.radius_x = Number(fields.width) / 2
357
+ }
358
+ if (fields.radius_y === undefined && fields.height !== undefined) {
359
+ fields.radius_y = Number(fields.height) / 2
360
+ }
361
+ }
362
+ if (normalizedType === 'pcb_courtyard_outline') {
363
+ fields.outline ??=
364
+ fields.points ?? fields.route ?? fields.path ?? []
365
+ fields.stroke_width ??= fields.width
366
+ delete fields.points
367
+ delete fields.route
368
+ delete fields.path
369
+ delete fields.width
370
+ }
371
+ if (COURTYARD_TYPES.has(normalizedType)) {
372
+ fields.pcb_component_id ??= ''
373
+ fields.layer = CircuitJsonLegacyNormalizer.#side(fields.layer)
374
+ }
375
+ if (COMPONENT_ARTWORK_TYPES.has(normalizedType)) {
376
+ fields.pcb_component_id ??= ''
377
+ }
378
+ if (SIDE_LAYER_TYPES.has(normalizedType)) {
379
+ fields.layer = CircuitJsonLegacyNormalizer.#side(fields.layer)
380
+ }
381
+ if (PCB_ROUTE_PATH_TYPES.has(normalizedType)) {
382
+ fields.route ??= fields.points ?? fields.path ?? []
383
+ fields.stroke_width ??= fields.width ?? 0
384
+ delete fields.points
385
+ delete fields.path
386
+ delete fields.width
387
+ }
388
+ if (normalizedType === 'pcb_fabrication_note_dimension') {
389
+ fields.from ??= fields.start
390
+ fields.to ??= fields.end
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Projects legacy string pad dimensions onto upstream numeric pad fields.
396
+ * @param {Record<string, any>} fields Pad fields.
397
+ * @returns {void}
398
+ */
399
+ static #padLengths(fields) {
400
+ for (const key of [
401
+ 'width',
402
+ 'height',
403
+ 'diameter',
404
+ 'radius',
405
+ 'corner_radius',
406
+ 'rect_border_radius',
407
+ 'soldermask_margin',
408
+ 'soldermask_margin_top',
409
+ 'soldermask_margin_right',
410
+ 'soldermask_margin_bottom',
411
+ 'soldermask_margin_left'
412
+ ]) {
413
+ if (typeof fields[key] !== 'string') continue
414
+ const value = CircuitJsonUnits.optionalLength(fields[key])
415
+ if (value !== null) fields[key] = value
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Projects historical SMT-pad diagnostic relations onto upstream pad ids.
421
+ * @param {Record<string, any>} fields Diagnostic fields.
422
+ * @param {string} type Diagnostic type.
423
+ * @returns {void}
424
+ */
425
+ static #padClearanceDiagnostic(fields, type) {
426
+ if (type === 'pcb_pad_pad_clearance_error') {
427
+ fields.pcb_pad_ids ??=
428
+ fields.pcb_smtpad_ids ??
429
+ [fields.pcb_smtpad_id, fields.pcb_pad_id].filter(
430
+ (value) => typeof value === 'string' && value
431
+ )
432
+ fields.error_type = type
433
+ delete fields.pcb_smtpad_ids
434
+ delete fields.pcb_smtpad_id
435
+ delete fields.pcb_pad_id
436
+ return
437
+ }
438
+ if (type === 'pcb_pad_trace_clearance_error') {
439
+ fields.pcb_pad_id ??= fields.pcb_smtpad_id
440
+ fields.error_type = type
441
+ delete fields.pcb_smtpad_id
442
+ return
443
+ }
444
+ if (type === 'pcb_via_clearance_error') {
445
+ fields.pcb_error_id ??= fields.pcb_via_clearance_error_id
446
+ fields.error_type = type
447
+ delete fields.pcb_via_clearance_error_id
448
+ return
449
+ }
450
+ if (type === 'pcb_via_trace_clearance_error') {
451
+ fields.error_type = type
452
+ }
453
+ }
454
+
455
+ /** @param {Record<string, any>} fields Element fields. @param {string} type Type. @returns {void} */
456
+ static #courtyard(fields, type) {
457
+ if (
458
+ ![
459
+ 'pcb_courtyard',
460
+ 'pcb_courtyard_line',
461
+ 'pcb_courtyard_path'
462
+ ].includes(type)
463
+ ) {
464
+ return
465
+ }
466
+ if (type === 'pcb_courtyard') {
467
+ fields.type = 'pcb_courtyard_rect'
468
+ fields.pcb_courtyard_rect_id = fields.pcb_courtyard_id
469
+ delete fields.pcb_courtyard_id
470
+ } else {
471
+ fields.type = 'pcb_courtyard_outline'
472
+ fields.legacy_shape =
473
+ type === 'pcb_courtyard_path' ? 'path' : 'line'
474
+ fields.pcb_courtyard_outline_id =
475
+ fields.pcb_courtyard_line_id ?? fields.pcb_courtyard_path_id
476
+ fields.stroke_width ??= fields.width
477
+ fields.outline =
478
+ type === 'pcb_courtyard_line'
479
+ ? [
480
+ fields.start ?? { x: fields.x1, y: fields.y1 },
481
+ fields.end ?? { x: fields.x2, y: fields.y2 }
482
+ ]
483
+ : (fields.route ?? fields.points ?? fields.path ?? [])
484
+ delete fields.pcb_courtyard_line_id
485
+ delete fields.pcb_courtyard_path_id
486
+ delete fields.start
487
+ delete fields.end
488
+ delete fields.x1
489
+ delete fields.y1
490
+ delete fields.x2
491
+ delete fields.y2
492
+ delete fields.route
493
+ delete fields.points
494
+ delete fields.path
495
+ delete fields.width
496
+ }
497
+ fields.pcb_component_id ??= ''
498
+ fields.layer = CircuitJsonLegacyNormalizer.#side(fields.layer)
499
+ }
500
+
501
+ /** @param {Record<string, any>} fields Element fields. @param {string} type Type. @returns {void} */
502
+ static #schematic(fields, type) {
503
+ if (type === 'schematic_trace') {
504
+ fields.junctions ??= []
505
+ fields.edges ??= []
506
+ if (
507
+ fields.start &&
508
+ fields.end &&
509
+ !fields.edges.some((edge) => edge?.from && edge?.to)
510
+ ) {
511
+ fields.edges = [{ from: fields.start, to: fields.end }]
512
+ }
513
+ }
514
+ if (type === 'schematic_arc') {
515
+ fields.center ??= { x: 0, y: 0 }
516
+ fields.radius ??= 0
517
+ if (
518
+ fields.direction === undefined &&
519
+ (fields.start_angle !== undefined ||
520
+ fields.end_angle !== undefined)
521
+ ) {
522
+ fields.direction = 'clockwise'
523
+ }
524
+ fields.start_angle_degrees ??= fields.start_angle ?? 0
525
+ fields.end_angle_degrees ??= fields.end_angle ?? 0
526
+ }
527
+ if (type === 'schematic_layout_error') {
528
+ fields.message ??= ''
529
+ fields.schematic_group_id ??= ''
530
+ fields.source_group_id ??= ''
531
+ }
532
+ if (type === 'schematic_group') {
533
+ fields.source_group_id ??= ''
534
+ fields.width ??= 0
535
+ fields.height ??= 0
536
+ fields.center ??= { x: 0, y: 0 }
537
+ fields.schematic_component_ids ??= []
538
+ }
539
+ if (type === 'schematic_debug_object') {
540
+ const shapeWasOmitted = fields.shape === undefined
541
+ fields.shape ??= fields.start && fields.end ? 'line' : 'rect'
542
+ if (shapeWasOmitted) fields.legacy_shape_omitted = true
543
+ fields.label ??= fields.message
544
+ if (fields.shape === 'rect') {
545
+ fields.center ??= { x: fields.x ?? 0, y: fields.y ?? 0 }
546
+ fields.size ??= {
547
+ width: fields.width ?? 0,
548
+ height: fields.height ?? 0
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Resolves one legacy layer reference without invoking caller coercion or
556
+ * accessors. Numeric Gerber layer ids use the conventional outer-layer
557
+ * aliases 1 (top) and 32 (bottom).
558
+ * @param {unknown} value Layer value.
559
+ * @returns {string} Canonical visible side.
560
+ */
561
+ static #side(value) {
562
+ const descriptors = CircuitJsonLegacyNormalizer.#record(value)
563
+ if (descriptors) {
564
+ const name = descriptors.name
565
+ return name && Object.hasOwn(name, 'value')
566
+ ? CircuitJsonLegacyNormalizer.#side(name.value)
567
+ : 'top'
568
+ }
569
+ if (value !== null && typeof value === 'object') return 'top'
570
+ if (value === 1) return 'top'
571
+ if (value === 32) return 'bottom'
572
+ const text = String(value ?? '')
573
+ .trim()
574
+ .toLowerCase()
575
+ if (VISIBLE_LAYERS.has(text)) return text
576
+ if (text === '1') return 'top'
577
+ if (text === '32') return 'bottom'
578
+ return /bottom|back|^b[._-]/u.test(text) ? 'bottom' : 'top'
579
+ }
580
+
581
+ /**
582
+ * Normalizes one dense layer-reference list with a stable fallback.
583
+ * @param {unknown} layers Layer list candidate.
584
+ * @param {unknown[]} fallback Missing or invalid layer list fallback.
585
+ * @returns {string[]} Canonical layer references.
586
+ */
587
+ static #layers(layers, fallback) {
588
+ const descriptors = CircuitJsonLegacyNormalizer.#array(layers)
589
+ const values = descriptors
590
+ ? Array.from(
591
+ { length: descriptors.length.value },
592
+ (_entry, index) => descriptors[String(index)].value
593
+ )
594
+ : fallback
595
+ return values.map((value) => CircuitJsonLegacyNormalizer.#side(value))
596
+ }
597
+
598
+ /** @param {Record<string, any>} fields Source component. @returns {string} */
599
+ static #sourceComponentFtype(fields) {
600
+ const reference = String(
601
+ fields.name ?? fields.reference ?? fields.designator ?? ''
602
+ ).trim()
603
+ const text = [
604
+ reference,
605
+ fields.footprint,
606
+ fields.package,
607
+ fields.package_name,
608
+ fields.value,
609
+ fields.description
610
+ ]
611
+ .map((value) => String(value ?? '').toLowerCase())
612
+ .join(' ')
613
+ if (text.includes('led')) return 'simple_led'
614
+ if (/^tp[0-9A-Z_-]*/iu.test(reference) || text.includes('test point')) {
615
+ return 'simple_test_point'
616
+ }
617
+ if (/^d[0-9A-Z_-]*/iu.test(reference)) return 'simple_diode'
618
+ if (/^r[0-9A-Z_-]*/iu.test(reference) || fields.resistance) {
619
+ return 'simple_resistor'
620
+ }
621
+ if (/^c[0-9A-Z_-]*/iu.test(reference) || fields.capacitance) {
622
+ return 'simple_capacitor'
623
+ }
624
+ return 'simple_chip'
625
+ }
626
+
627
+ /** @param {Record<string, any>} fields Source component. @returns {void} */
628
+ static #supplierNumbers(fields) {
629
+ const descriptors = CircuitJsonLegacyNormalizer.#record(
630
+ fields.supplier_part_numbers
631
+ )
632
+ if (!descriptors) return
633
+ const canonical = {}
634
+ const legacy = {}
635
+ for (const [supplier, descriptor] of Object.entries(descriptors)) {
636
+ if (UPSTREAM_SUPPLIERS.has(supplier)) {
637
+ canonical[supplier] = descriptor.value
638
+ } else {
639
+ legacy[supplier] = descriptor.value
640
+ }
641
+ }
642
+ if (Object.keys(legacy).length === 0) return
643
+ fields.supplier_part_numbers = canonical
644
+ fields.legacy_supplier_part_numbers = legacy
645
+ }
646
+
647
+ /** @param {unknown} route Legacy trace route. @returns {unknown} */
648
+ static #traceRoute(route) {
649
+ const descriptors = CircuitJsonLegacyNormalizer.#array(route)
650
+ if (!descriptors) return route
651
+ const normalized = new Array(descriptors.length.value)
652
+ for (let index = 0; index < normalized.length; index += 1) {
653
+ const element = descriptors[String(index)].value
654
+ const elementDescriptors =
655
+ CircuitJsonLegacyNormalizer.#record(element)
656
+ if (!elementDescriptors) {
657
+ normalized[index] = element
658
+ continue
659
+ }
660
+ const fields = Object.fromEntries(
661
+ Object.entries(elementDescriptors).map(([key, descriptor]) => [
662
+ key,
663
+ descriptor.value
664
+ ])
665
+ )
666
+ if (
667
+ !fields.route_type &&
668
+ (fields.via_diameter !== undefined ||
669
+ fields.from_layer !== undefined ||
670
+ fields.to_layer !== undefined)
671
+ ) {
672
+ fields.route_type = 'via'
673
+ } else if (
674
+ !fields.route_type &&
675
+ fields.x !== undefined &&
676
+ fields.y !== undefined
677
+ ) {
678
+ fields.route_type = 'wire'
679
+ }
680
+ if (fields.route_type === 'wire') {
681
+ fields.layer = CircuitJsonLegacyNormalizer.#side(fields.layer)
682
+ }
683
+ if (fields.route_type === 'via') {
684
+ fields.from_layer = CircuitJsonLegacyNormalizer.#side(
685
+ fields.from_layer ?? 'top'
686
+ )
687
+ fields.to_layer = CircuitJsonLegacyNormalizer.#side(
688
+ fields.to_layer ?? 'bottom'
689
+ )
690
+ fields.outer_diameter ??= fields.via_diameter
691
+ if (
692
+ fields.hole_diameter === undefined &&
693
+ fields.outer_diameter !== undefined
694
+ ) {
695
+ fields.hole_diameter = Number(fields.outer_diameter) / 2
696
+ }
697
+ }
698
+ normalized[index] = fields
699
+ }
700
+ return normalized
701
+ }
702
+
703
+ /** @param {Record<string, any>} fields B-Rep pour fields. @returns {void} */
704
+ static #brep(fields) {
705
+ const candidates = Array.isArray(fields.brep_shapes)
706
+ ? fields.brep_shapes
707
+ : Array.isArray(fields.brepShapes)
708
+ ? fields.brepShapes
709
+ : [fields.brep_shape ?? fields.brepShape]
710
+ const normalized = candidates
711
+ .map((shape) => CircuitJsonLegacyNormalizer.#brepShape(shape))
712
+ .filter(Boolean)
713
+ if (normalized.length > 0) fields.brep_shape = normalized[0]
714
+ }
715
+
716
+ /** @param {unknown} shape B-Rep shape. @returns {object | null} */
717
+ static #brepShape(shape) {
718
+ const descriptors = CircuitJsonLegacyNormalizer.#record(shape)
719
+ if (!descriptors) return null
720
+ const outer =
721
+ descriptors.outer_ring?.value ??
722
+ descriptors.outerRing?.value ??
723
+ descriptors.outer?.value ??
724
+ descriptors.ring?.value
725
+ const outerRing = CircuitJsonLegacyNormalizer.#brepRing(outer)
726
+ if (!outerRing) return null
727
+ const inner =
728
+ descriptors.inner_rings?.value ??
729
+ descriptors.innerRings?.value ??
730
+ descriptors.holes?.value ??
731
+ []
732
+ const innerDescriptors = CircuitJsonLegacyNormalizer.#array(inner)
733
+ const innerRings = []
734
+ if (innerDescriptors) {
735
+ for (
736
+ let index = 0;
737
+ index < innerDescriptors.length.value;
738
+ index += 1
739
+ ) {
740
+ const ring = CircuitJsonLegacyNormalizer.#brepRing(
741
+ innerDescriptors[String(index)].value
742
+ )
743
+ if (ring) innerRings.push(ring)
744
+ }
745
+ }
746
+ return { outer_ring: outerRing, inner_rings: innerRings }
747
+ }
748
+
749
+ /** @param {unknown} ring B-Rep ring. @returns {object | null} */
750
+ static #brepRing(ring) {
751
+ const arrayDescriptors = CircuitJsonLegacyNormalizer.#array(ring)
752
+ if (arrayDescriptors) {
753
+ return {
754
+ vertices: Array.from(
755
+ { length: arrayDescriptors.length.value },
756
+ (_entry, index) => arrayDescriptors[String(index)].value
757
+ )
758
+ }
759
+ }
760
+ const descriptors = CircuitJsonLegacyNormalizer.#record(ring)
761
+ if (!descriptors) return null
762
+ const vertices =
763
+ descriptors.vertices?.value ??
764
+ descriptors.cwVertices?.value ??
765
+ descriptors.ccwVertices?.value ??
766
+ descriptors.points?.value
767
+ return CircuitJsonLegacyNormalizer.#array(vertices)
768
+ ? { vertices }
769
+ : null
770
+ }
771
+
772
+ /** @param {unknown} points Point array. @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null} */
773
+ static #pointBounds(points) {
774
+ const descriptors = CircuitJsonLegacyNormalizer.#array(points)
775
+ if (!descriptors) return null
776
+ const bounds = {
777
+ minX: Infinity,
778
+ minY: Infinity,
779
+ maxX: -Infinity,
780
+ maxY: -Infinity
781
+ }
782
+ for (let index = 0; index < descriptors.length.value; index += 1) {
783
+ const point = CircuitJsonLegacyNormalizer.#record(
784
+ descriptors[String(index)].value
785
+ )
786
+ const x = point?.x?.value
787
+ const y = point?.y?.value
788
+ if (!Number.isFinite(x) || !Number.isFinite(y)) continue
789
+ bounds.minX = Math.min(bounds.minX, x)
790
+ bounds.minY = Math.min(bounds.minY, y)
791
+ bounds.maxX = Math.max(bounds.maxX, x)
792
+ bounds.maxY = Math.max(bounds.maxY, y)
793
+ }
794
+ return Number.isFinite(bounds.minX) ? bounds : null
795
+ }
796
+
797
+ /** @param {unknown} value Array candidate. @returns {Record<string, PropertyDescriptor> | null} */
798
+ static #array(value) {
799
+ if (!Array.isArray(value)) return null
800
+ try {
801
+ const descriptors = Object.getOwnPropertyDescriptors(value)
802
+ const length = descriptors.length?.value
803
+ if (
804
+ Object.getPrototypeOf(value) !== Array.prototype ||
805
+ !Number.isSafeInteger(length) ||
806
+ length < 0
807
+ ) {
808
+ return null
809
+ }
810
+ for (let index = 0; index < length; index += 1) {
811
+ const descriptor = descriptors[String(index)]
812
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
813
+ return null
814
+ }
815
+ }
816
+ return descriptors
817
+ } catch {
818
+ return null
819
+ }
820
+ }
821
+
822
+ /** @param {unknown} value Record candidate. @returns {Record<string, PropertyDescriptor> | null} */
823
+ static #record(value) {
824
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
825
+ return null
826
+ }
827
+ try {
828
+ const prototype = Object.getPrototypeOf(value)
829
+ const descriptors = Object.getOwnPropertyDescriptors(value)
830
+ if (prototype !== Object.prototype && prototype !== null)
831
+ return null
832
+ if (
833
+ Object.values(descriptors).some(
834
+ (descriptor) => !Object.hasOwn(descriptor, 'value')
835
+ )
836
+ ) {
837
+ return null
838
+ }
839
+ return descriptors
840
+ } catch {
841
+ return null
842
+ }
843
+ }
844
+ }
845
+
846
+ Object.freeze(CircuitJsonLegacyNormalizer.prototype)
847
+ Object.freeze(CircuitJsonLegacyNormalizer)