circuitjson-toolkit 1.0.3 → 1.0.16
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/AGENTS.md +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
|
|
2
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
3
|
+
import { CircuitJsonPcbPrimitiveGeometry } from './CircuitJsonPcbPrimitiveGeometry.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves shared CircuitJSON PCB primitive fields from common element shapes.
|
|
7
|
+
*/
|
|
8
|
+
export class CircuitJsonPcbPrimitiveFields {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves board bounds from outline or size metadata.
|
|
11
|
+
* @param {object | null} board Board element.
|
|
12
|
+
* @returns {object | null}
|
|
13
|
+
*/
|
|
14
|
+
static boardBounds(board) {
|
|
15
|
+
const outline = CircuitJsonPcbPrimitiveFields.points(board)
|
|
16
|
+
if (outline.length >= 3) {
|
|
17
|
+
return CircuitJsonPcbPrimitiveGeometry.pointsBounds(outline)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const width = CircuitJsonUnits.optionalLength(board?.width)
|
|
21
|
+
const height = CircuitJsonUnits.optionalLength(board?.height)
|
|
22
|
+
if (width === null || height === null || width <= 0 || height <= 0) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return CircuitJsonPcbPrimitiveGeometry.centerBounds(
|
|
27
|
+
CircuitJsonPcbPrimitiveFields.center(board) || { x: 0, y: 0 },
|
|
28
|
+
width,
|
|
29
|
+
height
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves merged bounds for all board rows.
|
|
35
|
+
* @param {object[]} boards Board rows.
|
|
36
|
+
* @returns {object | null}
|
|
37
|
+
*/
|
|
38
|
+
static mergedBoardBounds(boards) {
|
|
39
|
+
return CircuitJsonPcbPrimitiveGeometry.mergedPrimitiveBounds(
|
|
40
|
+
boards
|
|
41
|
+
.map((board) => ({
|
|
42
|
+
bounds: CircuitJsonPcbPrimitiveFields.boardBounds(board)
|
|
43
|
+
}))
|
|
44
|
+
.filter((row) => row.bounds)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds copper layer rows from board metadata.
|
|
50
|
+
* @param {object | null} board Board element.
|
|
51
|
+
* @returns {object[]}
|
|
52
|
+
*/
|
|
53
|
+
static layers(board) {
|
|
54
|
+
const count = Math.max(
|
|
55
|
+
1,
|
|
56
|
+
Math.round(CircuitJsonUnits.length(board?.num_layers, 2))
|
|
57
|
+
)
|
|
58
|
+
const keys =
|
|
59
|
+
count === 1
|
|
60
|
+
? ['top']
|
|
61
|
+
: [
|
|
62
|
+
'top',
|
|
63
|
+
...Array.from(
|
|
64
|
+
{ length: Math.max(count - 2, 0) },
|
|
65
|
+
(_entry, index) => 'inner' + (index + 1)
|
|
66
|
+
),
|
|
67
|
+
'bottom'
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return keys.map((key, index) => ({
|
|
71
|
+
key,
|
|
72
|
+
id: key,
|
|
73
|
+
layer: key,
|
|
74
|
+
name: key,
|
|
75
|
+
number: index + 1,
|
|
76
|
+
side: key === 'top' ? 'top' : key === 'bottom' ? 'bottom' : 'inner',
|
|
77
|
+
type: 'copper',
|
|
78
|
+
sourceFormat: 'circuitjson'
|
|
79
|
+
}))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves a center point.
|
|
84
|
+
* @param {object} element Element row.
|
|
85
|
+
* @returns {{ x: number, y: number } | null}
|
|
86
|
+
*/
|
|
87
|
+
static center(element) {
|
|
88
|
+
return CircuitJsonPcbPrimitiveFields.point(element?.center || element)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolves one point.
|
|
93
|
+
* @param {object | null | undefined} value Point candidate.
|
|
94
|
+
* @returns {{ x: number, y: number } | null}
|
|
95
|
+
*/
|
|
96
|
+
static point(value) {
|
|
97
|
+
return CircuitJsonUnits.optionalPoint(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolves polygon points from common fields.
|
|
102
|
+
* @param {object} element Element row.
|
|
103
|
+
* @returns {{ x: number, y: number }[]}
|
|
104
|
+
*/
|
|
105
|
+
static points(element) {
|
|
106
|
+
return (
|
|
107
|
+
(Array.isArray(element?.points) && element.points) ||
|
|
108
|
+
(Array.isArray(element?.outline) && element.outline) ||
|
|
109
|
+
(Array.isArray(element?.vertices) && element.vertices) ||
|
|
110
|
+
(Array.isArray(element?.route) && element.route) ||
|
|
111
|
+
(Array.isArray(element?.path) && element.path) ||
|
|
112
|
+
(Array.isArray(element?.shape?.points) && element.shape.points) ||
|
|
113
|
+
[]
|
|
114
|
+
)
|
|
115
|
+
.map((point) => CircuitJsonPcbPrimitiveFields.point(point))
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolves open line/path points from common detail geometry fields.
|
|
121
|
+
* @param {object} element Element row.
|
|
122
|
+
* @returns {{ x: number, y: number }[]}
|
|
123
|
+
*/
|
|
124
|
+
static linePoints(element) {
|
|
125
|
+
const points = CircuitJsonPcbPrimitiveFields.points(element)
|
|
126
|
+
if (points.length > 1) return points
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
CircuitJsonPcbPrimitiveFields.point({
|
|
130
|
+
x: element.x1 ?? element.start?.x,
|
|
131
|
+
y: element.y1 ?? element.start?.y
|
|
132
|
+
}),
|
|
133
|
+
CircuitJsonPcbPrimitiveFields.point({
|
|
134
|
+
x: element.x2 ?? element.end?.x,
|
|
135
|
+
y: element.y2 ?? element.end?.y
|
|
136
|
+
})
|
|
137
|
+
].filter(Boolean)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolves the center of a point list.
|
|
142
|
+
* @param {{ x: number, y: number }[]} points Points.
|
|
143
|
+
* @returns {{ x: number, y: number } | null}
|
|
144
|
+
*/
|
|
145
|
+
static pointsCenter(points) {
|
|
146
|
+
const bounds = CircuitJsonPcbPrimitiveGeometry.pointsBounds(points)
|
|
147
|
+
return bounds
|
|
148
|
+
? {
|
|
149
|
+
x: bounds.minX + bounds.width / 2,
|
|
150
|
+
y: bounds.minY + bounds.height / 2
|
|
151
|
+
}
|
|
152
|
+
: null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolves a normalized layer key.
|
|
157
|
+
* @param {unknown} value Layer candidate.
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
static layer(value) {
|
|
161
|
+
const raw =
|
|
162
|
+
typeof value === 'object' && value !== null ? value.name : value
|
|
163
|
+
const text = String(raw ?? '').trim()
|
|
164
|
+
const lowered = text.toLowerCase()
|
|
165
|
+
if (['top', 'front', 'f.cu', '1'].includes(lowered)) return 'top'
|
|
166
|
+
if (['bottom', 'back', 'b.cu', '32'].includes(lowered)) return 'bottom'
|
|
167
|
+
return text
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolves the side from a layer key.
|
|
172
|
+
* @param {string} layer Layer key.
|
|
173
|
+
* @returns {'top' | 'bottom' | ''}
|
|
174
|
+
*/
|
|
175
|
+
static side(layer) {
|
|
176
|
+
const text = String(layer || '').toLowerCase()
|
|
177
|
+
if (/\b(bottom|back)\b|\bb[._-]/u.test(text)) return 'bottom'
|
|
178
|
+
if (/\b(top|front)\b|\bf[._-]/u.test(text)) return 'top'
|
|
179
|
+
return ''
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolves a net name from common fields.
|
|
184
|
+
* @param {object} element Element row.
|
|
185
|
+
* @param {string | null} fallback Fallback net name.
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
static netName(element, fallback = '') {
|
|
189
|
+
return String(
|
|
190
|
+
element?.netName ??
|
|
191
|
+
element?.net ??
|
|
192
|
+
element?.net_name ??
|
|
193
|
+
element?.source_net_id ??
|
|
194
|
+
fallback ??
|
|
195
|
+
''
|
|
196
|
+
).trim()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolves first valid length from candidates.
|
|
201
|
+
* @param {unknown[]} values Candidate values.
|
|
202
|
+
* @returns {number | null}
|
|
203
|
+
*/
|
|
204
|
+
static optionalLength(values) {
|
|
205
|
+
for (const value of values) {
|
|
206
|
+
const length = CircuitJsonUnits.optionalLength(value)
|
|
207
|
+
if (length !== null) return length
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolves an id for original or derived shape primitives.
|
|
214
|
+
* @param {object} element Element row.
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
static derivedId(element) {
|
|
218
|
+
const id = CircuitJsonIndexer.getElementId(element)
|
|
219
|
+
const suffix = String(element.derived_id_suffix || '').trim()
|
|
220
|
+
return id && suffix ? id + ':' + suffix : id
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolves unique non-empty string values.
|
|
225
|
+
* @param {unknown[]} values Candidate values.
|
|
226
|
+
* @returns {string[]}
|
|
227
|
+
*/
|
|
228
|
+
static uniqueStrings(values) {
|
|
229
|
+
return [...new Set(values.map((value) => String(value || '').trim()))]
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.sort()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides shared geometry helpers for CircuitJSON PCB primitive builders.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonPcbPrimitiveGeometry {
|
|
5
|
+
/**
|
|
6
|
+
* Builds a normalized bounds record.
|
|
7
|
+
* @param {number} minX Minimum x.
|
|
8
|
+
* @param {number} minY Minimum y.
|
|
9
|
+
* @param {number} maxX Maximum x.
|
|
10
|
+
* @param {number} maxY Maximum y.
|
|
11
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }}
|
|
12
|
+
*/
|
|
13
|
+
static bounds(minX, minY, maxX, maxY) {
|
|
14
|
+
return {
|
|
15
|
+
minX,
|
|
16
|
+
minY,
|
|
17
|
+
maxX,
|
|
18
|
+
maxY,
|
|
19
|
+
width: maxX - minX,
|
|
20
|
+
height: maxY - minY
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds bounds from a center and dimensions.
|
|
26
|
+
* @param {{ x: number, y: number }} center Center point.
|
|
27
|
+
* @param {number} width Width.
|
|
28
|
+
* @param {number} height Height.
|
|
29
|
+
* @returns {object}
|
|
30
|
+
*/
|
|
31
|
+
static centerBounds(center, width, height) {
|
|
32
|
+
return CircuitJsonPcbPrimitiveGeometry.bounds(
|
|
33
|
+
center.x - width / 2,
|
|
34
|
+
center.y - height / 2,
|
|
35
|
+
center.x + width / 2,
|
|
36
|
+
center.y + height / 2
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds segment bounds.
|
|
42
|
+
* @param {{ x: number, y: number }} start Start point.
|
|
43
|
+
* @param {{ x: number, y: number }} end End point.
|
|
44
|
+
* @param {number} width Segment width.
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
static segmentBounds(start, end, width) {
|
|
48
|
+
const margin = Math.max(width / 2, 0)
|
|
49
|
+
return CircuitJsonPcbPrimitiveGeometry.bounds(
|
|
50
|
+
Math.min(start.x, end.x) - margin,
|
|
51
|
+
Math.min(start.y, end.y) - margin,
|
|
52
|
+
Math.max(start.x, end.x) + margin,
|
|
53
|
+
Math.max(start.y, end.y) + margin
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds bounds for a point list.
|
|
59
|
+
* @param {{ x: number, y: number }[]} points Points.
|
|
60
|
+
* @returns {object | null}
|
|
61
|
+
*/
|
|
62
|
+
static pointsBounds(points) {
|
|
63
|
+
return points.reduce(
|
|
64
|
+
(bounds, point) =>
|
|
65
|
+
bounds
|
|
66
|
+
? CircuitJsonPcbPrimitiveGeometry.bounds(
|
|
67
|
+
Math.min(bounds.minX, point.x),
|
|
68
|
+
Math.min(bounds.minY, point.y),
|
|
69
|
+
Math.max(bounds.maxX, point.x),
|
|
70
|
+
Math.max(bounds.maxY, point.y)
|
|
71
|
+
)
|
|
72
|
+
: CircuitJsonPcbPrimitiveGeometry.bounds(
|
|
73
|
+
point.x,
|
|
74
|
+
point.y,
|
|
75
|
+
point.x,
|
|
76
|
+
point.y
|
|
77
|
+
),
|
|
78
|
+
null
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Builds merged primitive bounds.
|
|
84
|
+
* @param {object[]} primitives Primitive rows.
|
|
85
|
+
* @returns {object | null}
|
|
86
|
+
*/
|
|
87
|
+
static mergedPrimitiveBounds(primitives) {
|
|
88
|
+
return primitives.reduce((bounds, primitive) => {
|
|
89
|
+
if (!primitive.bounds) return bounds
|
|
90
|
+
if (!bounds) return primitive.bounds
|
|
91
|
+
return CircuitJsonPcbPrimitiveGeometry.bounds(
|
|
92
|
+
Math.min(bounds.minX, primitive.bounds.minX),
|
|
93
|
+
Math.min(bounds.minY, primitive.bounds.minY),
|
|
94
|
+
Math.max(bounds.maxX, primitive.bounds.maxX),
|
|
95
|
+
Math.max(bounds.maxY, primitive.bounds.maxY)
|
|
96
|
+
)
|
|
97
|
+
}, null)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds corner anchors for bounds.
|
|
102
|
+
* @param {object} bounds Bounds record.
|
|
103
|
+
* @returns {object[]}
|
|
104
|
+
*/
|
|
105
|
+
static cornerAnchors(bounds) {
|
|
106
|
+
return CircuitJsonPcbPrimitiveGeometry.boundsAnchors(bounds)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds corner, edge-center, and center anchors for bounds.
|
|
111
|
+
* @param {object} bounds Bounds record.
|
|
112
|
+
* @returns {object[]}
|
|
113
|
+
*/
|
|
114
|
+
static boundsAnchors(bounds) {
|
|
115
|
+
const centerX = CircuitJsonPcbPrimitiveGeometry.#round(
|
|
116
|
+
bounds.minX + bounds.width / 2
|
|
117
|
+
)
|
|
118
|
+
const centerY = CircuitJsonPcbPrimitiveGeometry.#round(
|
|
119
|
+
bounds.minY + bounds.height / 2
|
|
120
|
+
)
|
|
121
|
+
return [
|
|
122
|
+
{ point: { x: bounds.minX, y: bounds.minY } },
|
|
123
|
+
{ point: { x: centerX, y: bounds.minY } },
|
|
124
|
+
{ point: { x: bounds.maxX, y: bounds.minY } },
|
|
125
|
+
{ point: { x: bounds.maxX, y: centerY } },
|
|
126
|
+
{ point: { x: bounds.maxX, y: bounds.maxY } },
|
|
127
|
+
{ point: { x: centerX, y: bounds.maxY } },
|
|
128
|
+
{ point: { x: bounds.minX, y: bounds.maxY } },
|
|
129
|
+
{ point: { x: bounds.minX, y: centerY } },
|
|
130
|
+
{ point: { x: centerX, y: centerY } }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Rounds computed geometry values to stable precision.
|
|
136
|
+
* @param {number} value Numeric value.
|
|
137
|
+
* @returns {number}
|
|
138
|
+
*/
|
|
139
|
+
static #round(value) {
|
|
140
|
+
return Math.round(Number(value || 0) * 1_000_000) / 1_000_000
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
2
|
+
import { CircuitJsonPcbPrimitiveGeometry } from './CircuitJsonPcbPrimitiveGeometry.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds PCB group rows and group-anchor overlays from element arrays.
|
|
6
|
+
*/
|
|
7
|
+
export class CircuitJsonPcbPrimitiveGroups {
|
|
8
|
+
/**
|
|
9
|
+
* Builds group metadata and decorated primitive rows.
|
|
10
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
11
|
+
* @param {object[]} primitives Primitive rows.
|
|
12
|
+
* @param {object[]} components Component rows.
|
|
13
|
+
* @returns {{ primitives: object[], groups: object[], anchorOffsets: object[] }}
|
|
14
|
+
*/
|
|
15
|
+
static build(index, primitives, components) {
|
|
16
|
+
const groups = CircuitJsonPcbPrimitiveGroups.#groups(
|
|
17
|
+
index,
|
|
18
|
+
primitives,
|
|
19
|
+
components
|
|
20
|
+
)
|
|
21
|
+
const componentGroups =
|
|
22
|
+
CircuitJsonPcbPrimitiveGroups.#componentGroupIds(components)
|
|
23
|
+
return {
|
|
24
|
+
primitives: primitives.map((primitive) =>
|
|
25
|
+
CircuitJsonPcbPrimitiveGroups.#withGroupIds(
|
|
26
|
+
primitive,
|
|
27
|
+
index,
|
|
28
|
+
componentGroups
|
|
29
|
+
)
|
|
30
|
+
),
|
|
31
|
+
groups,
|
|
32
|
+
anchorOffsets: CircuitJsonPcbPrimitiveGroups.#anchorOffsets(
|
|
33
|
+
groups,
|
|
34
|
+
components
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds PCB group rows.
|
|
41
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
42
|
+
* @param {object[]} primitives Primitive rows.
|
|
43
|
+
* @param {object[]} components Component rows.
|
|
44
|
+
* @returns {object[]}
|
|
45
|
+
*/
|
|
46
|
+
static #groups(index, primitives, components) {
|
|
47
|
+
return CircuitJsonPcbPrimitiveGroups.#all(index, 'pcb_group')
|
|
48
|
+
.map((group) =>
|
|
49
|
+
CircuitJsonPcbPrimitiveGroups.#groupRow(
|
|
50
|
+
group,
|
|
51
|
+
primitives,
|
|
52
|
+
components
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds one group row.
|
|
60
|
+
* @param {object} group Source group element.
|
|
61
|
+
* @param {object[]} primitives Primitive rows.
|
|
62
|
+
* @param {object[]} components Component rows.
|
|
63
|
+
* @returns {object | null}
|
|
64
|
+
*/
|
|
65
|
+
static #groupRow(group, primitives, components) {
|
|
66
|
+
const id = String(group.pcb_group_id || '').trim()
|
|
67
|
+
if (!id) return null
|
|
68
|
+
const componentIds = components
|
|
69
|
+
.filter(
|
|
70
|
+
(component) =>
|
|
71
|
+
String(component.pcb_group_id || '').trim() === id
|
|
72
|
+
)
|
|
73
|
+
.map((component) => component.pcbComponentId)
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
type: 'pcb_group',
|
|
78
|
+
name: String(group.name || id),
|
|
79
|
+
bounds: CircuitJsonPcbPrimitiveGroups.#groupBounds(
|
|
80
|
+
group,
|
|
81
|
+
primitives
|
|
82
|
+
),
|
|
83
|
+
sourceGroupId: String(group.source_group_id || '').trim(),
|
|
84
|
+
subcircuitId: String(group.subcircuit_id || '').trim(),
|
|
85
|
+
componentIds,
|
|
86
|
+
memberIds: CircuitJsonPcbPrimitiveGroups.#memberIds(
|
|
87
|
+
id,
|
|
88
|
+
primitives,
|
|
89
|
+
componentIds
|
|
90
|
+
),
|
|
91
|
+
anchor: CircuitJsonUnits.optionalPoint(group.anchor_position),
|
|
92
|
+
depth: Number.isFinite(Number(group.depth))
|
|
93
|
+
? Number(group.depth)
|
|
94
|
+
: 0,
|
|
95
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalGroupMetadata(group)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds optional group metadata for inspection.
|
|
101
|
+
* @param {object} group Source group element.
|
|
102
|
+
* @returns {object}
|
|
103
|
+
*/
|
|
104
|
+
static #optionalGroupMetadata(group) {
|
|
105
|
+
return {
|
|
106
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalStringField(
|
|
107
|
+
'anchorAlignment',
|
|
108
|
+
group.anchor_alignment
|
|
109
|
+
),
|
|
110
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalStringField(
|
|
111
|
+
'positionMode',
|
|
112
|
+
group.position_mode
|
|
113
|
+
),
|
|
114
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalStringField(
|
|
115
|
+
'childLayoutMode',
|
|
116
|
+
group.child_layout_mode
|
|
117
|
+
),
|
|
118
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalStringField(
|
|
119
|
+
'layoutMode',
|
|
120
|
+
group.layout_mode
|
|
121
|
+
),
|
|
122
|
+
...CircuitJsonPcbPrimitiveGroups.#optionalLengthField(
|
|
123
|
+
'autorouterTraceClearance',
|
|
124
|
+
group.autorouter_configuration?.trace_clearance
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Builds an optional string field.
|
|
131
|
+
* @param {string} key Output key.
|
|
132
|
+
* @param {unknown} value Candidate value.
|
|
133
|
+
* @returns {object}
|
|
134
|
+
*/
|
|
135
|
+
static #optionalStringField(key, value) {
|
|
136
|
+
const text = String(value || '').trim()
|
|
137
|
+
return text ? { [key]: text } : {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds an optional length field.
|
|
142
|
+
* @param {string} key Output key.
|
|
143
|
+
* @param {unknown} value Candidate value.
|
|
144
|
+
* @returns {object}
|
|
145
|
+
*/
|
|
146
|
+
static #optionalLengthField(key, value) {
|
|
147
|
+
const number = CircuitJsonUnits.optionalLength(value)
|
|
148
|
+
return number === null ? {} : { [key]: number }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolves group bounds from explicit size or grouped primitives.
|
|
153
|
+
* @param {object} group Group element.
|
|
154
|
+
* @param {object[]} primitives Primitive rows.
|
|
155
|
+
* @returns {object | null}
|
|
156
|
+
*/
|
|
157
|
+
static #groupBounds(group, primitives) {
|
|
158
|
+
const center = CircuitJsonUnits.optionalPoint(group.center || group)
|
|
159
|
+
const size = CircuitJsonUnits.optionalSize(group.size || group)
|
|
160
|
+
if (center && size) {
|
|
161
|
+
return CircuitJsonPcbPrimitiveGeometry.centerBounds(
|
|
162
|
+
center,
|
|
163
|
+
size.width,
|
|
164
|
+
size.height
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return CircuitJsonPcbPrimitiveGeometry.mergedPrimitiveBounds(
|
|
169
|
+
primitives.filter(
|
|
170
|
+
(primitive) =>
|
|
171
|
+
String(primitive.source?.pcb_group_id || '').trim() ===
|
|
172
|
+
String(group.pcb_group_id || '').trim()
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds member ids from components and primitives.
|
|
179
|
+
* @param {string} groupId Group id.
|
|
180
|
+
* @param {object[]} primitives Primitive rows.
|
|
181
|
+
* @param {string[]} componentIds Component ids.
|
|
182
|
+
* @returns {string[]}
|
|
183
|
+
*/
|
|
184
|
+
static #memberIds(groupId, primitives, componentIds) {
|
|
185
|
+
const ids = [...componentIds]
|
|
186
|
+
for (const primitive of primitives) {
|
|
187
|
+
if (
|
|
188
|
+
String(primitive.source?.pcb_group_id || '').trim() !== groupId
|
|
189
|
+
) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (primitive.id && !ids.includes(primitive.id)) {
|
|
193
|
+
ids.push(primitive.id)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return ids
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Builds group ids for each component id.
|
|
201
|
+
* @param {object[]} components Component rows.
|
|
202
|
+
* @returns {Map<string, string[]>}
|
|
203
|
+
*/
|
|
204
|
+
static #componentGroupIds(components) {
|
|
205
|
+
return new Map(
|
|
206
|
+
components.map((component) => [
|
|
207
|
+
String(component.pcbComponentId || ''),
|
|
208
|
+
CircuitJsonPcbPrimitiveGroups.#unique(component.groupIds || [])
|
|
209
|
+
])
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Decorates one primitive with group and subcircuit ids.
|
|
215
|
+
* @param {object} primitive Primitive row.
|
|
216
|
+
* @param {{ sourceComponentById?: Map<string, object> }} index Element index.
|
|
217
|
+
* @param {Map<string, string[]>} componentGroups Component group lookup.
|
|
218
|
+
* @returns {object}
|
|
219
|
+
*/
|
|
220
|
+
static #withGroupIds(primitive, index, componentGroups) {
|
|
221
|
+
const sourceComponent = index.sourceComponentById?.get(
|
|
222
|
+
String(primitive.component?.source_component_id || '').trim()
|
|
223
|
+
)
|
|
224
|
+
const groupIds = CircuitJsonPcbPrimitiveGroups.#unique([
|
|
225
|
+
...(primitive.groupIds || []),
|
|
226
|
+
primitive.source?.pcb_group_id,
|
|
227
|
+
primitive.source?.source_group_id,
|
|
228
|
+
primitive.component?.pcb_group_id,
|
|
229
|
+
primitive.component?.positioned_relative_to_pcb_group_id,
|
|
230
|
+
primitive.component?.source_group_id,
|
|
231
|
+
primitive.component?.sourceGroupId,
|
|
232
|
+
sourceComponent?.source_group_id,
|
|
233
|
+
...(componentGroups.get(String(primitive.componentId || '')) || [])
|
|
234
|
+
])
|
|
235
|
+
const subcircuitIds = CircuitJsonPcbPrimitiveGroups.#unique([
|
|
236
|
+
...(primitive.subcircuitIds || []),
|
|
237
|
+
primitive.source?.subcircuit_id,
|
|
238
|
+
primitive.source?.subcircuitId,
|
|
239
|
+
primitive.component?.subcircuit_id,
|
|
240
|
+
primitive.component?.subcircuitId,
|
|
241
|
+
...(primitive.component?.subcircuitIds || []),
|
|
242
|
+
sourceComponent?.subcircuit_id
|
|
243
|
+
])
|
|
244
|
+
return {
|
|
245
|
+
...primitive,
|
|
246
|
+
groupIds,
|
|
247
|
+
subcircuitIds
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Builds visual anchor offsets for grouped component placements.
|
|
253
|
+
* @param {object[]} groups Group rows.
|
|
254
|
+
* @param {object[]} components Component rows.
|
|
255
|
+
* @returns {object[]}
|
|
256
|
+
*/
|
|
257
|
+
static #anchorOffsets(groups, components) {
|
|
258
|
+
return groups.flatMap((group) => {
|
|
259
|
+
if (!group.anchor) return []
|
|
260
|
+
return group.componentIds.flatMap((componentId) => {
|
|
261
|
+
const component = components.find(
|
|
262
|
+
(row) => row.pcbComponentId === componentId
|
|
263
|
+
)
|
|
264
|
+
if (!component) return []
|
|
265
|
+
return [
|
|
266
|
+
{
|
|
267
|
+
id: 'anchor-offset:' + group.id + ':' + componentId,
|
|
268
|
+
kind: 'group-anchor-offset',
|
|
269
|
+
sourceId: group.id,
|
|
270
|
+
targetId: componentId,
|
|
271
|
+
targetType: 'component',
|
|
272
|
+
start: group.anchor,
|
|
273
|
+
end: { x: component.x, y: component.y },
|
|
274
|
+
label: component.componentKey
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Returns indexed elements by type.
|
|
283
|
+
* @param {{ elementsByType: Map<string, object[]> }} index Element index.
|
|
284
|
+
* @param {string} type Element type.
|
|
285
|
+
* @returns {object[]}
|
|
286
|
+
*/
|
|
287
|
+
static #all(index, type) {
|
|
288
|
+
return index.elementsByType.get(type) || []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolves unique non-empty strings.
|
|
293
|
+
* @param {unknown[]} values Candidate values.
|
|
294
|
+
* @returns {string[]}
|
|
295
|
+
*/
|
|
296
|
+
static #unique(values) {
|
|
297
|
+
return [...new Set(values.map((value) => String(value || '').trim()))]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.sort((left, right) => {
|
|
300
|
+
const leftPcb = left.startsWith('pcb_') ? 0 : 1
|
|
301
|
+
const rightPcb = right.startsWith('pcb_') ? 0 : 1
|
|
302
|
+
return leftPcb - rightPcb || left.localeCompare(right)
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
}
|