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,964 @@
|
|
|
1
|
+
import { PcbInteractionPrimitiveModel } from '../core/PcbInteractionPrimitiveModel.mjs'
|
|
2
|
+
import { CircuitJsonPcbPrimitiveAttributeRenderer } from './CircuitJsonPcbPrimitiveAttributeRenderer.mjs'
|
|
3
|
+
import { CircuitJsonPcbViaSvgRenderer } from './CircuitJsonPcbViaSvgRenderer.mjs'
|
|
4
|
+
/**
|
|
5
|
+
* Renders standards-shaped PCB element arrays into app-compatible SVG.
|
|
6
|
+
*/
|
|
7
|
+
export class CircuitJsonPcbSvgRenderer {
|
|
8
|
+
/**
|
|
9
|
+
* Renders one PCB document into SVG markup.
|
|
10
|
+
* @param {object | object[]} documentModel Parsed document model.
|
|
11
|
+
* @param {{ side?: 'top' | 'bottom' }} [options] Render options.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
static render(documentModel, options = {}) {
|
|
15
|
+
const side = options.side === 'bottom' ? 'bottom' : 'top'
|
|
16
|
+
const model = PcbInteractionPrimitiveModel.build(documentModel)
|
|
17
|
+
const viewBox = CircuitJsonPcbSvgRenderer.#viewBox(model.bounds)
|
|
18
|
+
return (
|
|
19
|
+
'<svg class="pcb-svg pcb-svg--app-palette pcb-svg--circuitjson pcb-svg--' +
|
|
20
|
+
side +
|
|
21
|
+
'" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="' +
|
|
22
|
+
CircuitJsonPcbSvgRenderer.#formatViewBox(viewBox) +
|
|
23
|
+
'">' +
|
|
24
|
+
CircuitJsonPcbSvgRenderer.#renderBoard(model) +
|
|
25
|
+
CircuitJsonPcbSvgRenderer.#renderCopper(model, side) +
|
|
26
|
+
CircuitJsonPcbSvgRenderer.#renderComponentLabels(model) +
|
|
27
|
+
CircuitJsonPcbSvgRenderer.#renderOverlays(model, side) +
|
|
28
|
+
'</svg>'
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Renders the board substrate.
|
|
33
|
+
* @param {{ primitives: object[] }} model Primitive model.
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
static #renderBoard(model) {
|
|
37
|
+
const board = model.primitives.find(
|
|
38
|
+
(primitive) => primitive.kind === 'board'
|
|
39
|
+
)
|
|
40
|
+
if (!board?.bounds) return ''
|
|
41
|
+
if (Array.isArray(board.points) && board.points.length >= 3) {
|
|
42
|
+
return (
|
|
43
|
+
'<polygon class="pcb-board" data-layer="board" points="' +
|
|
44
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
45
|
+
CircuitJsonPcbSvgRenderer.#pointsAttribute(board.points)
|
|
46
|
+
) +
|
|
47
|
+
'"></polygon>'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
'<rect class="pcb-board" x="' +
|
|
52
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minX) +
|
|
53
|
+
'" y="' +
|
|
54
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.minY) +
|
|
55
|
+
'" width="' +
|
|
56
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.width) +
|
|
57
|
+
'" height="' +
|
|
58
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(board.bounds.height) +
|
|
59
|
+
'" rx="' +
|
|
60
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
61
|
+
Math.min(board.bounds.width, board.bounds.height) * 0.018
|
|
62
|
+
) +
|
|
63
|
+
'" data-layer="board"></rect>'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Renders copper and drilled primitives.
|
|
68
|
+
* @param {{ primitives: object[] }} model Primitive model.
|
|
69
|
+
* @param {'top' | 'bottom'} side Active side.
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
static #renderCopper(model, side) {
|
|
73
|
+
const surface = []
|
|
74
|
+
const subsurface = []
|
|
75
|
+
for (const primitive of model.primitives) {
|
|
76
|
+
const markup =
|
|
77
|
+
CircuitJsonPcbSvgRenderer.#renderCopperPrimitive(primitive)
|
|
78
|
+
if (!markup) continue
|
|
79
|
+
const target = CircuitJsonPcbSvgRenderer.#isSurfacePrimitive(
|
|
80
|
+
primitive,
|
|
81
|
+
side
|
|
82
|
+
)
|
|
83
|
+
? surface
|
|
84
|
+
: subsurface
|
|
85
|
+
target.push(markup)
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
'<g class="pcb-copper pcb-copper--subsurface">' +
|
|
89
|
+
subsurface.join('') +
|
|
90
|
+
'</g>' +
|
|
91
|
+
'<g class="pcb-copper pcb-copper--surface">' +
|
|
92
|
+
surface.join('') +
|
|
93
|
+
'</g>'
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Renders one copper primitive.
|
|
98
|
+
* @param {object} primitive Primitive row.
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
static #renderCopperPrimitive(primitive) {
|
|
102
|
+
if (primitive.kind === 'pad') {
|
|
103
|
+
return CircuitJsonPcbSvgRenderer.#renderPad(primitive)
|
|
104
|
+
}
|
|
105
|
+
if (primitive.kind === 'track') {
|
|
106
|
+
return CircuitJsonPcbSvgRenderer.#renderTrack(primitive)
|
|
107
|
+
}
|
|
108
|
+
if (primitive.kind === 'via') {
|
|
109
|
+
return CircuitJsonPcbSvgRenderer.#renderVia(primitive)
|
|
110
|
+
}
|
|
111
|
+
if (primitive.kind === 'zone') {
|
|
112
|
+
return CircuitJsonPcbSvgRenderer.#renderZone(primitive)
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
[
|
|
116
|
+
'silkscreen',
|
|
117
|
+
'silkscreen_text',
|
|
118
|
+
'silkscreen_line',
|
|
119
|
+
'fabrication',
|
|
120
|
+
'courtyard',
|
|
121
|
+
'keepout',
|
|
122
|
+
'cutout',
|
|
123
|
+
'copper-text',
|
|
124
|
+
'note',
|
|
125
|
+
'dimension',
|
|
126
|
+
'solder-mask',
|
|
127
|
+
'solder-paste',
|
|
128
|
+
'thermal-spoke',
|
|
129
|
+
'route-hint',
|
|
130
|
+
'breakout-point',
|
|
131
|
+
'panel'
|
|
132
|
+
].includes(primitive.kind)
|
|
133
|
+
) {
|
|
134
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailPrimitive(primitive)
|
|
135
|
+
}
|
|
136
|
+
return ''
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Renders one pad primitive.
|
|
140
|
+
* @param {object} primitive Pad primitive.
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
static #renderPad(primitive) {
|
|
144
|
+
const attributes =
|
|
145
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive)
|
|
146
|
+
const shape = String(primitive.shape || 'rect').toLowerCase()
|
|
147
|
+
const className =
|
|
148
|
+
'pcb-pad pcb-smd-pad pcb-pad--' +
|
|
149
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(shape)
|
|
150
|
+
if (shape === 'polygon' && Array.isArray(primitive.points)) {
|
|
151
|
+
return (
|
|
152
|
+
'<polygon class="' +
|
|
153
|
+
className +
|
|
154
|
+
'" ' +
|
|
155
|
+
attributes +
|
|
156
|
+
' points="' +
|
|
157
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
158
|
+
CircuitJsonPcbSvgRenderer.#pointsAttribute(primitive.points)
|
|
159
|
+
) +
|
|
160
|
+
'"></polygon>'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (shape === 'circle') {
|
|
165
|
+
return (
|
|
166
|
+
'<circle class="' +
|
|
167
|
+
className +
|
|
168
|
+
'" ' +
|
|
169
|
+
attributes +
|
|
170
|
+
' cx="' +
|
|
171
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x) +
|
|
172
|
+
'" cy="' +
|
|
173
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y) +
|
|
174
|
+
'" r="' +
|
|
175
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
176
|
+
Math.max(primitive.width, primitive.height) / 2
|
|
177
|
+
) +
|
|
178
|
+
'"></circle>'
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
'<rect class="' +
|
|
184
|
+
className +
|
|
185
|
+
'" ' +
|
|
186
|
+
attributes +
|
|
187
|
+
' x="' +
|
|
188
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.minX) +
|
|
189
|
+
'" y="' +
|
|
190
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.minY) +
|
|
191
|
+
'" width="' +
|
|
192
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.width) +
|
|
193
|
+
'" height="' +
|
|
194
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.height) +
|
|
195
|
+
'" rx="' +
|
|
196
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
197
|
+
primitive.radius ||
|
|
198
|
+
Math.min(primitive.bounds.width, primitive.bounds.height) *
|
|
199
|
+
0.18
|
|
200
|
+
) +
|
|
201
|
+
'"' +
|
|
202
|
+
CircuitJsonPcbSvgRenderer.#rotationAttribute(primitive) +
|
|
203
|
+
'></rect>'
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Renders one track primitive.
|
|
209
|
+
* @param {object} primitive Track primitive.
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
static #renderTrack(primitive) {
|
|
213
|
+
return (
|
|
214
|
+
'<line class="pcb-track pcb-segment" ' +
|
|
215
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
216
|
+
' x1="' +
|
|
217
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x1) +
|
|
218
|
+
'" y1="' +
|
|
219
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y1) +
|
|
220
|
+
'" x2="' +
|
|
221
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x2) +
|
|
222
|
+
'" y2="' +
|
|
223
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y2) +
|
|
224
|
+
'" stroke-width="' +
|
|
225
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.width) +
|
|
226
|
+
'" stroke-linecap="round"></line>'
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renders one via primitive.
|
|
232
|
+
* @param {object} primitive Via primitive.
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
static #renderVia(primitive) {
|
|
236
|
+
return CircuitJsonPcbViaSvgRenderer.render(
|
|
237
|
+
primitive,
|
|
238
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Renders one polygonal zone primitive.
|
|
244
|
+
* @param {object} primitive Zone primitive.
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
static #renderZone(primitive) {
|
|
248
|
+
if (Array.isArray(primitive.rings) && primitive.rings.length) {
|
|
249
|
+
const path = CircuitJsonPcbSvgRenderer.#ringsPathData(
|
|
250
|
+
primitive.rings
|
|
251
|
+
)
|
|
252
|
+
if (!path) return ''
|
|
253
|
+
return (
|
|
254
|
+
'<path class="pcb-zone" ' +
|
|
255
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
256
|
+
' d="' +
|
|
257
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(path) +
|
|
258
|
+
'" fill-rule="evenodd" clip-rule="evenodd"></path>'
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const points = CircuitJsonPcbSvgRenderer.#pointsAttribute(
|
|
263
|
+
primitive.points || []
|
|
264
|
+
)
|
|
265
|
+
if (!points) return ''
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
'<polygon class="pcb-zone" ' +
|
|
269
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
270
|
+
' points="' +
|
|
271
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(points) +
|
|
272
|
+
'"></polygon>'
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Renders one non-copper detail primitive.
|
|
278
|
+
* @param {object} primitive Detail primitive.
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
static #renderDetailPrimitive(primitive) {
|
|
282
|
+
if (primitive.kind === 'dimension') {
|
|
283
|
+
return CircuitJsonPcbSvgRenderer.#renderDimension(primitive)
|
|
284
|
+
}
|
|
285
|
+
if (primitive.text !== undefined) {
|
|
286
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailText(primitive)
|
|
287
|
+
}
|
|
288
|
+
if (Number.isFinite(Number(primitive.x1))) {
|
|
289
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailLine(primitive)
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(primitive.points) && primitive.points.length >= 3) {
|
|
292
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailPolygon(primitive)
|
|
293
|
+
}
|
|
294
|
+
if (String(primitive.shape || '').toLowerCase() === 'circle') {
|
|
295
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailCircle(primitive)
|
|
296
|
+
}
|
|
297
|
+
return CircuitJsonPcbSvgRenderer.#renderDetailRect(primitive)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Renders a dimension line with a centered label.
|
|
302
|
+
* @param {object} primitive Dimension primitive.
|
|
303
|
+
* @returns {string}
|
|
304
|
+
*/
|
|
305
|
+
static #renderDimension(primitive) {
|
|
306
|
+
const midX = (Number(primitive.x1) + Number(primitive.x2)) / 2
|
|
307
|
+
const midY = (Number(primitive.y1) + Number(primitive.y2)) / 2
|
|
308
|
+
return (
|
|
309
|
+
'<g class="' +
|
|
310
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
311
|
+
'" ' +
|
|
312
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
313
|
+
'><line x1="' +
|
|
314
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x1) +
|
|
315
|
+
'" y1="' +
|
|
316
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y1) +
|
|
317
|
+
'" x2="' +
|
|
318
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x2) +
|
|
319
|
+
'" y2="' +
|
|
320
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y2) +
|
|
321
|
+
'" stroke-width="' +
|
|
322
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.width || 0.08) +
|
|
323
|
+
'"></line><text x="' +
|
|
324
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(midX) +
|
|
325
|
+
'" y="' +
|
|
326
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(midY) +
|
|
327
|
+
'" font-size="0.32" text-anchor="middle" dominant-baseline="central">' +
|
|
328
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(primitive.text || '') +
|
|
329
|
+
'</text></g>'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Renders a detail text primitive.
|
|
335
|
+
* @param {object} primitive Text primitive.
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
static #renderDetailText(primitive) {
|
|
339
|
+
return (
|
|
340
|
+
'<text class="' +
|
|
341
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
342
|
+
'" ' +
|
|
343
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
344
|
+
' x="' +
|
|
345
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x) +
|
|
346
|
+
'" y="' +
|
|
347
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y) +
|
|
348
|
+
'" font-size="' +
|
|
349
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.fontSize || 1) +
|
|
350
|
+
'" text-anchor="' +
|
|
351
|
+
CircuitJsonPcbSvgRenderer.#textAnchor(primitive) +
|
|
352
|
+
'" dominant-baseline="' +
|
|
353
|
+
CircuitJsonPcbSvgRenderer.#textBaseline(primitive) +
|
|
354
|
+
'"' +
|
|
355
|
+
CircuitJsonPcbSvgRenderer.#rotationAttribute(primitive) +
|
|
356
|
+
'>' +
|
|
357
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(primitive.text) +
|
|
358
|
+
'</text>'
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Renders a detail line primitive.
|
|
364
|
+
* @param {object} primitive Line primitive.
|
|
365
|
+
* @returns {string}
|
|
366
|
+
*/
|
|
367
|
+
static #renderDetailLine(primitive) {
|
|
368
|
+
return (
|
|
369
|
+
'<line class="' +
|
|
370
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
371
|
+
'" ' +
|
|
372
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
373
|
+
' x1="' +
|
|
374
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x1) +
|
|
375
|
+
'" y1="' +
|
|
376
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y1) +
|
|
377
|
+
'" x2="' +
|
|
378
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x2) +
|
|
379
|
+
'" y2="' +
|
|
380
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y2) +
|
|
381
|
+
'" stroke-width="' +
|
|
382
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.width || 0.12) +
|
|
383
|
+
'" stroke-linecap="round"></line>'
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Renders a detail polygon primitive.
|
|
389
|
+
* @param {object} primitive Polygon primitive.
|
|
390
|
+
* @returns {string}
|
|
391
|
+
*/
|
|
392
|
+
static #renderDetailPolygon(primitive) {
|
|
393
|
+
return (
|
|
394
|
+
'<polygon class="' +
|
|
395
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
396
|
+
'" ' +
|
|
397
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
398
|
+
' points="' +
|
|
399
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
400
|
+
CircuitJsonPcbSvgRenderer.#pointsAttribute(primitive.points)
|
|
401
|
+
) +
|
|
402
|
+
'"></polygon>'
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Renders a detail circle primitive.
|
|
408
|
+
* @param {object} primitive Circle primitive.
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
static #renderDetailCircle(primitive) {
|
|
412
|
+
return (
|
|
413
|
+
'<circle class="' +
|
|
414
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
415
|
+
'" ' +
|
|
416
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
417
|
+
' cx="' +
|
|
418
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x) +
|
|
419
|
+
'" cy="' +
|
|
420
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y) +
|
|
421
|
+
'" r="' +
|
|
422
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
423
|
+
primitive.radius ||
|
|
424
|
+
Math.max(primitive.width || 0, primitive.height || 0) / 2
|
|
425
|
+
) +
|
|
426
|
+
'"></circle>'
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Renders a detail rectangle primitive.
|
|
432
|
+
* @param {object} primitive Rectangle primitive.
|
|
433
|
+
* @returns {string}
|
|
434
|
+
*/
|
|
435
|
+
static #renderDetailRect(primitive) {
|
|
436
|
+
if (!primitive.bounds) return ''
|
|
437
|
+
return (
|
|
438
|
+
'<rect class="' +
|
|
439
|
+
CircuitJsonPcbSvgRenderer.#detailClass(primitive) +
|
|
440
|
+
'" ' +
|
|
441
|
+
CircuitJsonPcbSvgRenderer.#primitiveAttributes(primitive) +
|
|
442
|
+
' x="' +
|
|
443
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.minX) +
|
|
444
|
+
'" y="' +
|
|
445
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.minY) +
|
|
446
|
+
'" width="' +
|
|
447
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.width) +
|
|
448
|
+
'" height="' +
|
|
449
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.bounds.height) +
|
|
450
|
+
'" rx="' +
|
|
451
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.radius || 0) +
|
|
452
|
+
'"' +
|
|
453
|
+
CircuitJsonPcbSvgRenderer.#rotationAttribute(primitive) +
|
|
454
|
+
'></rect>'
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Renders diagnostic markers, length labels, groups, and rats-nest lines.
|
|
460
|
+
* @param {{ diagnostics?: object[], airwires?: object[], traceLengths?: object[], groups?: object[], anchorOffsets?: object[] }} model Primitive model.
|
|
461
|
+
* @param {'top' | 'bottom'} side Active board side.
|
|
462
|
+
* @returns {string}
|
|
463
|
+
*/
|
|
464
|
+
static #renderOverlays(model, side) {
|
|
465
|
+
return (
|
|
466
|
+
CircuitJsonPcbSvgRenderer.#renderGroups(model.groups || []) +
|
|
467
|
+
CircuitJsonPcbSvgRenderer.#renderAnchorOffsets(
|
|
468
|
+
model.anchorOffsets || []
|
|
469
|
+
) +
|
|
470
|
+
CircuitJsonPcbSvgRenderer.#renderAirwires(model.airwires || []) +
|
|
471
|
+
CircuitJsonPcbSvgRenderer.#renderTraceLengths(
|
|
472
|
+
model.traceLengths || [],
|
|
473
|
+
side
|
|
474
|
+
) +
|
|
475
|
+
CircuitJsonPcbSvgRenderer.#renderDiagnostics(
|
|
476
|
+
model.diagnostics || []
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Renders PCB group outlines.
|
|
483
|
+
* @param {object[]} groups Group rows.
|
|
484
|
+
* @returns {string}
|
|
485
|
+
*/
|
|
486
|
+
static #renderGroups(groups) {
|
|
487
|
+
const outlines = groups
|
|
488
|
+
.filter((group) => group.bounds)
|
|
489
|
+
.map(
|
|
490
|
+
(group) =>
|
|
491
|
+
'<rect class="pcb-group-outline" data-layer="groups" data-pcb-group-id="' +
|
|
492
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(group.id) +
|
|
493
|
+
'" data-source-group-id="' +
|
|
494
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
495
|
+
group.sourceGroupId || ''
|
|
496
|
+
) +
|
|
497
|
+
'" x="' +
|
|
498
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(group.bounds.minX) +
|
|
499
|
+
'" y="' +
|
|
500
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(group.bounds.minY) +
|
|
501
|
+
'" width="' +
|
|
502
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
503
|
+
group.bounds.width
|
|
504
|
+
) +
|
|
505
|
+
'" height="' +
|
|
506
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(
|
|
507
|
+
group.bounds.height
|
|
508
|
+
) +
|
|
509
|
+
'"><title>' +
|
|
510
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
511
|
+
group.name || group.id
|
|
512
|
+
) +
|
|
513
|
+
'</title></rect>'
|
|
514
|
+
)
|
|
515
|
+
return outlines.length
|
|
516
|
+
? '<g class="pcb-groups" data-layer="groups">' +
|
|
517
|
+
outlines.join('') +
|
|
518
|
+
'</g>'
|
|
519
|
+
: ''
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Renders group anchor offset lines.
|
|
524
|
+
* @param {object[]} anchorOffsets Anchor offset rows.
|
|
525
|
+
* @returns {string}
|
|
526
|
+
*/
|
|
527
|
+
static #renderAnchorOffsets(anchorOffsets) {
|
|
528
|
+
const lines = anchorOffsets.map(
|
|
529
|
+
(offset) =>
|
|
530
|
+
'<line class="pcb-anchor-offset" data-layer="anchor_offsets" data-pcb-group-id="' +
|
|
531
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(offset.sourceId || '') +
|
|
532
|
+
'" data-target-id="' +
|
|
533
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(offset.targetId || '') +
|
|
534
|
+
'" x1="' +
|
|
535
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(offset.start?.x) +
|
|
536
|
+
'" y1="' +
|
|
537
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(offset.start?.y) +
|
|
538
|
+
'" x2="' +
|
|
539
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(offset.end?.x) +
|
|
540
|
+
'" y2="' +
|
|
541
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(offset.end?.y) +
|
|
542
|
+
'"></line>'
|
|
543
|
+
)
|
|
544
|
+
return lines.length
|
|
545
|
+
? '<g class="pcb-anchor-offsets" data-layer="anchor_offsets">' +
|
|
546
|
+
lines.join('') +
|
|
547
|
+
'</g>'
|
|
548
|
+
: ''
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Renders routed trace length labels.
|
|
553
|
+
* @param {object[]} traceLengths Trace length rows.
|
|
554
|
+
* @param {'top' | 'bottom'} side Active board side.
|
|
555
|
+
* @returns {string}
|
|
556
|
+
*/
|
|
557
|
+
static #renderTraceLengths(traceLengths, side) {
|
|
558
|
+
const labels = traceLengths
|
|
559
|
+
.filter((row) => !row.side || row.side === side)
|
|
560
|
+
.map(
|
|
561
|
+
(row) =>
|
|
562
|
+
'<text class="' +
|
|
563
|
+
CircuitJsonPcbSvgRenderer.#traceLengthClass(row) +
|
|
564
|
+
'" data-layer="trace_lengths" data-pcb-trace-length-id="' +
|
|
565
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(row.id) +
|
|
566
|
+
'" data-net="' +
|
|
567
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(row.netName || '') +
|
|
568
|
+
'" x="' +
|
|
569
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(row.point?.x) +
|
|
570
|
+
'" y="' +
|
|
571
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(row.point?.y) +
|
|
572
|
+
'" text-anchor="middle" dominant-baseline="central">' +
|
|
573
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
574
|
+
CircuitJsonPcbSvgRenderer.#traceLengthLabel(row)
|
|
575
|
+
) +
|
|
576
|
+
'</text>'
|
|
577
|
+
)
|
|
578
|
+
return labels.length
|
|
579
|
+
? '<g class="pcb-trace-lengths" data-layer="trace_lengths">' +
|
|
580
|
+
labels.join('') +
|
|
581
|
+
'</g>'
|
|
582
|
+
: ''
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Builds trace length label classes.
|
|
587
|
+
* @param {object} row Trace length row.
|
|
588
|
+
* @returns {string}
|
|
589
|
+
*/
|
|
590
|
+
static #traceLengthClass(row) {
|
|
591
|
+
return (
|
|
592
|
+
'pcb-trace-length-label' +
|
|
593
|
+
(row?.overLimit ? ' pcb-trace-length-label--over-limit' : '')
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Builds trace length label text.
|
|
599
|
+
* @param {object} row Trace length row.
|
|
600
|
+
* @returns {string}
|
|
601
|
+
*/
|
|
602
|
+
static #traceLengthLabel(row) {
|
|
603
|
+
return String(row?.label || '').trim() || row.length + ' mm'
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Renders rats-nest airwire lines.
|
|
608
|
+
* @param {object[]} airwires Airwire rows.
|
|
609
|
+
* @returns {string}
|
|
610
|
+
*/
|
|
611
|
+
static #renderAirwires(airwires) {
|
|
612
|
+
const lines = airwires.map(
|
|
613
|
+
(airwire) =>
|
|
614
|
+
'<line class="pcb-ratsnest-line" data-layer="ratsnest" data-net="' +
|
|
615
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(airwire.netName) +
|
|
616
|
+
'" data-pcb-ratsnest-net="' +
|
|
617
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(airwire.netName) +
|
|
618
|
+
'" x1="' +
|
|
619
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(airwire.start?.x) +
|
|
620
|
+
'" y1="' +
|
|
621
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(airwire.start?.y) +
|
|
622
|
+
'" x2="' +
|
|
623
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(airwire.end?.x) +
|
|
624
|
+
'" y2="' +
|
|
625
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(airwire.end?.y) +
|
|
626
|
+
'"></line>'
|
|
627
|
+
)
|
|
628
|
+
return lines.length
|
|
629
|
+
? '<g class="pcb-ratsnest" data-layer="ratsnest">' +
|
|
630
|
+
lines.join('') +
|
|
631
|
+
'</g>'
|
|
632
|
+
: ''
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Renders diagnostic marker rows.
|
|
637
|
+
* @param {object[]} diagnostics Diagnostic rows.
|
|
638
|
+
* @returns {string}
|
|
639
|
+
*/
|
|
640
|
+
static #renderDiagnostics(diagnostics) {
|
|
641
|
+
const markers = diagnostics.map(
|
|
642
|
+
(diagnostic) =>
|
|
643
|
+
'<g class="pcb-diagnostic-marker pcb-diagnostic-marker--' +
|
|
644
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(diagnostic.severity) +
|
|
645
|
+
'" data-layer="diagnostics" data-pcb-diagnostic-id="' +
|
|
646
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(diagnostic.id) +
|
|
647
|
+
'" data-component-key="' +
|
|
648
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
649
|
+
diagnostic.componentKey || ''
|
|
650
|
+
) +
|
|
651
|
+
'" data-net="' +
|
|
652
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
653
|
+
diagnostic.netName || ''
|
|
654
|
+
) +
|
|
655
|
+
'">' +
|
|
656
|
+
'<title>' +
|
|
657
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(diagnostic.message) +
|
|
658
|
+
'</title><circle cx="' +
|
|
659
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(diagnostic.point?.x) +
|
|
660
|
+
'" cy="' +
|
|
661
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(diagnostic.point?.y) +
|
|
662
|
+
'" r="0.24"></circle></g>'
|
|
663
|
+
)
|
|
664
|
+
return markers.length
|
|
665
|
+
? '<g class="pcb-diagnostics" data-layer="diagnostics">' +
|
|
666
|
+
markers.join('') +
|
|
667
|
+
'</g>'
|
|
668
|
+
: ''
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Renders component reference labels.
|
|
673
|
+
* @param {{ components: object[] }} model Primitive model.
|
|
674
|
+
* @returns {string}
|
|
675
|
+
*/
|
|
676
|
+
static #renderComponentLabels(model) {
|
|
677
|
+
const labels = model.components
|
|
678
|
+
.filter((component) => component.componentKey)
|
|
679
|
+
.map((component) => {
|
|
680
|
+
const key = String(component.componentKey || '')
|
|
681
|
+
return (
|
|
682
|
+
'<text class="pcb-label pcb-reference pcb-footprint-text" data-component-key="' +
|
|
683
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(key) +
|
|
684
|
+
'" data-footprint-id="' +
|
|
685
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
686
|
+
'footprint:' + key + ':label'
|
|
687
|
+
) +
|
|
688
|
+
'" x="' +
|
|
689
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(component.x || 0) +
|
|
690
|
+
'" y="' +
|
|
691
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(component.y || 0) +
|
|
692
|
+
'" text-anchor="middle" dominant-baseline="central">' +
|
|
693
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
694
|
+
component.designator || key
|
|
695
|
+
) +
|
|
696
|
+
'</text>'
|
|
697
|
+
)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
return labels.length
|
|
701
|
+
? '<g class="pcb-footprints pcb-texts">' + labels.join('') + '</g>'
|
|
702
|
+
: ''
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Resolves CSS classes for detail primitives.
|
|
707
|
+
* @param {object} primitive Detail primitive.
|
|
708
|
+
* @returns {string}
|
|
709
|
+
*/
|
|
710
|
+
static #detailClass(primitive) {
|
|
711
|
+
const kind = String(primitive.kind || '')
|
|
712
|
+
if (kind === 'silkscreen_text') {
|
|
713
|
+
return (
|
|
714
|
+
'pcb-silkscreen pcb-silkscreen-text pcb-footprint-text' +
|
|
715
|
+
(primitive.isKnockout ? ' pcb-silkscreen-text--knockout' : '')
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
if (kind === 'silkscreen_line') {
|
|
719
|
+
return 'pcb-silkscreen pcb-silkscreen-line'
|
|
720
|
+
}
|
|
721
|
+
if (kind === 'silkscreen') return 'pcb-silkscreen'
|
|
722
|
+
if (kind === 'fabrication') return 'pcb-fabrication'
|
|
723
|
+
if (kind === 'courtyard') return 'pcb-courtyard'
|
|
724
|
+
if (kind === 'keepout') return 'pcb-keepout'
|
|
725
|
+
if (kind === 'cutout') return 'pcb-cutout'
|
|
726
|
+
if (kind === 'note') return 'pcb-note'
|
|
727
|
+
if (kind === 'dimension') return 'pcb-dimension'
|
|
728
|
+
if (kind === 'solder-mask') return 'pcb-solder-mask'
|
|
729
|
+
if (kind === 'solder-paste') {
|
|
730
|
+
return (
|
|
731
|
+
'pcb-solder-paste pcb-solder-paste--' +
|
|
732
|
+
CircuitJsonPcbSvgRenderer.#escapeHtml(
|
|
733
|
+
String(primitive.shape || 'rect').toLowerCase()
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
if (kind === 'thermal-spoke') return 'pcb-thermal-spoke'
|
|
738
|
+
if (kind === 'route-hint') return 'pcb-route-hint'
|
|
739
|
+
if (kind === 'breakout-point') return 'pcb-breakout-point'
|
|
740
|
+
if (kind === 'panel') return 'pcb-panel-outline'
|
|
741
|
+
if (kind === 'copper-text') {
|
|
742
|
+
return 'pcb-copper-text pcb-footprint-text'
|
|
743
|
+
}
|
|
744
|
+
return 'pcb-detail'
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Returns true when a primitive belongs to the active surface.
|
|
749
|
+
* @param {object} primitive Primitive row.
|
|
750
|
+
* @param {'top' | 'bottom'} side Active side.
|
|
751
|
+
* @returns {boolean}
|
|
752
|
+
*/
|
|
753
|
+
static #isSurfacePrimitive(primitive, side) {
|
|
754
|
+
const primitiveSide = String(primitive.side || '').trim()
|
|
755
|
+
if (primitiveSide) return primitiveSide === side
|
|
756
|
+
|
|
757
|
+
const layer = String(primitive.layer || '').toLowerCase()
|
|
758
|
+
if (side === 'bottom') return layer === 'bottom'
|
|
759
|
+
|
|
760
|
+
return layer === 'top' || !layer || layer === 'board'
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Renders shared primitive data attributes.
|
|
765
|
+
* @param {object} primitive Primitive row.
|
|
766
|
+
* @returns {string}
|
|
767
|
+
*/
|
|
768
|
+
static #primitiveAttributes(primitive) {
|
|
769
|
+
return CircuitJsonPcbPrimitiveAttributeRenderer.render(primitive)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Resolves the SVG horizontal anchor for a text primitive.
|
|
774
|
+
* @param {object} primitive Text primitive.
|
|
775
|
+
* @returns {'start' | 'middle' | 'end'}
|
|
776
|
+
*/
|
|
777
|
+
static #textAnchor(primitive) {
|
|
778
|
+
const alignment =
|
|
779
|
+
CircuitJsonPcbSvgRenderer.#normalizedAlignment(primitive)
|
|
780
|
+
if (
|
|
781
|
+
CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'right') ||
|
|
782
|
+
CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'end')
|
|
783
|
+
) {
|
|
784
|
+
return 'end'
|
|
785
|
+
}
|
|
786
|
+
if (
|
|
787
|
+
CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'left') ||
|
|
788
|
+
CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'start')
|
|
789
|
+
) {
|
|
790
|
+
return 'start'
|
|
791
|
+
}
|
|
792
|
+
return 'middle'
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Resolves the SVG vertical baseline for a text primitive.
|
|
797
|
+
* @param {object} primitive Text primitive.
|
|
798
|
+
* @returns {'text-before-edge' | 'central' | 'text-after-edge'}
|
|
799
|
+
*/
|
|
800
|
+
static #textBaseline(primitive) {
|
|
801
|
+
const alignment =
|
|
802
|
+
CircuitJsonPcbSvgRenderer.#normalizedAlignment(primitive)
|
|
803
|
+
if (CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'top')) {
|
|
804
|
+
return 'text-before-edge'
|
|
805
|
+
}
|
|
806
|
+
if (CircuitJsonPcbSvgRenderer.#alignmentHas(alignment, 'bottom')) {
|
|
807
|
+
return 'text-after-edge'
|
|
808
|
+
}
|
|
809
|
+
return 'central'
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Normalizes a text alignment token for matching.
|
|
814
|
+
* @param {object} primitive Text primitive.
|
|
815
|
+
* @returns {string}
|
|
816
|
+
*/
|
|
817
|
+
static #normalizedAlignment(primitive) {
|
|
818
|
+
return String(
|
|
819
|
+
primitive.anchorAlignment || primitive.anchor_alignment || ''
|
|
820
|
+
)
|
|
821
|
+
.trim()
|
|
822
|
+
.toLowerCase()
|
|
823
|
+
.replaceAll('-', '_')
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Returns true when a normalized alignment contains one token.
|
|
828
|
+
* @param {string} alignment Normalized alignment text.
|
|
829
|
+
* @param {string} token Token to match.
|
|
830
|
+
* @returns {boolean}
|
|
831
|
+
*/
|
|
832
|
+
static #alignmentHas(alignment, token) {
|
|
833
|
+
return alignment
|
|
834
|
+
.split(/[^a-z0-9]+/u)
|
|
835
|
+
.filter(Boolean)
|
|
836
|
+
.includes(token)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Builds a polygon points attribute.
|
|
841
|
+
* @param {{ x: number, y: number }[]} points Points.
|
|
842
|
+
* @returns {string}
|
|
843
|
+
*/
|
|
844
|
+
static #pointsAttribute(points) {
|
|
845
|
+
return (points || [])
|
|
846
|
+
.map(
|
|
847
|
+
(point) =>
|
|
848
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(point.x) +
|
|
849
|
+
',' +
|
|
850
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(point.y)
|
|
851
|
+
)
|
|
852
|
+
.join(' ')
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Builds an SVG path for zone rings.
|
|
857
|
+
* @param {{ points?: { x: number, y: number }[] }[]} rings Zone rings.
|
|
858
|
+
* @returns {string}
|
|
859
|
+
*/
|
|
860
|
+
static #ringsPathData(rings) {
|
|
861
|
+
return (rings || [])
|
|
862
|
+
.map((ring) => CircuitJsonPcbSvgRenderer.#ringPathData(ring))
|
|
863
|
+
.filter(Boolean)
|
|
864
|
+
.join(' ')
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Builds an SVG path segment for one closed ring.
|
|
869
|
+
* @param {{ points?: { x: number, y: number }[] }} ring Zone ring.
|
|
870
|
+
* @returns {string}
|
|
871
|
+
*/
|
|
872
|
+
static #ringPathData(ring) {
|
|
873
|
+
const points = Array.isArray(ring?.points) ? ring.points : []
|
|
874
|
+
if (points.length < 3) return ''
|
|
875
|
+
const [first, ...rest] = points
|
|
876
|
+
return (
|
|
877
|
+
'M ' +
|
|
878
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(first.x) +
|
|
879
|
+
' ' +
|
|
880
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(first.y) +
|
|
881
|
+
rest
|
|
882
|
+
.map(
|
|
883
|
+
(point) =>
|
|
884
|
+
' L ' +
|
|
885
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(point.x) +
|
|
886
|
+
' ' +
|
|
887
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(point.y)
|
|
888
|
+
)
|
|
889
|
+
.join('') +
|
|
890
|
+
' Z'
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Builds a rotation transform attribute for centered primitives.
|
|
896
|
+
* @param {object} primitive Primitive row.
|
|
897
|
+
* @returns {string}
|
|
898
|
+
*/
|
|
899
|
+
static #rotationAttribute(primitive) {
|
|
900
|
+
const rotation = Number(primitive.rotation || 0)
|
|
901
|
+
if (!Number.isFinite(rotation) || rotation === 0) return ''
|
|
902
|
+
return (
|
|
903
|
+
' transform="rotate(' +
|
|
904
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(rotation) +
|
|
905
|
+
' ' +
|
|
906
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.x) +
|
|
907
|
+
' ' +
|
|
908
|
+
CircuitJsonPcbSvgRenderer.#formatNumber(primitive.y) +
|
|
909
|
+
')"'
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Builds a padded SVG viewBox for model bounds.
|
|
915
|
+
* @param {{ minX: number, minY: number, width: number, height: number }} bounds Model bounds.
|
|
916
|
+
* @returns {{ minX: number, minY: number, width: number, height: number }}
|
|
917
|
+
*/
|
|
918
|
+
static #viewBox(bounds) {
|
|
919
|
+
const padding = Math.max(bounds.width, bounds.height, 1) * 0.06
|
|
920
|
+
|
|
921
|
+
return {
|
|
922
|
+
minX: bounds.minX - padding,
|
|
923
|
+
minY: bounds.minY - padding,
|
|
924
|
+
width: bounds.width + padding * 2,
|
|
925
|
+
height: bounds.height + padding * 2
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Formats a viewBox record.
|
|
931
|
+
* @param {{ minX: number, minY: number, width: number, height: number }} viewBox ViewBox record.
|
|
932
|
+
* @returns {string}
|
|
933
|
+
*/
|
|
934
|
+
static #formatViewBox(viewBox) {
|
|
935
|
+
return [viewBox.minX, viewBox.minY, viewBox.width, viewBox.height]
|
|
936
|
+
.map((value) => CircuitJsonPcbSvgRenderer.#formatNumber(value))
|
|
937
|
+
.join(' ')
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Formats one SVG number.
|
|
942
|
+
* @param {number} value Number.
|
|
943
|
+
* @returns {string}
|
|
944
|
+
*/
|
|
945
|
+
static #formatNumber(value) {
|
|
946
|
+
const number = Number(value)
|
|
947
|
+
if (!Number.isFinite(number)) return '0'
|
|
948
|
+
|
|
949
|
+
return Number(number.toFixed(6)).toString()
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Escapes markup text.
|
|
954
|
+
* @param {unknown} value Raw value.
|
|
955
|
+
* @returns {string}
|
|
956
|
+
*/
|
|
957
|
+
static #escapeHtml(value) {
|
|
958
|
+
return String(value ?? '')
|
|
959
|
+
.replaceAll('&', '&')
|
|
960
|
+
.replaceAll('<', '<')
|
|
961
|
+
.replaceAll('>', '>')
|
|
962
|
+
.replaceAll('"', '"')
|
|
963
|
+
}
|
|
964
|
+
}
|