circuitjson-toolkit 1.0.10 → 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/docs/model-format.md +15 -0
- package/package.json +3 -2
- package/src/core/CircuitJsonBomBuilder.mjs +22 -25
- package/src/core/CircuitJsonElementValidator.mjs +233 -16
- package/src/core/CircuitJsonIndexer.mjs +510 -5
- package/src/core/CircuitJsonManufacturingBuilder.mjs +488 -16
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- 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 +227 -5
- 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/index.mjs +2 -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,335 @@
|
|
|
1
|
+
import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds standards-shaped CircuitJSON for selected-part ZIP exports.
|
|
5
|
+
*/
|
|
6
|
+
export class SelectedPartCircuitJsonExportAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* Builds a CircuitJSON element array for one selected part.
|
|
9
|
+
* @param {{ designator?: string, symbol?: object, footprint?: object }} selectedPart Selected part data.
|
|
10
|
+
* @param {object} documentModel Active document model.
|
|
11
|
+
* @param {string} partName Export artifact name.
|
|
12
|
+
* @returns {object[]}
|
|
13
|
+
*/
|
|
14
|
+
static build(selectedPart, documentModel, partName) {
|
|
15
|
+
const designator = selectedPart.designator || 'selected-part'
|
|
16
|
+
const idToken =
|
|
17
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(designator)
|
|
18
|
+
const sourceComponentId = 'source_component_' + idToken
|
|
19
|
+
const pcbComponentId = 'pcb_component_' + idToken
|
|
20
|
+
const circuitJson = [
|
|
21
|
+
{
|
|
22
|
+
type: 'source_project_metadata',
|
|
23
|
+
name: documentModel?.fileName || 'Selected part export',
|
|
24
|
+
software_used_string:
|
|
25
|
+
documentModel?.sourceFormat || documentModel?.fileType || ''
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: 'source_component',
|
|
29
|
+
source_component_id: sourceComponentId,
|
|
30
|
+
name: partName,
|
|
31
|
+
ftype: 'simple_chip',
|
|
32
|
+
manufacturer_part_number: selectedPart.symbol?.value || '',
|
|
33
|
+
supplier_part_numbers: {}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'schematic_component',
|
|
37
|
+
schematic_component_id: 'schematic_component_' + idToken,
|
|
38
|
+
source_component_id: sourceComponentId,
|
|
39
|
+
center: { x: 0, y: 0 },
|
|
40
|
+
size: SelectedPartCircuitJsonExportAdapter.#schematicSize(
|
|
41
|
+
selectedPart
|
|
42
|
+
),
|
|
43
|
+
rotation: 0
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'pcb_component',
|
|
47
|
+
pcb_component_id: pcbComponentId,
|
|
48
|
+
source_component_id: sourceComponentId,
|
|
49
|
+
center: SelectedPartCircuitJsonExportAdapter.#footprintCenter(
|
|
50
|
+
selectedPart
|
|
51
|
+
),
|
|
52
|
+
layer: 'top',
|
|
53
|
+
rotation: 0,
|
|
54
|
+
width: SelectedPartCircuitJsonExportAdapter.#footprintSize(
|
|
55
|
+
selectedPart
|
|
56
|
+
).width,
|
|
57
|
+
height: SelectedPartCircuitJsonExportAdapter.#footprintSize(
|
|
58
|
+
selectedPart
|
|
59
|
+
).height
|
|
60
|
+
},
|
|
61
|
+
...SelectedPartCircuitJsonExportAdapter.#sourcePorts(
|
|
62
|
+
selectedPart,
|
|
63
|
+
sourceComponentId
|
|
64
|
+
),
|
|
65
|
+
...SelectedPartCircuitJsonExportAdapter.#pcbPads(
|
|
66
|
+
selectedPart,
|
|
67
|
+
pcbComponentId
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
CircuitJsonDocument.assertModel(circuitJson)
|
|
72
|
+
return circuitJson
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Builds source port entries.
|
|
77
|
+
* @param {{ symbol?: { pins?: object[] } }} selectedPart Selected part data.
|
|
78
|
+
* @param {string} sourceComponentId Source component id.
|
|
79
|
+
* @returns {object[]}
|
|
80
|
+
*/
|
|
81
|
+
static #sourcePorts(selectedPart, sourceComponentId) {
|
|
82
|
+
return SelectedPartCircuitJsonExportAdapter.#array(
|
|
83
|
+
selectedPart.symbol?.pins
|
|
84
|
+
).map((pin, index) => {
|
|
85
|
+
const pinName = String(pin.name || index + 1)
|
|
86
|
+
const pinNumber = String(pin.number || index + 1)
|
|
87
|
+
const entry = {
|
|
88
|
+
type: 'source_port',
|
|
89
|
+
source_port_id:
|
|
90
|
+
sourceComponentId +
|
|
91
|
+
'_port_' +
|
|
92
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(
|
|
93
|
+
pinNumber
|
|
94
|
+
),
|
|
95
|
+
source_component_id: sourceComponentId,
|
|
96
|
+
name: pinName,
|
|
97
|
+
port_hints: [pinNumber]
|
|
98
|
+
}
|
|
99
|
+
const numericPinNumber =
|
|
100
|
+
SelectedPartCircuitJsonExportAdapter.#numericPinNumber(
|
|
101
|
+
pinNumber
|
|
102
|
+
)
|
|
103
|
+
if (numericPinNumber !== null) entry.pin_number = numericPinNumber
|
|
104
|
+
return entry
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds PCB SMT pad entries.
|
|
110
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
111
|
+
* @param {string} pcbComponentId PCB component id.
|
|
112
|
+
* @returns {object[]}
|
|
113
|
+
*/
|
|
114
|
+
static #pcbPads(selectedPart, pcbComponentId) {
|
|
115
|
+
return SelectedPartCircuitJsonExportAdapter.#array(
|
|
116
|
+
selectedPart.footprint?.pads
|
|
117
|
+
).map((pad, index) =>
|
|
118
|
+
SelectedPartCircuitJsonExportAdapter.#pcbPad(
|
|
119
|
+
pad,
|
|
120
|
+
index,
|
|
121
|
+
pcbComponentId
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Builds one PCB SMT pad entry.
|
|
128
|
+
* @param {object} pad Pad data.
|
|
129
|
+
* @param {number} index Pad index.
|
|
130
|
+
* @param {string} pcbComponentId PCB component id.
|
|
131
|
+
* @returns {object}
|
|
132
|
+
*/
|
|
133
|
+
static #pcbPad(pad, index, pcbComponentId) {
|
|
134
|
+
const padNumber = String(pad.number || index + 1)
|
|
135
|
+
const width = SelectedPartCircuitJsonExportAdapter.#number(pad.width, 1)
|
|
136
|
+
const height = SelectedPartCircuitJsonExportAdapter.#number(
|
|
137
|
+
pad.height,
|
|
138
|
+
1
|
|
139
|
+
)
|
|
140
|
+
const rotation = SelectedPartCircuitJsonExportAdapter.#number(
|
|
141
|
+
pad.ccw_rotation ?? pad.rotation,
|
|
142
|
+
0
|
|
143
|
+
)
|
|
144
|
+
const shape =
|
|
145
|
+
rotation !== 0
|
|
146
|
+
? 'rotated_rect'
|
|
147
|
+
: SelectedPartCircuitJsonExportAdapter.#padShape(
|
|
148
|
+
pad,
|
|
149
|
+
width,
|
|
150
|
+
height
|
|
151
|
+
)
|
|
152
|
+
const entry = {
|
|
153
|
+
type: 'pcb_smtpad',
|
|
154
|
+
shape,
|
|
155
|
+
pcb_smtpad_id:
|
|
156
|
+
pcbComponentId +
|
|
157
|
+
'_pad_' +
|
|
158
|
+
SelectedPartCircuitJsonExportAdapter.#safeIdentifier(padNumber),
|
|
159
|
+
pcb_component_id: pcbComponentId,
|
|
160
|
+
port_hints: [padNumber],
|
|
161
|
+
x: SelectedPartCircuitJsonExportAdapter.#number(pad.x, 0),
|
|
162
|
+
y: SelectedPartCircuitJsonExportAdapter.#number(pad.y, 0),
|
|
163
|
+
layer: SelectedPartCircuitJsonExportAdapter.#padLayer(pad)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (shape === 'circle') {
|
|
167
|
+
entry.radius = Math.max(width, height) / 2
|
|
168
|
+
} else {
|
|
169
|
+
entry.width = width
|
|
170
|
+
entry.height = height
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (shape === 'rotated_rect') entry.ccw_rotation = rotation
|
|
174
|
+
return entry
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the schematic component size.
|
|
179
|
+
* @param {{ symbol?: { pins?: object[] } }} selectedPart Selected part data.
|
|
180
|
+
* @returns {{ width: number, height: number }}
|
|
181
|
+
*/
|
|
182
|
+
static #schematicSize(selectedPart) {
|
|
183
|
+
const pinCount = SelectedPartCircuitJsonExportAdapter.#array(
|
|
184
|
+
selectedPart.symbol?.pins
|
|
185
|
+
).length
|
|
186
|
+
const edge = Math.max(2.54, Math.ceil(Math.sqrt(pinCount || 1)) * 2.54)
|
|
187
|
+
return { width: edge, height: edge }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves footprint size from owned pads.
|
|
192
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
193
|
+
* @returns {{ width: number, height: number }}
|
|
194
|
+
*/
|
|
195
|
+
static #footprintSize(selectedPart) {
|
|
196
|
+
const bounds =
|
|
197
|
+
SelectedPartCircuitJsonExportAdapter.#footprintBounds(selectedPart)
|
|
198
|
+
return {
|
|
199
|
+
width: Math.max(bounds.maxX - bounds.minX, 1),
|
|
200
|
+
height: Math.max(bounds.maxY - bounds.minY, 1)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Resolves footprint center from owned pads.
|
|
206
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
207
|
+
* @returns {{ x: number, y: number }}
|
|
208
|
+
*/
|
|
209
|
+
static #footprintCenter(selectedPart) {
|
|
210
|
+
const bounds =
|
|
211
|
+
SelectedPartCircuitJsonExportAdapter.#footprintBounds(selectedPart)
|
|
212
|
+
return {
|
|
213
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
214
|
+
y: (bounds.minY + bounds.maxY) / 2
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolves footprint pad bounds.
|
|
220
|
+
* @param {{ footprint?: { pads?: object[] } }} selectedPart Selected part data.
|
|
221
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
222
|
+
*/
|
|
223
|
+
static #footprintBounds(selectedPart) {
|
|
224
|
+
const pads = SelectedPartCircuitJsonExportAdapter.#array(
|
|
225
|
+
selectedPart.footprint?.pads
|
|
226
|
+
)
|
|
227
|
+
if (!pads.length)
|
|
228
|
+
return { minX: -0.5, minY: -0.5, maxX: 0.5, maxY: 0.5 }
|
|
229
|
+
|
|
230
|
+
return pads.reduce(
|
|
231
|
+
(bounds, pad) => {
|
|
232
|
+
const x = SelectedPartCircuitJsonExportAdapter.#number(pad.x, 0)
|
|
233
|
+
const y = SelectedPartCircuitJsonExportAdapter.#number(pad.y, 0)
|
|
234
|
+
const halfWidth =
|
|
235
|
+
SelectedPartCircuitJsonExportAdapter.#number(pad.width, 1) /
|
|
236
|
+
2
|
|
237
|
+
const halfHeight =
|
|
238
|
+
SelectedPartCircuitJsonExportAdapter.#number(
|
|
239
|
+
pad.height,
|
|
240
|
+
1
|
|
241
|
+
) / 2
|
|
242
|
+
return {
|
|
243
|
+
minX: Math.min(bounds.minX, x - halfWidth),
|
|
244
|
+
minY: Math.min(bounds.minY, y - halfHeight),
|
|
245
|
+
maxX: Math.max(bounds.maxX, x + halfWidth),
|
|
246
|
+
maxY: Math.max(bounds.maxY, y + halfHeight)
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
minX: Infinity,
|
|
251
|
+
minY: Infinity,
|
|
252
|
+
maxX: -Infinity,
|
|
253
|
+
maxY: -Infinity
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves an SMT pad shape.
|
|
260
|
+
* @param {object} pad Pad data.
|
|
261
|
+
* @param {number} width Pad width.
|
|
262
|
+
* @param {number} height Pad height.
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
static #padShape(pad, width, height) {
|
|
266
|
+
const rawShape = String(
|
|
267
|
+
pad.shape || pad.shapeTopName || pad.shapeName || ''
|
|
268
|
+
).toLowerCase()
|
|
269
|
+
if (
|
|
270
|
+
rawShape.includes('circle') ||
|
|
271
|
+
rawShape.includes('round') ||
|
|
272
|
+
rawShape.includes('oval')
|
|
273
|
+
) {
|
|
274
|
+
return width === height ? 'circle' : 'rect'
|
|
275
|
+
}
|
|
276
|
+
return 'rect'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolves an SMT pad layer.
|
|
281
|
+
* @param {object} pad Pad data.
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
static #padLayer(pad) {
|
|
285
|
+
const layer = String(pad.layer || pad.layerName || '').toLowerCase()
|
|
286
|
+
if (
|
|
287
|
+
layer.includes('bottom') ||
|
|
288
|
+
layer === 'bottom' ||
|
|
289
|
+
pad.layerId === 32
|
|
290
|
+
) {
|
|
291
|
+
return 'bottom'
|
|
292
|
+
}
|
|
293
|
+
return 'top'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns a number pin when a pin token is numeric.
|
|
298
|
+
* @param {unknown} value Candidate pin number.
|
|
299
|
+
* @returns {number | null}
|
|
300
|
+
*/
|
|
301
|
+
static #numericPinNumber(value) {
|
|
302
|
+
const text = String(value || '').trim()
|
|
303
|
+
const parsed = Number(text)
|
|
304
|
+
return text && Number.isFinite(parsed) ? parsed : null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Creates a safe CircuitJSON id token.
|
|
309
|
+
* @param {unknown} value Raw value.
|
|
310
|
+
* @returns {string}
|
|
311
|
+
*/
|
|
312
|
+
static #safeIdentifier(value) {
|
|
313
|
+
return String(value || 'selected_part').replace(/[^a-z0-9_]/giu, '_')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Normalizes a possible array.
|
|
318
|
+
* @param {unknown} value Candidate array.
|
|
319
|
+
* @returns {object[]}
|
|
320
|
+
*/
|
|
321
|
+
static #array(value) {
|
|
322
|
+
return Array.isArray(value) ? value : []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Reads a finite number with fallback.
|
|
327
|
+
* @param {unknown} value Candidate number.
|
|
328
|
+
* @param {number} fallback Fallback number.
|
|
329
|
+
* @returns {number}
|
|
330
|
+
*/
|
|
331
|
+
static #number(value, fallback) {
|
|
332
|
+
const parsed = Number(value)
|
|
333
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
334
|
+
}
|
|
335
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -4,7 +4,9 @@ export { CircuitJsonElementValidator } from './core/CircuitJsonElementValidator.
|
|
|
4
4
|
export { CircuitJsonIndexer } from './core/CircuitJsonIndexer.mjs'
|
|
5
5
|
export { CircuitJsonManufacturingBuilder } from './core/CircuitJsonManufacturingBuilder.mjs'
|
|
6
6
|
export { CircuitJsonParser } from './core/CircuitJsonParser.mjs'
|
|
7
|
+
export { CircuitJsonSourceMetadata } from './core/CircuitJsonSourceMetadata.mjs'
|
|
7
8
|
export { CircuitJsonSupportMatrixBuilder } from './core/CircuitJsonSupportMatrixBuilder.mjs'
|
|
8
9
|
export { CircuitJsonUnits } from './core/CircuitJsonUnits.mjs'
|
|
9
10
|
export { SpiceCompatibilityPreprocessor } from './core/spice/SpiceCompatibilityPreprocessor.mjs'
|
|
10
11
|
export { SpiceSimulationService } from './core/spice/SpiceSimulationService.mjs'
|
|
12
|
+
export * from './renderers.mjs'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { CircuitJsonManufacturingDownloadBuilder } from './core/CircuitJsonManufacturingDownloadBuilder.mjs'
|
|
2
|
+
export { CircuitJsonPcbClearanceDiagnostics } from './core/CircuitJsonPcbClearanceDiagnostics.mjs'
|
|
3
|
+
export { CircuitJsonPcbCopperGeometry } from './core/CircuitJsonPcbCopperGeometry.mjs'
|
|
4
|
+
export { CircuitJsonPcbDrawingStyle } from './core/CircuitJsonPcbDrawingStyle.mjs'
|
|
5
|
+
export { CircuitJsonPcbHolePrimitiveModel } from './core/CircuitJsonPcbHolePrimitiveModel.mjs'
|
|
6
|
+
export { CircuitJsonPcbNetMetadata } from './core/CircuitJsonPcbNetMetadata.mjs'
|
|
7
|
+
export { CircuitJsonPcbPadPrimitiveModel } from './core/CircuitJsonPcbPadPrimitiveModel.mjs'
|
|
8
|
+
export { CircuitJsonPcbPrimitiveArtwork } from './core/CircuitJsonPcbPrimitiveArtwork.mjs'
|
|
9
|
+
export { CircuitJsonPcbPrimitiveBuilder } from './core/CircuitJsonPcbPrimitiveBuilder.mjs'
|
|
10
|
+
export { CircuitJsonPcbPrimitiveFields } from './core/CircuitJsonPcbPrimitiveFields.mjs'
|
|
11
|
+
export { CircuitJsonPcbPrimitiveGeometry } from './core/CircuitJsonPcbPrimitiveGeometry.mjs'
|
|
12
|
+
export { CircuitJsonPcbPrimitiveGroups } from './core/CircuitJsonPcbPrimitiveGroups.mjs'
|
|
13
|
+
export { CircuitJsonPcbPrimitiveIndex } from './core/CircuitJsonPcbPrimitiveIndex.mjs'
|
|
14
|
+
export { CircuitJsonPcbPrimitiveOverlays } from './core/CircuitJsonPcbPrimitiveOverlays.mjs'
|
|
15
|
+
export { CircuitJsonPcbTraceLengthModel } from './core/CircuitJsonPcbTraceLengthModel.mjs'
|
|
16
|
+
export { CircuitJsonPcbZonePrimitiveBuilder } from './core/CircuitJsonPcbZonePrimitiveBuilder.mjs'
|
|
17
|
+
export { PcbBoundsSelectionModel } from './core/PcbBoundsSelectionModel.mjs'
|
|
18
|
+
export { PcbCandidateSelectionModel } from './core/PcbCandidateSelectionModel.mjs'
|
|
19
|
+
export { PcbDiagnosticFocusModel } from './core/PcbDiagnosticFocusModel.mjs'
|
|
20
|
+
export { PcbInteractionPrimitiveModel } from './core/PcbInteractionPrimitiveModel.mjs'
|
|
21
|
+
export { SelectedPartCircuitJsonExportAdapter } from './core/SelectedPartCircuitJsonExportAdapter.mjs'
|
|
22
|
+
export { CircuitJsonPcbPrimitiveAttributeRenderer } from './ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs'
|
|
23
|
+
export { CircuitJsonPcbSvgRenderer } from './ui/CircuitJsonPcbSvgRenderer.mjs'
|
|
24
|
+
export { CircuitJsonPcbViaSvgRenderer } from './ui/CircuitJsonPcbViaSvgRenderer.mjs'
|
|
25
|
+
export { CircuitJsonSchematicSvgArcPath } from './ui/CircuitJsonSchematicSvgArcPath.mjs'
|
|
26
|
+
export { CircuitJsonSchematicSvgPortMetadata } from './ui/CircuitJsonSchematicSvgPortMetadata.mjs'
|
|
27
|
+
export { CircuitJsonSchematicSvgPrimitiveAttributes } from './ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs'
|
|
28
|
+
export { CircuitJsonSchematicSvgRenderer } from './ui/CircuitJsonSchematicSvgRenderer.mjs'
|
|
29
|
+
export { CircuitJsonSchematicTableSvgRenderer } from './ui/CircuitJsonSchematicTableSvgRenderer.mjs'
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders shared SVG attributes for CircuitJSON PCB primitives.
|
|
3
|
+
*/
|
|
4
|
+
export class CircuitJsonPcbPrimitiveAttributeRenderer {
|
|
5
|
+
/**
|
|
6
|
+
* Renders data, paint, and style attributes for one primitive.
|
|
7
|
+
* @param {object} primitive Primitive row.
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
static render(primitive) {
|
|
11
|
+
const dataAttributes = [
|
|
12
|
+
['data-pcb-primitive-id', primitive.id],
|
|
13
|
+
['data-layer', primitive.layer],
|
|
14
|
+
['data-net', primitive.netName],
|
|
15
|
+
['data-component-key', primitive.componentKey],
|
|
16
|
+
['data-footprint-id', primitive.footprintId],
|
|
17
|
+
['data-pcb-group-ids', (primitive.groupIds || []).join(' ')],
|
|
18
|
+
['data-subcircuit-ids', (primitive.subcircuitIds || []).join(' ')],
|
|
19
|
+
['data-source-net-id', primitive.sourceNetId],
|
|
20
|
+
['data-net-color', primitive.netColor],
|
|
21
|
+
['data-knockout', primitive.isKnockout ? 'true' : ''],
|
|
22
|
+
['data-pcb-component-side', this.#componentSide(primitive)],
|
|
23
|
+
[
|
|
24
|
+
'data-solder-mask-covered',
|
|
25
|
+
primitive.coveredWithSolderMask === undefined ||
|
|
26
|
+
primitive.coveredWithSolderMask === null
|
|
27
|
+
? ''
|
|
28
|
+
: String(Boolean(primitive.coveredWithSolderMask))
|
|
29
|
+
],
|
|
30
|
+
['data-primitive-kind', primitive.kind]
|
|
31
|
+
]
|
|
32
|
+
.filter((entry) => String(entry[1] || '').trim())
|
|
33
|
+
.map(([name, value]) => name + '="' + this.#escapeHtml(value) + '"')
|
|
34
|
+
const style = this.#styleAttribute(primitive)
|
|
35
|
+
const attributes = [
|
|
36
|
+
...dataAttributes,
|
|
37
|
+
...this.#paintAttributes(primitive)
|
|
38
|
+
]
|
|
39
|
+
if (style) attributes.push(style)
|
|
40
|
+
return attributes.join(' ')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a primitive side from explicit side or layer metadata.
|
|
45
|
+
* @param {object} primitive Primitive row.
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
static #componentSide(primitive) {
|
|
49
|
+
const side = String(primitive.side || '')
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
if (side === 'top' || side === 'bottom') return side
|
|
53
|
+
const layer = String(primitive.layer || '')
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
return layer === 'top' || layer === 'bottom' ? layer : ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds an inline style attribute for primitive metadata.
|
|
61
|
+
* @param {object} primitive Primitive row.
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
static #styleAttribute(primitive) {
|
|
65
|
+
const netColor = this.#safeColor(primitive.netColor)
|
|
66
|
+
if (!netColor) return ''
|
|
67
|
+
return 'style="--pcb-net-color: ' + netColor + '"'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds explicit SVG paint attributes for documentation primitives.
|
|
72
|
+
* @param {object} primitive Primitive row.
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
static #paintAttributes(primitive) {
|
|
76
|
+
const attributes = []
|
|
77
|
+
const stroke = this.#safeColor(primitive.strokeColor)
|
|
78
|
+
const fill = this.#safeColor(primitive.fillColor)
|
|
79
|
+
const dashArray = this.#safeDashArray(primitive.dashArray)
|
|
80
|
+
if (stroke) attributes.push('stroke="' + stroke + '"')
|
|
81
|
+
if (fill) attributes.push('fill="' + fill + '"')
|
|
82
|
+
if (dashArray) {
|
|
83
|
+
attributes.push(
|
|
84
|
+
'stroke-dasharray="' + this.#escapeHtml(dashArray) + '"'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return attributes
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns a color safe for SVG attributes and CSS variables.
|
|
92
|
+
* @param {unknown} value Color candidate.
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
static #safeColor(value) {
|
|
96
|
+
const text = String(value || '').trim()
|
|
97
|
+
return /^#[0-9a-f]{3,8}$/iu.test(text) ? text : ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns a numeric dash array safe for SVG attributes.
|
|
102
|
+
* @param {unknown} value Dash array candidate.
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
static #safeDashArray(value) {
|
|
106
|
+
const parts = String(value || '')
|
|
107
|
+
.trim()
|
|
108
|
+
.split(/\s+/u)
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
if (!parts.length) return ''
|
|
111
|
+
return parts.every((part) => Number.isFinite(Number(part)))
|
|
112
|
+
? parts.join(' ')
|
|
113
|
+
: ''
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Escapes markup text.
|
|
118
|
+
* @param {unknown} value Raw value.
|
|
119
|
+
* @returns {string}
|
|
120
|
+
*/
|
|
121
|
+
static #escapeHtml(value) {
|
|
122
|
+
return String(value ?? '')
|
|
123
|
+
.replaceAll('&', '&')
|
|
124
|
+
.replaceAll('<', '<')
|
|
125
|
+
.replaceAll('>', '>')
|
|
126
|
+
.replaceAll('"', '"')
|
|
127
|
+
}
|
|
128
|
+
}
|