altium-toolkit 1.0.8 → 1.0.9
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/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -12,6 +12,7 @@ import { PcbRegionPrimitiveRenderer } from './PcbRegionPrimitiveRenderer.mjs'
|
|
|
12
12
|
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
13
13
|
import { PcbTextPrimitiveRenderer } from './PcbTextPrimitiveRenderer.mjs'
|
|
14
14
|
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
15
|
+
import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
|
|
15
16
|
/**
|
|
16
17
|
* Renders normalized PCB models into HTML and SVG markup.
|
|
17
18
|
*/
|
|
@@ -19,16 +20,25 @@ export class PcbSvgRenderer {
|
|
|
19
20
|
static #PAD_SHAPE_RECTANGULAR = 2
|
|
20
21
|
static #PAD_HOLE_SHAPE_SLOT = 2
|
|
21
22
|
static #GENERIC_DETAIL_SEARCH_HALF_EXTENT = 240
|
|
23
|
+
static #SEMANTIC_SCHEMA = 'altium-toolkit.pcb.svg.semantics.a1'
|
|
22
24
|
/**
|
|
23
25
|
* Renders a normalized PCB model into HTML and SVG markup.
|
|
24
26
|
* @param {{ summary: { title?: string }, pcb?: { boardOutline: { segments: Array<Record<string, number | string>>, minX: number, minY: number, widthMil: number, heightMil: number }, layers: { name: string }[], primitiveLayers?: { layerId: number, name: string }[], polygons?: { layer?: string, segments: Array<Record<string, number | string>> }[], fills?: { x1: number, y1: number, x2: number, y2: number, layerCode?: number, layerId?: number }[], tracks?: { x1: number, y1: number, x2: number, y2: number, width: number, layerCode?: number, layerId?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, width: number, layerCode?: number, layerId?: number }[], vias?: { x: number, y: number, diameter: number, holeDiameter: number }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, shapeTop?: number, shapeMid?: number, shapeBottom?: number, rotation?: number, isPlated?: boolean }[], texts?: { text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, visible?: boolean }[], components: { designator: string, x: number, y: number, rotation: number, layer: string, pattern: string }[] } }} documentModel
|
|
27
|
+
* @param {{ viewKind?: string, layerView?: object } | undefined} options Render options.
|
|
25
28
|
* @returns {string}
|
|
26
29
|
*/
|
|
27
|
-
static render(documentModel) {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
+
static render(documentModel, options = {}) {
|
|
31
|
+
const sourcePcb = documentModel?.pcb
|
|
32
|
+
if (!sourcePcb) {
|
|
30
33
|
return '<section class="altium-renderer-empty">No PCB entities were recovered from this file.</section>'
|
|
31
34
|
}
|
|
35
|
+
const viewOptions = PcbSvgRenderer.#normalizeViewOptions(options)
|
|
36
|
+
const pcb = viewOptions.layerView
|
|
37
|
+
? PcbSvgRenderer.#filterPcbForLayer(
|
|
38
|
+
sourcePcb,
|
|
39
|
+
viewOptions.layerView
|
|
40
|
+
)
|
|
41
|
+
: sourcePcb
|
|
32
42
|
const outline = PcbScene3dBoardOutlineRefiner.refine(
|
|
33
43
|
{ board: pcb.boardOutline },
|
|
34
44
|
documentModel
|
|
@@ -47,7 +57,20 @@ export class PcbSvgRenderer {
|
|
|
47
57
|
const components = pcb.components.slice(0, 260)
|
|
48
58
|
const stackLayers = Array.isArray(pcb.layers) ? pcb.layers : []
|
|
49
59
|
const primitiveLayers = pcb.primitiveLayers || []
|
|
50
|
-
const displayLayers =
|
|
60
|
+
const displayLayers = viewOptions.layerView
|
|
61
|
+
? [viewOptions.layerView]
|
|
62
|
+
: stackLayers.length
|
|
63
|
+
? stackLayers
|
|
64
|
+
: primitiveLayers
|
|
65
|
+
const semanticContext = PcbSvgRenderer.#buildSemanticContext(
|
|
66
|
+
pcb,
|
|
67
|
+
displayLayers,
|
|
68
|
+
viewOptions
|
|
69
|
+
)
|
|
70
|
+
const semanticMetadata = PcbSvgRenderer.#buildSemanticMetadata(
|
|
71
|
+
pcb,
|
|
72
|
+
semanticContext
|
|
73
|
+
)
|
|
51
74
|
const copperGroups = PcbCopperPrimitiveSplitter.split(
|
|
52
75
|
polygons,
|
|
53
76
|
fills,
|
|
@@ -120,19 +143,31 @@ export class PcbSvgRenderer {
|
|
|
120
143
|
const polygonMarkup = (polygonList, visibilityClass) =>
|
|
121
144
|
polygonList
|
|
122
145
|
.map(
|
|
123
|
-
(polygon) =>
|
|
146
|
+
(polygon, index) =>
|
|
124
147
|
'<path class="pcb-polygon pcb-polygon--' +
|
|
125
148
|
visibilityClass +
|
|
126
149
|
'" d="' +
|
|
127
150
|
SchematicSvgUtils.escapeHtml(
|
|
128
151
|
PcbSvgRenderer.#buildBoardPath(polygon.segments)
|
|
129
152
|
) +
|
|
130
|
-
'"
|
|
153
|
+
'"' +
|
|
154
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
155
|
+
'polygon',
|
|
156
|
+
polygon,
|
|
157
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
158
|
+
semanticContext,
|
|
159
|
+
'polygons',
|
|
160
|
+
polygon,
|
|
161
|
+
index
|
|
162
|
+
),
|
|
163
|
+
semanticContext
|
|
164
|
+
) +
|
|
165
|
+
' />'
|
|
131
166
|
)
|
|
132
167
|
.join('')
|
|
133
168
|
const fillMarkup = (fillList, visibilityClass) =>
|
|
134
169
|
fillList
|
|
135
|
-
.map((fill) => {
|
|
170
|
+
.map((fill, index) => {
|
|
136
171
|
const x = Math.min(fill.x1, fill.x2)
|
|
137
172
|
const y = Math.min(fill.y1, fill.y2)
|
|
138
173
|
const width = Math.abs(fill.x2 - fill.x1)
|
|
@@ -153,14 +188,26 @@ export class PcbSvgRenderer {
|
|
|
153
188
|
SchematicSvgUtils.formatNumber(
|
|
154
189
|
Math.min(width, height) / 6
|
|
155
190
|
) +
|
|
156
|
-
'"
|
|
191
|
+
'"' +
|
|
192
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
193
|
+
'fill',
|
|
194
|
+
fill,
|
|
195
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
196
|
+
semanticContext,
|
|
197
|
+
'fills',
|
|
198
|
+
fill,
|
|
199
|
+
index
|
|
200
|
+
),
|
|
201
|
+
semanticContext
|
|
202
|
+
) +
|
|
203
|
+
' />'
|
|
157
204
|
)
|
|
158
205
|
})
|
|
159
206
|
.join('')
|
|
160
207
|
const trackMarkup = (trackList, visibilityClass) =>
|
|
161
208
|
trackList
|
|
162
209
|
.map(
|
|
163
|
-
(track) =>
|
|
210
|
+
(track, index) =>
|
|
164
211
|
'<line class="pcb-track pcb-track--' +
|
|
165
212
|
visibilityClass +
|
|
166
213
|
'" x1="' +
|
|
@@ -175,15 +222,40 @@ export class PcbSvgRenderer {
|
|
|
175
222
|
SchematicSvgUtils.formatNumber(
|
|
176
223
|
Math.max(track.width || 0, 1)
|
|
177
224
|
) +
|
|
178
|
-
'"
|
|
225
|
+
'"' +
|
|
226
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
227
|
+
'track',
|
|
228
|
+
track,
|
|
229
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
230
|
+
semanticContext,
|
|
231
|
+
'tracks',
|
|
232
|
+
track,
|
|
233
|
+
index
|
|
234
|
+
),
|
|
235
|
+
semanticContext
|
|
236
|
+
) +
|
|
237
|
+
' />'
|
|
179
238
|
)
|
|
180
239
|
.join('')
|
|
181
240
|
const arcMarkup = (arcList, visibilityClass) =>
|
|
182
241
|
arcList
|
|
183
|
-
.map((arc) =>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
242
|
+
.map((arc, index) =>
|
|
243
|
+
PcbSvgRenderer.#appendSvgAttributes(
|
|
244
|
+
PcbArcUtils.buildMarkup(
|
|
245
|
+
arc,
|
|
246
|
+
'pcb-arc pcb-arc--' + visibilityClass
|
|
247
|
+
),
|
|
248
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
249
|
+
'arc',
|
|
250
|
+
arc,
|
|
251
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
252
|
+
semanticContext,
|
|
253
|
+
'arcs',
|
|
254
|
+
arc,
|
|
255
|
+
index
|
|
256
|
+
),
|
|
257
|
+
semanticContext
|
|
258
|
+
)
|
|
187
259
|
)
|
|
188
260
|
)
|
|
189
261
|
.join('')
|
|
@@ -193,11 +265,24 @@ export class PcbSvgRenderer {
|
|
|
193
265
|
'pcb-region pcb-region--' + visibilityClass
|
|
194
266
|
)
|
|
195
267
|
const viaMarkup = vias
|
|
196
|
-
.map((via) => {
|
|
268
|
+
.map((via, index) => {
|
|
197
269
|
const ringRadius = Math.max((via.diameter || 0) / 2, 1)
|
|
198
270
|
const holeRadius = Math.max((via.holeDiameter || 0) / 2, 0.6)
|
|
271
|
+
const viaIndex = PcbSvgRenderer.#primitiveIndex(
|
|
272
|
+
semanticContext,
|
|
273
|
+
'vias',
|
|
274
|
+
via,
|
|
275
|
+
index
|
|
276
|
+
)
|
|
199
277
|
return (
|
|
200
|
-
'<g class="pcb-via"
|
|
278
|
+
'<g class="pcb-via"' +
|
|
279
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
280
|
+
'via',
|
|
281
|
+
via,
|
|
282
|
+
viaIndex,
|
|
283
|
+
semanticContext
|
|
284
|
+
) +
|
|
285
|
+
'>' +
|
|
201
286
|
'<circle class="pcb-via__pad" cx="' +
|
|
202
287
|
SchematicSvgUtils.formatNumber(via.x) +
|
|
203
288
|
'" cy="' +
|
|
@@ -211,13 +296,34 @@ export class PcbSvgRenderer {
|
|
|
211
296
|
SchematicSvgUtils.formatNumber(via.y) +
|
|
212
297
|
'" r="' +
|
|
213
298
|
SchematicSvgUtils.formatNumber(holeRadius) +
|
|
214
|
-
'"
|
|
299
|
+
'"' +
|
|
300
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
301
|
+
'data-primitive': 'via-hole',
|
|
302
|
+
'data-element-key': 'pcb-via-hole-' + viaIndex,
|
|
303
|
+
'data-hole-owner': 'via',
|
|
304
|
+
'data-hole-kind': 'via',
|
|
305
|
+
'data-plating': PcbSvgRenderer.#drillPlating(via),
|
|
306
|
+
'data-drill-render-state':
|
|
307
|
+
PcbSvgRenderer.#drillRenderState(via)
|
|
308
|
+
}) +
|
|
309
|
+
' />' +
|
|
215
310
|
'</g>'
|
|
216
311
|
)
|
|
217
312
|
})
|
|
218
313
|
.join('')
|
|
219
314
|
const padMarkup = pads
|
|
220
|
-
.map((pad) =>
|
|
315
|
+
.map((pad, index) =>
|
|
316
|
+
PcbSvgRenderer.#renderPad(
|
|
317
|
+
pad,
|
|
318
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
319
|
+
semanticContext,
|
|
320
|
+
'pads',
|
|
321
|
+
pad,
|
|
322
|
+
index
|
|
323
|
+
),
|
|
324
|
+
semanticContext
|
|
325
|
+
)
|
|
326
|
+
)
|
|
221
327
|
.join('')
|
|
222
328
|
const footprintFillMarkup = footprintPrimitives.fills
|
|
223
329
|
.map((fill) => {
|
|
@@ -267,7 +373,15 @@ export class PcbSvgRenderer {
|
|
|
267
373
|
footprintPrimitives.regions,
|
|
268
374
|
'pcb-footprint-region'
|
|
269
375
|
)
|
|
270
|
-
const textMarkup = PcbTextPrimitiveRenderer.render(texts
|
|
376
|
+
const textMarkup = PcbTextPrimitiveRenderer.render(texts, {
|
|
377
|
+
semanticContext
|
|
378
|
+
})
|
|
379
|
+
const textGeometryMarkup = viewOptions.includeTextGeometrySidecar
|
|
380
|
+
? PcbSvgRenderer.#buildTextGeometryMetadataMarkup(
|
|
381
|
+
texts,
|
|
382
|
+
semanticContext
|
|
383
|
+
)
|
|
384
|
+
: ''
|
|
271
385
|
const textGroupTransform = PcbSvgRenderer.#renderTextGroupTransform(
|
|
272
386
|
pcb.textGroupTransform
|
|
273
387
|
)
|
|
@@ -276,7 +390,7 @@ export class PcbSvgRenderer {
|
|
|
276
390
|
)
|
|
277
391
|
|
|
278
392
|
const componentMarkup = components
|
|
279
|
-
.map((component) => {
|
|
393
|
+
.map((component, index) => {
|
|
280
394
|
const bodyGeometry = PcbSvgRenderer.#footprintSize(
|
|
281
395
|
component.pattern
|
|
282
396
|
)
|
|
@@ -310,7 +424,19 @@ export class PcbSvgRenderer {
|
|
|
310
424
|
SchematicSvgUtils.formatNumber(component.y) +
|
|
311
425
|
') rotate(' +
|
|
312
426
|
SchematicSvgUtils.formatNumber(component.rotation) +
|
|
313
|
-
')"
|
|
427
|
+
')"' +
|
|
428
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
429
|
+
'component',
|
|
430
|
+
component,
|
|
431
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
432
|
+
semanticContext,
|
|
433
|
+
'components',
|
|
434
|
+
component,
|
|
435
|
+
index
|
|
436
|
+
),
|
|
437
|
+
semanticContext
|
|
438
|
+
) +
|
|
439
|
+
'>' +
|
|
314
440
|
bodyMarkup +
|
|
315
441
|
'</g>'
|
|
316
442
|
)
|
|
@@ -331,9 +457,24 @@ export class PcbSvgRenderer {
|
|
|
331
457
|
'<aside class="pcb-legend"><h4>Board stack</h4><p>Top-facing composite view</p><ul>' +
|
|
332
458
|
layerMarkup +
|
|
333
459
|
'</ul></aside>' +
|
|
334
|
-
'<svg class="pcb-svg"
|
|
335
|
-
|
|
336
|
-
'
|
|
460
|
+
'<svg class="pcb-svg"' +
|
|
461
|
+
PcbSvgRenderer.#renderRootViewBoxAttributes(viewOptions, viewBox) +
|
|
462
|
+
' preserveAspectRatio="xMidYMid meet" aria-label="PCB view" data-semantic-schema="' +
|
|
463
|
+
PcbSvgRenderer.#SEMANTIC_SCHEMA +
|
|
464
|
+
'"' +
|
|
465
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
466
|
+
'data-doc-id': viewOptions.documentId,
|
|
467
|
+
'data-doc-ver': viewOptions.documentVersion,
|
|
468
|
+
'data-view-kind': semanticContext.viewKind,
|
|
469
|
+
'data-layer-view-key': semanticContext.layerView?.layerKey,
|
|
470
|
+
'data-layer-view-display-name':
|
|
471
|
+
semanticContext.layerView?.displayName,
|
|
472
|
+
'data-included-layer-ids':
|
|
473
|
+
PcbSvgRenderer.#includedLayerIds(semanticContext),
|
|
474
|
+
'data-board-outline-only':
|
|
475
|
+
PcbSvgRenderer.#isBoardOutlineOnly(pcb)
|
|
476
|
+
}) +
|
|
477
|
+
'">' +
|
|
337
478
|
'<defs>' +
|
|
338
479
|
fontFaceMarkup +
|
|
339
480
|
'<clipPath id="' +
|
|
@@ -341,9 +482,24 @@ export class PcbSvgRenderer {
|
|
|
341
482
|
'"><path d="' +
|
|
342
483
|
SchematicSvgUtils.escapeHtml(path) +
|
|
343
484
|
'" /></clipPath></defs>' +
|
|
344
|
-
'<
|
|
485
|
+
'<metadata id="pcb-semantic-metadata" data-schema="' +
|
|
486
|
+
PcbSvgRenderer.#SEMANTIC_SCHEMA +
|
|
487
|
+
'">' +
|
|
488
|
+
SchematicSvgUtils.escapeHtml(JSON.stringify(semanticMetadata)) +
|
|
489
|
+
'</metadata>' +
|
|
490
|
+
textGeometryMarkup +
|
|
491
|
+
'<path class="board-outline pcb-layer pcb-layer--edge-cuts" data-layer-name="Edge.Cuts"' +
|
|
492
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
493
|
+
'data-primitive': 'board-outline',
|
|
494
|
+
'data-element-key': 'pcb-board-outline',
|
|
495
|
+
'data-feature': 'board-outline',
|
|
496
|
+
'data-layer-key': 'EDGE',
|
|
497
|
+
'data-layer-display-name': 'Edge.Cuts'
|
|
498
|
+
}) +
|
|
499
|
+
' d="' +
|
|
345
500
|
SchematicSvgUtils.escapeHtml(path) +
|
|
346
|
-
'"
|
|
501
|
+
'"' +
|
|
502
|
+
' />' +
|
|
347
503
|
'<g class="pcb-copper-layers" clip-path="url(#' +
|
|
348
504
|
clipPathId +
|
|
349
505
|
')">' +
|
|
@@ -380,13 +536,981 @@ export class PcbSvgRenderer {
|
|
|
380
536
|
'>' +
|
|
381
537
|
textMarkup +
|
|
382
538
|
'</g>' +
|
|
383
|
-
'<path class="board-outline board-outline--stroke pcb-layer pcb-layer--edge-cuts" data-layer-name="Edge.Cuts"
|
|
539
|
+
'<path class="board-outline board-outline--stroke pcb-layer pcb-layer--edge-cuts" data-layer-name="Edge.Cuts"' +
|
|
540
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
541
|
+
'data-primitive': 'board-outline',
|
|
542
|
+
'data-element-key': 'pcb-board-outline-stroke',
|
|
543
|
+
'data-feature': 'board-outline',
|
|
544
|
+
'data-layer-key': 'EDGE',
|
|
545
|
+
'data-layer-display-name': 'Edge.Cuts'
|
|
546
|
+
}) +
|
|
547
|
+
' d="' +
|
|
384
548
|
SchematicSvgUtils.escapeHtml(path) +
|
|
385
|
-
'"
|
|
549
|
+
'"' +
|
|
550
|
+
' />' +
|
|
386
551
|
'</svg></div></section>'
|
|
387
552
|
)
|
|
388
553
|
}
|
|
389
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Renders one deterministic SVG entry per physical or primitive layer.
|
|
557
|
+
* @param {object} documentModel Normalized PCB document model.
|
|
558
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string, role: string, svg: string }[]}
|
|
559
|
+
*/
|
|
560
|
+
static renderLayerSvgs(documentModel) {
|
|
561
|
+
const pcb = documentModel?.pcb
|
|
562
|
+
if (!pcb) {
|
|
563
|
+
return []
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return PcbSvgRenderer.#displayLayerDescriptors(pcb).map(
|
|
567
|
+
(layerView) => ({
|
|
568
|
+
...layerView,
|
|
569
|
+
svg: PcbSvgRenderer.render(documentModel, {
|
|
570
|
+
viewKind: 'layer',
|
|
571
|
+
layerView
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Normalizes renderer view options.
|
|
579
|
+
* @param {{ viewKind?: string, layerView?: object } | undefined} options Render options.
|
|
580
|
+
* @returns {{ viewKind: string, layerView: object | null, includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean }}
|
|
581
|
+
*/
|
|
582
|
+
static #normalizeViewOptions(options) {
|
|
583
|
+
const includeViewBox =
|
|
584
|
+
options?.includeViewBox ?? options?.include_view_box
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
viewKind: String(options?.viewKind || 'top-composite'),
|
|
588
|
+
layerView: options?.layerView
|
|
589
|
+
? PcbSvgRenderer.#layerDescriptor(options.layerView)
|
|
590
|
+
: null,
|
|
591
|
+
includeViewBox: includeViewBox === false ? false : true,
|
|
592
|
+
documentId: String(options?.documentId || options?.docId || ''),
|
|
593
|
+
documentVersion: String(
|
|
594
|
+
options?.documentVersion || options?.documentVer || ''
|
|
595
|
+
),
|
|
596
|
+
includeTextGeometrySidecar:
|
|
597
|
+
options?.includeTextGeometrySidecar === true ||
|
|
598
|
+
options?.textGeometry === 'sidecar'
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Renders root SVG viewBox attributes according to export options.
|
|
604
|
+
* @param {{ includeViewBox: boolean }} options Normalized options.
|
|
605
|
+
* @param {string} viewBox ViewBox value.
|
|
606
|
+
* @returns {string}
|
|
607
|
+
*/
|
|
608
|
+
static #renderRootViewBoxAttributes(options, viewBox) {
|
|
609
|
+
return options.includeViewBox
|
|
610
|
+
? ' viewBox="' + SchematicSvgUtils.escapeHtml(viewBox) + '"'
|
|
611
|
+
: ''
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Builds optional PCB text geometry metadata markup.
|
|
616
|
+
* @param {object[]} texts Text rows.
|
|
617
|
+
* @param {object} semanticContext Semantic context.
|
|
618
|
+
* @returns {string}
|
|
619
|
+
*/
|
|
620
|
+
static #buildTextGeometryMetadataMarkup(texts, semanticContext) {
|
|
621
|
+
const metadata = TextGeometrySidecarBuilder.buildPcb(
|
|
622
|
+
texts,
|
|
623
|
+
semanticContext.primitiveIndexes?.texts
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
return (
|
|
627
|
+
'<metadata id="pcb-text-geometry" data-schema="' +
|
|
628
|
+
TextGeometrySidecarBuilder.SCHEMA_ID +
|
|
629
|
+
'">' +
|
|
630
|
+
SchematicSvgUtils.escapeHtml(JSON.stringify(metadata)) +
|
|
631
|
+
'</metadata>'
|
|
632
|
+
)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Returns stable display layer descriptors for per-layer exports.
|
|
637
|
+
* @param {object} pcb Normalized PCB model.
|
|
638
|
+
* @returns {object[]}
|
|
639
|
+
*/
|
|
640
|
+
static #displayLayerDescriptors(pcb) {
|
|
641
|
+
const layers = Array.isArray(pcb?.layers) ? pcb.layers : []
|
|
642
|
+
const primitiveLayers = Array.isArray(pcb?.primitiveLayers)
|
|
643
|
+
? pcb.primitiveLayers
|
|
644
|
+
: []
|
|
645
|
+
const sourceLayers = layers.length ? layers : primitiveLayers
|
|
646
|
+
const byKey = new Map()
|
|
647
|
+
|
|
648
|
+
for (const layer of sourceLayers) {
|
|
649
|
+
const descriptor = PcbSvgRenderer.#layerDescriptor(layer)
|
|
650
|
+
if (descriptor && !byKey.has(descriptor.layerKey)) {
|
|
651
|
+
byKey.set(descriptor.layerKey, descriptor)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return [...byKey.values()].sort(
|
|
656
|
+
(left, right) =>
|
|
657
|
+
Number(left.layerId ?? Number.MAX_SAFE_INTEGER) -
|
|
658
|
+
Number(right.layerId ?? Number.MAX_SAFE_INTEGER) ||
|
|
659
|
+
left.displayName.localeCompare(right.displayName, undefined, {
|
|
660
|
+
numeric: true
|
|
661
|
+
})
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Clones and filters the PCB model down to one layer view.
|
|
667
|
+
* @param {object} pcb Normalized PCB model.
|
|
668
|
+
* @param {object} layerView Layer descriptor.
|
|
669
|
+
* @returns {object}
|
|
670
|
+
*/
|
|
671
|
+
static #filterPcbForLayer(pcb, layerView) {
|
|
672
|
+
const filter = (primitive) =>
|
|
673
|
+
PcbSvgRenderer.#primitiveBelongsToLayer(primitive, layerView)
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
...pcb,
|
|
677
|
+
layers: [layerView],
|
|
678
|
+
primitiveLayers: [layerView],
|
|
679
|
+
polygons: (pcb?.polygons || []).filter(filter),
|
|
680
|
+
fills: (pcb?.fills || []).filter(filter),
|
|
681
|
+
tracks: (pcb?.tracks || []).filter(filter),
|
|
682
|
+
arcs: (pcb?.arcs || []).filter(filter),
|
|
683
|
+
vias: (pcb?.vias || []).filter(filter),
|
|
684
|
+
pads: (pcb?.pads || []).filter(filter),
|
|
685
|
+
regions: (pcb?.regions || []).filter(filter),
|
|
686
|
+
shapeBasedRegions: (pcb?.shapeBasedRegions || []).filter(filter),
|
|
687
|
+
texts: (pcb?.texts || []).filter(filter),
|
|
688
|
+
components: (pcb?.components || []).filter(filter)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Returns true when one primitive belongs to the requested layer.
|
|
694
|
+
* @param {object} primitive Primitive row.
|
|
695
|
+
* @param {object} layerView Layer descriptor.
|
|
696
|
+
* @returns {boolean}
|
|
697
|
+
*/
|
|
698
|
+
static #primitiveBelongsToLayer(primitive, layerView) {
|
|
699
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
700
|
+
primitive?.layerId,
|
|
701
|
+
primitive?.layerCode
|
|
702
|
+
])
|
|
703
|
+
if (Number.isInteger(layerId) && Number.isInteger(layerView?.layerId)) {
|
|
704
|
+
return layerId === layerView.layerId
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const primitiveName =
|
|
708
|
+
primitive?.layerName || primitive?.layer || primitive?.side || ''
|
|
709
|
+
if (primitiveName && layerView?.displayName) {
|
|
710
|
+
return (
|
|
711
|
+
PcbSvgRenderer.#normalizeSemanticLookup(primitiveName) ===
|
|
712
|
+
PcbSvgRenderer.#normalizeSemanticLookup(layerView.displayName)
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return (
|
|
717
|
+
!Number.isInteger(layerId) &&
|
|
718
|
+
!primitiveName &&
|
|
719
|
+
['pad', 'via', 'copper'].includes(layerView?.role)
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Builds reusable semantic lookup data for one PCB render.
|
|
725
|
+
* @param {object} pcb Normalized PCB model.
|
|
726
|
+
* @param {{ layerId?: number, layerCode?: number, index?: number, name?: string, displayName?: string }[]} displayLayers Visible layer records.
|
|
727
|
+
* @param {{ viewKind?: string, layerView?: object }} viewOptions View options.
|
|
728
|
+
* @returns {{ viewKind: string, layerView?: object, layersById: Map<number, object>, layersByName: Map<string, object>, layerDescriptors: object[], netByIndex: Map<number, object>, netClassNamesByNetName: Map<string, string[]>, componentsByIndex: Map<number, object>, primitiveIndexes: Record<string, Map<object, number>> }}
|
|
729
|
+
*/
|
|
730
|
+
static #buildSemanticContext(pcb, displayLayers, viewOptions = {}) {
|
|
731
|
+
const layerRecords = [
|
|
732
|
+
...(displayLayers || []),
|
|
733
|
+
...(pcb?.primitiveLayers || [])
|
|
734
|
+
]
|
|
735
|
+
const layersById = new Map()
|
|
736
|
+
const layersByName = new Map()
|
|
737
|
+
const layerDescriptors = []
|
|
738
|
+
|
|
739
|
+
for (const layer of layerRecords) {
|
|
740
|
+
const descriptor = PcbSvgRenderer.#layerDescriptor(layer)
|
|
741
|
+
if (!descriptor) {
|
|
742
|
+
continue
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
Number.isInteger(descriptor.layerId) &&
|
|
746
|
+
!layersById.has(descriptor.layerId)
|
|
747
|
+
) {
|
|
748
|
+
layersById.set(descriptor.layerId, descriptor)
|
|
749
|
+
}
|
|
750
|
+
const normalizedName = PcbSvgRenderer.#normalizeSemanticLookup(
|
|
751
|
+
descriptor.displayName
|
|
752
|
+
)
|
|
753
|
+
if (normalizedName && !layersByName.has(normalizedName)) {
|
|
754
|
+
layersByName.set(normalizedName, descriptor)
|
|
755
|
+
}
|
|
756
|
+
if (
|
|
757
|
+
!layerDescriptors.some(
|
|
758
|
+
(existing) => existing.layerKey === descriptor.layerKey
|
|
759
|
+
)
|
|
760
|
+
) {
|
|
761
|
+
layerDescriptors.push(descriptor)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const netByIndex = new Map()
|
|
766
|
+
const netNameByLookup = new Map()
|
|
767
|
+
for (const net of pcb?.nets || []) {
|
|
768
|
+
const netIndex = Number(net?.netIndex)
|
|
769
|
+
if (Number.isInteger(netIndex)) {
|
|
770
|
+
netByIndex.set(netIndex, net)
|
|
771
|
+
}
|
|
772
|
+
if (net?.name) {
|
|
773
|
+
netNameByLookup.set(
|
|
774
|
+
PcbSvgRenderer.#normalizeSemanticLookup(net.name),
|
|
775
|
+
net.name
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const netClassNamesByNetName = new Map()
|
|
781
|
+
for (const classRecord of pcb?.classes || []) {
|
|
782
|
+
if (!PcbSvgRenderer.#isNetClass(classRecord)) {
|
|
783
|
+
continue
|
|
784
|
+
}
|
|
785
|
+
for (const member of classRecord.members || []) {
|
|
786
|
+
const netName =
|
|
787
|
+
netNameByLookup.get(
|
|
788
|
+
PcbSvgRenderer.#normalizeSemanticLookup(member)
|
|
789
|
+
) || member
|
|
790
|
+
const classNames = netClassNamesByNetName.get(netName) || []
|
|
791
|
+
classNames.push(classRecord.name)
|
|
792
|
+
netClassNamesByNetName.set(netName, classNames)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
viewKind: viewOptions.viewKind || 'top-composite',
|
|
798
|
+
layerView: viewOptions.layerView || null,
|
|
799
|
+
layersById,
|
|
800
|
+
layersByName,
|
|
801
|
+
layerDescriptors,
|
|
802
|
+
netByIndex,
|
|
803
|
+
netClassNamesByNetName,
|
|
804
|
+
componentsByIndex: PcbSvgRenderer.#componentIndexMap(
|
|
805
|
+
pcb?.components || []
|
|
806
|
+
),
|
|
807
|
+
primitiveIndexes: {
|
|
808
|
+
polygons: PcbSvgRenderer.#objectIndexMap(pcb?.polygons || []),
|
|
809
|
+
fills: PcbSvgRenderer.#objectIndexMap(pcb?.fills || []),
|
|
810
|
+
tracks: PcbSvgRenderer.#objectIndexMap(pcb?.tracks || []),
|
|
811
|
+
arcs: PcbSvgRenderer.#objectIndexMap(pcb?.arcs || []),
|
|
812
|
+
vias: PcbSvgRenderer.#objectIndexMap(pcb?.vias || []),
|
|
813
|
+
pads: PcbSvgRenderer.#objectIndexMap(pcb?.pads || []),
|
|
814
|
+
texts: PcbSvgRenderer.#objectIndexMap(pcb?.texts || []),
|
|
815
|
+
components: PcbSvgRenderer.#objectIndexMap(
|
|
816
|
+
pcb?.components || []
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Builds a compact JSON sidecar describing semantic SVG element keys.
|
|
824
|
+
* @param {object} pcb Normalized PCB model.
|
|
825
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
826
|
+
* @returns {{ schema: string, boardOutline: object, layers: object[], elements: object[] }}
|
|
827
|
+
*/
|
|
828
|
+
static #buildSemanticMetadata(pcb, semanticContext) {
|
|
829
|
+
return {
|
|
830
|
+
schema: PcbSvgRenderer.#SEMANTIC_SCHEMA,
|
|
831
|
+
view: PcbSvgRenderer.#buildViewMetadata(pcb, semanticContext),
|
|
832
|
+
boardOutline: {
|
|
833
|
+
feature: 'board-outline',
|
|
834
|
+
elementKeys: ['pcb-board-outline', 'pcb-board-outline-stroke']
|
|
835
|
+
},
|
|
836
|
+
layers: semanticContext.layerDescriptors,
|
|
837
|
+
elements: [
|
|
838
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
839
|
+
'polygon',
|
|
840
|
+
'polygons',
|
|
841
|
+
pcb?.polygons || [],
|
|
842
|
+
semanticContext
|
|
843
|
+
),
|
|
844
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
845
|
+
'fill',
|
|
846
|
+
'fills',
|
|
847
|
+
pcb?.fills || [],
|
|
848
|
+
semanticContext
|
|
849
|
+
),
|
|
850
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
851
|
+
'track',
|
|
852
|
+
'tracks',
|
|
853
|
+
pcb?.tracks || [],
|
|
854
|
+
semanticContext
|
|
855
|
+
),
|
|
856
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
857
|
+
'arc',
|
|
858
|
+
'arcs',
|
|
859
|
+
pcb?.arcs || [],
|
|
860
|
+
semanticContext
|
|
861
|
+
),
|
|
862
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
863
|
+
'via',
|
|
864
|
+
'vias',
|
|
865
|
+
pcb?.vias || [],
|
|
866
|
+
semanticContext
|
|
867
|
+
),
|
|
868
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
869
|
+
'pad',
|
|
870
|
+
'pads',
|
|
871
|
+
pcb?.pads || [],
|
|
872
|
+
semanticContext
|
|
873
|
+
),
|
|
874
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
875
|
+
'text',
|
|
876
|
+
'texts',
|
|
877
|
+
pcb?.texts || [],
|
|
878
|
+
semanticContext
|
|
879
|
+
),
|
|
880
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
881
|
+
'component',
|
|
882
|
+
'components',
|
|
883
|
+
pcb?.components || [],
|
|
884
|
+
semanticContext
|
|
885
|
+
)
|
|
886
|
+
]
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Builds metadata for the rendered PCB view.
|
|
892
|
+
* @param {object} pcb Normalized PCB model.
|
|
893
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
894
|
+
* @returns {object}
|
|
895
|
+
*/
|
|
896
|
+
static #buildViewMetadata(pcb, semanticContext) {
|
|
897
|
+
return {
|
|
898
|
+
kind: semanticContext.viewKind || 'top-composite',
|
|
899
|
+
board: PcbSvgRenderer.#buildBoardViewMetadata(pcb),
|
|
900
|
+
layerSet: {
|
|
901
|
+
includedLayerIds:
|
|
902
|
+
PcbSvgRenderer.#includedLayerIds(semanticContext),
|
|
903
|
+
layerView: semanticContext.layerView || undefined,
|
|
904
|
+
roles: semanticContext.layerDescriptors.map((layer) =>
|
|
905
|
+
PcbSvgRenderer.#stripEmptySemanticObject({
|
|
906
|
+
layerId: layer.layerId,
|
|
907
|
+
layerKey: layer.layerKey,
|
|
908
|
+
displayName: layer.displayName,
|
|
909
|
+
role: layer.role
|
|
910
|
+
})
|
|
911
|
+
)
|
|
912
|
+
},
|
|
913
|
+
cutouts: PcbSvgRenderer.#boardCutoutMetadata(pcb),
|
|
914
|
+
drills: [
|
|
915
|
+
...(pcb?.vias || [])
|
|
916
|
+
.filter((via) => Number(via?.holeDiameter || 0) > 0)
|
|
917
|
+
.map((via, index) =>
|
|
918
|
+
PcbSvgRenderer.#drillDescriptor(
|
|
919
|
+
'via',
|
|
920
|
+
via,
|
|
921
|
+
'pcb-via-hole-' +
|
|
922
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
923
|
+
semanticContext,
|
|
924
|
+
'vias',
|
|
925
|
+
via,
|
|
926
|
+
index
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
),
|
|
930
|
+
...(pcb?.pads || [])
|
|
931
|
+
.filter((pad) => Number(pad?.holeDiameter || 0) > 0)
|
|
932
|
+
.map((pad, index) =>
|
|
933
|
+
PcbSvgRenderer.#drillDescriptor(
|
|
934
|
+
'pad',
|
|
935
|
+
pad,
|
|
936
|
+
'pcb-pad-hole-' +
|
|
937
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
938
|
+
semanticContext,
|
|
939
|
+
'pads',
|
|
940
|
+
pad,
|
|
941
|
+
index
|
|
942
|
+
)
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Builds board-level view metadata.
|
|
951
|
+
* @param {object} pcb Normalized PCB model.
|
|
952
|
+
* @returns {object}
|
|
953
|
+
*/
|
|
954
|
+
static #buildBoardViewMetadata(pcb) {
|
|
955
|
+
const outline = pcb?.boardOutline || {}
|
|
956
|
+
const minX = Number(outline.minX || 0)
|
|
957
|
+
const minY = Number(outline.minY || 0)
|
|
958
|
+
const width = Number(outline.widthMil || 0)
|
|
959
|
+
const height = Number(outline.heightMil || 0)
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
elementKey: 'pcb-board-outline',
|
|
963
|
+
outlineOnly: PcbSvgRenderer.#isBoardOutlineOnly(pcb),
|
|
964
|
+
centroid: {
|
|
965
|
+
x: minX + width / 2,
|
|
966
|
+
y: minY + height / 2
|
|
967
|
+
},
|
|
968
|
+
bounds: {
|
|
969
|
+
minX,
|
|
970
|
+
minY,
|
|
971
|
+
maxX: minX + width,
|
|
972
|
+
maxY: minY + height
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Builds board cutout sidecar entries.
|
|
979
|
+
* @param {object} pcb Normalized PCB model.
|
|
980
|
+
* @returns {object[]}
|
|
981
|
+
*/
|
|
982
|
+
static #boardCutoutMetadata(pcb) {
|
|
983
|
+
const outlineCutouts = Array.isArray(pcb?.boardOutline?.cutouts)
|
|
984
|
+
? pcb.boardOutline.cutouts
|
|
985
|
+
: []
|
|
986
|
+
const regionCutouts = (pcb?.boardRegions || []).filter(
|
|
987
|
+
(region) => region?.isBoardCutout === true
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
return [...outlineCutouts, ...regionCutouts].map((cutout, index) =>
|
|
991
|
+
PcbSvgRenderer.#stripEmptySemanticObject({
|
|
992
|
+
id: cutout.id || cutout.uniqueId || 'cutout-' + index,
|
|
993
|
+
kind: cutout.kind || 'board-cutout',
|
|
994
|
+
elementKey: 'pcb-board-cutout-' + index
|
|
995
|
+
})
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Builds one drill sidecar entry.
|
|
1001
|
+
* @param {'pad' | 'via'} owner Drill owner kind.
|
|
1002
|
+
* @param {object} primitive Drill owner primitive.
|
|
1003
|
+
* @param {string} elementKey SVG element key.
|
|
1004
|
+
* @returns {object}
|
|
1005
|
+
*/
|
|
1006
|
+
static #drillDescriptor(owner, primitive, elementKey) {
|
|
1007
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1008
|
+
elementKey,
|
|
1009
|
+
owner,
|
|
1010
|
+
holeKind: owner,
|
|
1011
|
+
plating: PcbSvgRenderer.#drillPlating(primitive),
|
|
1012
|
+
renderState: PcbSvgRenderer.#drillRenderState(primitive),
|
|
1013
|
+
ipc4761Type:
|
|
1014
|
+
primitive?.ipc4761Type ?? primitive?.viaProtection?.ipc4761Type
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Builds sidecar entries for one primitive collection.
|
|
1020
|
+
* @param {string} primitiveKind Public primitive kind.
|
|
1021
|
+
* @param {string} collectionKey Primitive collection key.
|
|
1022
|
+
* @param {object[]} primitives Primitive records.
|
|
1023
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1024
|
+
* @returns {object[]}
|
|
1025
|
+
*/
|
|
1026
|
+
static #semanticMetadataEntries(
|
|
1027
|
+
primitiveKind,
|
|
1028
|
+
collectionKey,
|
|
1029
|
+
primitives,
|
|
1030
|
+
semanticContext
|
|
1031
|
+
) {
|
|
1032
|
+
return (primitives || []).map((primitive, fallbackIndex) => {
|
|
1033
|
+
const index = PcbSvgRenderer.#primitiveIndex(
|
|
1034
|
+
semanticContext,
|
|
1035
|
+
collectionKey,
|
|
1036
|
+
primitive,
|
|
1037
|
+
fallbackIndex
|
|
1038
|
+
)
|
|
1039
|
+
const layer = PcbSvgRenderer.#layerForPrimitive(
|
|
1040
|
+
primitive,
|
|
1041
|
+
semanticContext
|
|
1042
|
+
)
|
|
1043
|
+
const netName = PcbSvgRenderer.#netNameForPrimitive(
|
|
1044
|
+
primitive,
|
|
1045
|
+
semanticContext
|
|
1046
|
+
)
|
|
1047
|
+
const component = PcbSvgRenderer.#componentForPrimitive(
|
|
1048
|
+
primitive,
|
|
1049
|
+
semanticContext
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1053
|
+
elementKey: 'pcb-' + primitiveKind + '-' + index,
|
|
1054
|
+
primitive: primitiveKind,
|
|
1055
|
+
layerKey: layer?.layerKey,
|
|
1056
|
+
layerDisplayName: layer?.displayName,
|
|
1057
|
+
net: netName,
|
|
1058
|
+
netClasses: PcbSvgRenderer.#netClassNames(
|
|
1059
|
+
netName,
|
|
1060
|
+
semanticContext
|
|
1061
|
+
),
|
|
1062
|
+
component: component?.designator,
|
|
1063
|
+
componentIndex: Number.isInteger(
|
|
1064
|
+
Number(primitive?.componentIndex)
|
|
1065
|
+
)
|
|
1066
|
+
? Number(primitive.componentIndex)
|
|
1067
|
+
: undefined,
|
|
1068
|
+
padNumber:
|
|
1069
|
+
primitiveKind === 'pad'
|
|
1070
|
+
? PcbSvgRenderer.#padNumber(primitive)
|
|
1071
|
+
: undefined,
|
|
1072
|
+
textRole:
|
|
1073
|
+
primitiveKind === 'text'
|
|
1074
|
+
? primitive?.role || primitive?.textRole
|
|
1075
|
+
: undefined
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Renders semantic data attributes for one SVG element.
|
|
1082
|
+
* @param {string} primitiveKind Public primitive kind.
|
|
1083
|
+
* @param {object} primitive Primitive record.
|
|
1084
|
+
* @param {number} index Stable primitive index.
|
|
1085
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1086
|
+
* @returns {string}
|
|
1087
|
+
*/
|
|
1088
|
+
static #semanticAttributes(
|
|
1089
|
+
primitiveKind,
|
|
1090
|
+
primitive,
|
|
1091
|
+
index,
|
|
1092
|
+
semanticContext
|
|
1093
|
+
) {
|
|
1094
|
+
const layer = PcbSvgRenderer.#layerForPrimitive(
|
|
1095
|
+
primitive,
|
|
1096
|
+
semanticContext
|
|
1097
|
+
)
|
|
1098
|
+
const netName = PcbSvgRenderer.#netNameForPrimitive(
|
|
1099
|
+
primitive,
|
|
1100
|
+
semanticContext
|
|
1101
|
+
)
|
|
1102
|
+
const component = PcbSvgRenderer.#componentForPrimitive(
|
|
1103
|
+
primitive,
|
|
1104
|
+
semanticContext
|
|
1105
|
+
)
|
|
1106
|
+
const netClasses = PcbSvgRenderer.#netClassNames(
|
|
1107
|
+
netName,
|
|
1108
|
+
semanticContext
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
return PcbSvgRenderer.#renderDataAttributes({
|
|
1112
|
+
'data-primitive': primitiveKind,
|
|
1113
|
+
'data-element-key': 'pcb-' + primitiveKind + '-' + index,
|
|
1114
|
+
'data-layer-key': layer?.layerKey,
|
|
1115
|
+
'data-layer-display-name': layer?.displayName,
|
|
1116
|
+
'data-layer-id': layer?.layerId,
|
|
1117
|
+
'data-net': netName,
|
|
1118
|
+
'data-net-index': primitive?.netIndex,
|
|
1119
|
+
'data-net-class': netClasses[0],
|
|
1120
|
+
'data-net-classes': netClasses.length > 1 ? netClasses : undefined,
|
|
1121
|
+
'data-component': component?.designator,
|
|
1122
|
+
'data-component-index': component?.componentIndex,
|
|
1123
|
+
'data-component-unique-id': component?.uniqueId,
|
|
1124
|
+
'data-pad-number':
|
|
1125
|
+
primitiveKind === 'pad'
|
|
1126
|
+
? PcbSvgRenderer.#padNumber(primitive)
|
|
1127
|
+
: undefined
|
|
1128
|
+
})
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Inserts generated attributes into a simple SVG element string.
|
|
1133
|
+
* @param {string} markup SVG element markup.
|
|
1134
|
+
* @param {string} attributes Rendered attributes.
|
|
1135
|
+
* @returns {string}
|
|
1136
|
+
*/
|
|
1137
|
+
static #appendSvgAttributes(markup, attributes) {
|
|
1138
|
+
if (!attributes) {
|
|
1139
|
+
return markup
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return String(markup).replace(/(\s*\/?>)/u, attributes + '$1')
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Returns a stable primitive index from the original source collection.
|
|
1147
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1148
|
+
* @param {string} collectionKey Primitive collection key.
|
|
1149
|
+
* @param {object} primitive Primitive record.
|
|
1150
|
+
* @param {number} fallbackIndex Index in the rendered collection.
|
|
1151
|
+
* @returns {number}
|
|
1152
|
+
*/
|
|
1153
|
+
static #primitiveIndex(
|
|
1154
|
+
semanticContext,
|
|
1155
|
+
collectionKey,
|
|
1156
|
+
primitive,
|
|
1157
|
+
fallbackIndex
|
|
1158
|
+
) {
|
|
1159
|
+
const resolved =
|
|
1160
|
+
semanticContext.primitiveIndexes?.[collectionKey]?.get(primitive)
|
|
1161
|
+
|
|
1162
|
+
return Number.isInteger(resolved) ? resolved : fallbackIndex
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Renders a dictionary as SVG data attributes.
|
|
1167
|
+
* @param {Record<string, unknown>} attributes Attribute dictionary.
|
|
1168
|
+
* @returns {string}
|
|
1169
|
+
*/
|
|
1170
|
+
static #renderDataAttributes(attributes) {
|
|
1171
|
+
return Object.entries(attributes || {})
|
|
1172
|
+
.filter(([, value]) => {
|
|
1173
|
+
if (Array.isArray(value)) {
|
|
1174
|
+
return value.length > 0
|
|
1175
|
+
}
|
|
1176
|
+
return value !== null && value !== undefined && value !== ''
|
|
1177
|
+
})
|
|
1178
|
+
.map(([name, value]) => {
|
|
1179
|
+
const renderedValue = Array.isArray(value)
|
|
1180
|
+
? value.join(',')
|
|
1181
|
+
: String(value)
|
|
1182
|
+
return (
|
|
1183
|
+
' ' +
|
|
1184
|
+
name +
|
|
1185
|
+
'="' +
|
|
1186
|
+
SchematicSvgUtils.escapeHtml(renderedValue) +
|
|
1187
|
+
'"'
|
|
1188
|
+
)
|
|
1189
|
+
})
|
|
1190
|
+
.join('')
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Builds an object identity map for stable primitive indexes.
|
|
1195
|
+
* @param {object[]} records Primitive records.
|
|
1196
|
+
* @returns {Map<object, number>}
|
|
1197
|
+
*/
|
|
1198
|
+
static #objectIndexMap(records) {
|
|
1199
|
+
return new Map((records || []).map((record, index) => [record, index]))
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Builds a component lookup keyed by native component index.
|
|
1204
|
+
* @param {object[]} components Component records.
|
|
1205
|
+
* @returns {Map<number, object>}
|
|
1206
|
+
*/
|
|
1207
|
+
static #componentIndexMap(components) {
|
|
1208
|
+
const componentsByIndex = new Map()
|
|
1209
|
+
|
|
1210
|
+
for (const component of components || []) {
|
|
1211
|
+
const componentIndex = Number(component?.componentIndex)
|
|
1212
|
+
if (Number.isInteger(componentIndex)) {
|
|
1213
|
+
componentsByIndex.set(componentIndex, component)
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return componentsByIndex
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Resolves a normalized layer descriptor from a layer record.
|
|
1222
|
+
* @param {object} layer Layer record.
|
|
1223
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
1224
|
+
*/
|
|
1225
|
+
static #layerDescriptor(layer) {
|
|
1226
|
+
if (!layer || typeof layer !== 'object') {
|
|
1227
|
+
return null
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1231
|
+
layer.layerId,
|
|
1232
|
+
layer.layerCode,
|
|
1233
|
+
layer.id,
|
|
1234
|
+
layer.index
|
|
1235
|
+
])
|
|
1236
|
+
const displayName =
|
|
1237
|
+
layer.displayName || layer.name || layer.layerName || ''
|
|
1238
|
+
const layerKey = Number.isInteger(layerId)
|
|
1239
|
+
? 'L' + layerId
|
|
1240
|
+
: PcbSvgRenderer.#normalizeSemanticLookup(displayName)
|
|
1241
|
+
|
|
1242
|
+
if (!layerKey && !displayName) {
|
|
1243
|
+
return null
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1247
|
+
layerId,
|
|
1248
|
+
layerKey,
|
|
1249
|
+
displayName: displayName || layerKey,
|
|
1250
|
+
role:
|
|
1251
|
+
layer.role ||
|
|
1252
|
+
layer.layerRole ||
|
|
1253
|
+
PcbSvgRenderer.#inferLayerRole(displayName)
|
|
1254
|
+
})
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Returns included layer ids from semantic context.
|
|
1259
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1260
|
+
* @returns {number[]}
|
|
1261
|
+
*/
|
|
1262
|
+
static #includedLayerIds(semanticContext) {
|
|
1263
|
+
return (semanticContext?.layerDescriptors || [])
|
|
1264
|
+
.map((layer) => layer.layerId)
|
|
1265
|
+
.filter((layerId) => Number.isInteger(layerId))
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Returns true when a PCB view contains only board outline metadata.
|
|
1270
|
+
* @param {object} pcb Normalized PCB model.
|
|
1271
|
+
* @returns {boolean}
|
|
1272
|
+
*/
|
|
1273
|
+
static #isBoardOutlineOnly(pcb) {
|
|
1274
|
+
return [
|
|
1275
|
+
'polygons',
|
|
1276
|
+
'fills',
|
|
1277
|
+
'tracks',
|
|
1278
|
+
'arcs',
|
|
1279
|
+
'vias',
|
|
1280
|
+
'pads',
|
|
1281
|
+
'texts',
|
|
1282
|
+
'components',
|
|
1283
|
+
'regions',
|
|
1284
|
+
'shapeBasedRegions'
|
|
1285
|
+
].every((key) => !Array.isArray(pcb?.[key]) || pcb[key].length === 0)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Infers one broad rendering role from a layer name.
|
|
1290
|
+
* @param {string} displayName Layer display name.
|
|
1291
|
+
* @returns {string}
|
|
1292
|
+
*/
|
|
1293
|
+
static #inferLayerRole(displayName) {
|
|
1294
|
+
const normalized = String(displayName || '').toLowerCase()
|
|
1295
|
+
if (/overlay|silk/u.test(normalized)) return 'overlay'
|
|
1296
|
+
if (/paste/u.test(normalized)) return 'paste'
|
|
1297
|
+
if (/mask/u.test(normalized)) return 'mask'
|
|
1298
|
+
if (/mechanical|dimension|outline/u.test(normalized)) {
|
|
1299
|
+
return 'mechanical'
|
|
1300
|
+
}
|
|
1301
|
+
if (/drill/u.test(normalized)) return 'drill'
|
|
1302
|
+
if (/layer|copper|plane/u.test(normalized)) return 'copper'
|
|
1303
|
+
return 'other'
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolves drill plating metadata for SVG and sidecar output.
|
|
1308
|
+
* @param {object} primitive Drill owner primitive.
|
|
1309
|
+
* @returns {'plated' | 'non-plated'}
|
|
1310
|
+
*/
|
|
1311
|
+
static #drillPlating(primitive) {
|
|
1312
|
+
return primitive?.isPlated === false ? 'non-plated' : 'plated'
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Resolves the visible drill state from explicit metadata and
|
|
1317
|
+
* via-protection features.
|
|
1318
|
+
* @param {object} primitive Drill owner primitive.
|
|
1319
|
+
* @returns {'open' | 'covered' | 'filled' | 'capped'}
|
|
1320
|
+
*/
|
|
1321
|
+
static #drillRenderState(primitive) {
|
|
1322
|
+
const explicit =
|
|
1323
|
+
primitive?.drillRenderState ||
|
|
1324
|
+
primitive?.renderState ||
|
|
1325
|
+
primitive?.drill?.renderState
|
|
1326
|
+
if (explicit) {
|
|
1327
|
+
return PcbSvgRenderer.#normalizeDrillRenderState(explicit)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const featureText = (primitive?.viaProtection?.features || [])
|
|
1331
|
+
.flatMap((feature) => [feature.type, feature.material])
|
|
1332
|
+
.join(' ')
|
|
1333
|
+
.toLowerCase()
|
|
1334
|
+
|
|
1335
|
+
if (/cap/u.test(featureText)) return 'capped'
|
|
1336
|
+
if (/fill|plug/u.test(featureText)) return 'filled'
|
|
1337
|
+
if (/cover|tent|mask/u.test(featureText)) return 'covered'
|
|
1338
|
+
|
|
1339
|
+
const ipcType = Number(
|
|
1340
|
+
primitive?.ipc4761Type ?? primitive?.viaProtection?.ipc4761Type
|
|
1341
|
+
)
|
|
1342
|
+
if (ipcType === 6 || ipcType === 7) return 'capped'
|
|
1343
|
+
if (ipcType === 3 || ipcType === 4 || ipcType === 5) return 'filled'
|
|
1344
|
+
if (ipcType === 1 || ipcType === 2) return 'covered'
|
|
1345
|
+
|
|
1346
|
+
return 'open'
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Normalizes a drill render-state label.
|
|
1351
|
+
* @param {unknown} value Raw state label.
|
|
1352
|
+
* @returns {'open' | 'covered' | 'filled' | 'capped'}
|
|
1353
|
+
*/
|
|
1354
|
+
static #normalizeDrillRenderState(value) {
|
|
1355
|
+
const normalized = String(value || '').toLowerCase()
|
|
1356
|
+
if (/cap/u.test(normalized)) return 'capped'
|
|
1357
|
+
if (/fill|plug/u.test(normalized)) return 'filled'
|
|
1358
|
+
if (/cover|tent|mask/u.test(normalized)) return 'covered'
|
|
1359
|
+
return 'open'
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Resolves a layer descriptor for one primitive.
|
|
1364
|
+
* @param {object} primitive Primitive record.
|
|
1365
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1366
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
1367
|
+
*/
|
|
1368
|
+
static #layerForPrimitive(primitive, semanticContext) {
|
|
1369
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1370
|
+
primitive?.layerId,
|
|
1371
|
+
primitive?.layerCode
|
|
1372
|
+
])
|
|
1373
|
+
if (Number.isInteger(layerId)) {
|
|
1374
|
+
return (
|
|
1375
|
+
semanticContext.layersById.get(layerId) || {
|
|
1376
|
+
layerId,
|
|
1377
|
+
layerKey: 'L' + layerId,
|
|
1378
|
+
displayName:
|
|
1379
|
+
primitive?.layerName ||
|
|
1380
|
+
primitive?.layer ||
|
|
1381
|
+
'Layer ' + layerId
|
|
1382
|
+
}
|
|
1383
|
+
)
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const layerName =
|
|
1387
|
+
primitive?.layerName || primitive?.layer || primitive?.side || ''
|
|
1388
|
+
const byName = semanticContext.layersByName.get(
|
|
1389
|
+
PcbSvgRenderer.#normalizeSemanticLookup(layerName)
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
return byName || null
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Resolves a net name for one primitive.
|
|
1397
|
+
* @param {object} primitive Primitive record.
|
|
1398
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1399
|
+
* @returns {string}
|
|
1400
|
+
*/
|
|
1401
|
+
static #netNameForPrimitive(primitive, semanticContext) {
|
|
1402
|
+
if (primitive?.netName) {
|
|
1403
|
+
return String(primitive.netName)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const netIndex = Number(primitive?.netIndex)
|
|
1407
|
+
if (Number.isInteger(netIndex)) {
|
|
1408
|
+
return semanticContext.netByIndex.get(netIndex)?.name || ''
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return ''
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Resolves a component owner for one primitive.
|
|
1416
|
+
* @param {object} primitive Primitive record.
|
|
1417
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1418
|
+
* @returns {object | null}
|
|
1419
|
+
*/
|
|
1420
|
+
static #componentForPrimitive(primitive, semanticContext) {
|
|
1421
|
+
if (primitive?.designator && primitive?.pattern) {
|
|
1422
|
+
return primitive
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const componentIndex = Number(primitive?.componentIndex)
|
|
1426
|
+
if (Number.isInteger(componentIndex)) {
|
|
1427
|
+
return semanticContext.componentsByIndex.get(componentIndex) || null
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
return null
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Returns class names for one net name.
|
|
1435
|
+
* @param {string} netName Net name.
|
|
1436
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1437
|
+
* @returns {string[]}
|
|
1438
|
+
*/
|
|
1439
|
+
static #netClassNames(netName, semanticContext) {
|
|
1440
|
+
return netName
|
|
1441
|
+
? semanticContext.netClassNamesByNetName.get(netName) || []
|
|
1442
|
+
: []
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Returns true when a class record describes nets.
|
|
1447
|
+
* @param {object} classRecord Class record.
|
|
1448
|
+
* @returns {boolean}
|
|
1449
|
+
*/
|
|
1450
|
+
static #isNetClass(classRecord) {
|
|
1451
|
+
return (
|
|
1452
|
+
classRecord?.kindName === 'net' || Number(classRecord?.kind) === 0
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Returns a pad number-like label from pad metadata.
|
|
1458
|
+
* @param {object} pad Pad record.
|
|
1459
|
+
* @returns {string}
|
|
1460
|
+
*/
|
|
1461
|
+
static #padNumber(pad) {
|
|
1462
|
+
return String(
|
|
1463
|
+
pad?.padNumber || pad?.designator || pad?.number || pad?.name || ''
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Returns the first finite numeric value from a list.
|
|
1469
|
+
* @param {unknown[]} values Candidate values.
|
|
1470
|
+
* @returns {number | undefined}
|
|
1471
|
+
*/
|
|
1472
|
+
static #firstFiniteNumber(values) {
|
|
1473
|
+
for (const value of values) {
|
|
1474
|
+
const parsed = Number(value)
|
|
1475
|
+
if (Number.isFinite(parsed)) {
|
|
1476
|
+
return parsed
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
return undefined
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Builds a case-insensitive lookup key for semantic names.
|
|
1485
|
+
* @param {unknown} value Raw value.
|
|
1486
|
+
* @returns {string}
|
|
1487
|
+
*/
|
|
1488
|
+
static #normalizeSemanticLookup(value) {
|
|
1489
|
+
return String(value || '')
|
|
1490
|
+
.trim()
|
|
1491
|
+
.toUpperCase()
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Removes empty fields from a semantic metadata object.
|
|
1496
|
+
* @param {Record<string, unknown>} value Metadata object.
|
|
1497
|
+
* @returns {Record<string, unknown>}
|
|
1498
|
+
*/
|
|
1499
|
+
static #stripEmptySemanticObject(value) {
|
|
1500
|
+
return Object.fromEntries(
|
|
1501
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
1502
|
+
if (Array.isArray(entryValue)) {
|
|
1503
|
+
return entryValue.length > 0
|
|
1504
|
+
}
|
|
1505
|
+
return (
|
|
1506
|
+
entryValue !== null &&
|
|
1507
|
+
entryValue !== undefined &&
|
|
1508
|
+
entryValue !== ''
|
|
1509
|
+
)
|
|
1510
|
+
})
|
|
1511
|
+
)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
390
1514
|
/**
|
|
391
1515
|
* Builds a best-effort board path from outline segments.
|
|
392
1516
|
* @param {Array<Record<string, number | string>>} segments
|
|
@@ -672,9 +1796,11 @@ export class PcbSvgRenderer {
|
|
|
672
1796
|
/**
|
|
673
1797
|
* Renders one through-hole pad as SVG.
|
|
674
1798
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number, holeDiameter?: number, shapeTop?: number, rotation?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null, offsetTopX?: number, offsetTopY?: number, hasRoundedRect?: boolean, roundedRectShapeTop?: number | null, cornerRadiusTop?: number | null }} pad
|
|
1799
|
+
* @param {number} index Stable pad index.
|
|
1800
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
675
1801
|
* @returns {string}
|
|
676
1802
|
*/
|
|
677
|
-
static #renderPad(pad) {
|
|
1803
|
+
static #renderPad(pad, index, semanticContext) {
|
|
678
1804
|
const size = PcbSvgRenderer.#resolvePadSurfaceSize(pad)
|
|
679
1805
|
const padIsCircular = PcbSvgRenderer.#isCircularPad(pad, size)
|
|
680
1806
|
const ringRadius = Math.max(Math.max(size.width, size.height) / 2, 0.6)
|
|
@@ -702,7 +1828,7 @@ export class PcbSvgRenderer {
|
|
|
702
1828
|
PcbSvgRenderer.#resolvePadCornerRadius(pad, size)
|
|
703
1829
|
) +
|
|
704
1830
|
'" />'
|
|
705
|
-
const holeMarkup = PcbSvgRenderer.#renderPadHole(pad)
|
|
1831
|
+
const holeMarkup = PcbSvgRenderer.#renderPadHole(pad, index)
|
|
706
1832
|
|
|
707
1833
|
return (
|
|
708
1834
|
'<g class="pcb-pad pcb-pad--' +
|
|
@@ -715,7 +1841,14 @@ export class PcbSvgRenderer {
|
|
|
715
1841
|
SchematicSvgUtils.formatNumber(pad.y) +
|
|
716
1842
|
') rotate(' +
|
|
717
1843
|
SchematicSvgUtils.formatNumber(Number(pad.rotation || 0)) +
|
|
718
|
-
')"
|
|
1844
|
+
')"' +
|
|
1845
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
1846
|
+
'pad',
|
|
1847
|
+
pad,
|
|
1848
|
+
index,
|
|
1849
|
+
semanticContext
|
|
1850
|
+
) +
|
|
1851
|
+
'>' +
|
|
719
1852
|
ringMarkup +
|
|
720
1853
|
holeMarkup +
|
|
721
1854
|
'</g>'
|
|
@@ -725,9 +1858,10 @@ export class PcbSvgRenderer {
|
|
|
725
1858
|
/**
|
|
726
1859
|
* Renders one pad drill hole as SVG.
|
|
727
1860
|
* @param {{ holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null }} pad
|
|
1861
|
+
* @param {number} index Stable pad index.
|
|
728
1862
|
* @returns {string}
|
|
729
1863
|
*/
|
|
730
|
-
static #renderPadHole(pad) {
|
|
1864
|
+
static #renderPadHole(pad, index) {
|
|
731
1865
|
if (Number(pad.holeDiameter || 0) <= 0) {
|
|
732
1866
|
return ''
|
|
733
1867
|
}
|
|
@@ -755,7 +1889,17 @@ export class PcbSvgRenderer {
|
|
|
755
1889
|
SchematicSvgUtils.formatNumber(holeDiameter) +
|
|
756
1890
|
'" rx="' +
|
|
757
1891
|
SchematicSvgUtils.formatNumber(holeRadius) +
|
|
758
|
-
'"
|
|
1892
|
+
'"' +
|
|
1893
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
1894
|
+
'data-primitive': 'pad-hole',
|
|
1895
|
+
'data-element-key': 'pcb-pad-hole-' + index,
|
|
1896
|
+
'data-hole-owner': 'pad',
|
|
1897
|
+
'data-hole-kind': 'pad',
|
|
1898
|
+
'data-plating': PcbSvgRenderer.#drillPlating(pad),
|
|
1899
|
+
'data-drill-render-state':
|
|
1900
|
+
PcbSvgRenderer.#drillRenderState(pad)
|
|
1901
|
+
}) +
|
|
1902
|
+
' />' +
|
|
759
1903
|
'</g>'
|
|
760
1904
|
)
|
|
761
1905
|
}
|
|
@@ -763,7 +1907,16 @@ export class PcbSvgRenderer {
|
|
|
763
1907
|
return (
|
|
764
1908
|
'<circle class="pcb-pad__hole" cx="0" cy="0" r="' +
|
|
765
1909
|
SchematicSvgUtils.formatNumber(holeRadius) +
|
|
766
|
-
'"
|
|
1910
|
+
'"' +
|
|
1911
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
1912
|
+
'data-primitive': 'pad-hole',
|
|
1913
|
+
'data-element-key': 'pcb-pad-hole-' + index,
|
|
1914
|
+
'data-hole-owner': 'pad',
|
|
1915
|
+
'data-hole-kind': 'pad',
|
|
1916
|
+
'data-plating': PcbSvgRenderer.#drillPlating(pad),
|
|
1917
|
+
'data-drill-render-state': PcbSvgRenderer.#drillRenderState(pad)
|
|
1918
|
+
}) +
|
|
1919
|
+
' />'
|
|
767
1920
|
)
|
|
768
1921
|
}
|
|
769
1922
|
|