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.
- package/NOTICE.md +31 -0
- package/README.md +267 -107
- package/docs/api.md +501 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +120 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/testing.md +117 -7
- package/package.json +31 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +997 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +623 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- 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
|
|
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
|
-
|
|
41
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
@@ -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.
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
120
|
+
interactionModel.primitives,
|
|
121
|
+
interactionModel.bounds,
|
|
122
|
+
{
|
|
123
|
+
groups: interactionModel.groups,
|
|
124
|
+
anchorOffsets: interactionModel.anchorOffsets
|
|
125
|
+
},
|
|
76
126
|
[
|
|
77
127
|
...CircuitJsonPcbPrimitiveBuilder.#generatedDiagnostics(index),
|
|
78
|
-
...
|
|
128
|
+
...areaDiagnostics
|
|
79
129
|
]
|
|
80
130
|
)
|
|
81
131
|
|
|
82
132
|
return {
|
|
83
|
-
|
|
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))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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}
|
|
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(
|
|
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
|
-
|
|
56
|
-
|
|
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.
|