altium-toolkit 1.0.8 → 1.0.10
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/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -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 +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- 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/ParserCompatibilityFuzzer.mjs +192 -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 +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- 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 +541 -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 +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -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/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -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 +1252 -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,1046 @@ 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
|
+
lookups: PcbSvgRenderer.#buildSemanticLookups(pcb, semanticContext),
|
|
833
|
+
boardOutline: {
|
|
834
|
+
feature: 'board-outline',
|
|
835
|
+
elementKeys: ['pcb-board-outline', 'pcb-board-outline-stroke']
|
|
836
|
+
},
|
|
837
|
+
layers: semanticContext.layerDescriptors,
|
|
838
|
+
elements: [
|
|
839
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
840
|
+
'polygon',
|
|
841
|
+
'polygons',
|
|
842
|
+
pcb?.polygons || [],
|
|
843
|
+
semanticContext
|
|
844
|
+
),
|
|
845
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
846
|
+
'fill',
|
|
847
|
+
'fills',
|
|
848
|
+
pcb?.fills || [],
|
|
849
|
+
semanticContext
|
|
850
|
+
),
|
|
851
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
852
|
+
'track',
|
|
853
|
+
'tracks',
|
|
854
|
+
pcb?.tracks || [],
|
|
855
|
+
semanticContext
|
|
856
|
+
),
|
|
857
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
858
|
+
'arc',
|
|
859
|
+
'arcs',
|
|
860
|
+
pcb?.arcs || [],
|
|
861
|
+
semanticContext
|
|
862
|
+
),
|
|
863
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
864
|
+
'via',
|
|
865
|
+
'vias',
|
|
866
|
+
pcb?.vias || [],
|
|
867
|
+
semanticContext
|
|
868
|
+
),
|
|
869
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
870
|
+
'pad',
|
|
871
|
+
'pads',
|
|
872
|
+
pcb?.pads || [],
|
|
873
|
+
semanticContext
|
|
874
|
+
),
|
|
875
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
876
|
+
'text',
|
|
877
|
+
'texts',
|
|
878
|
+
pcb?.texts || [],
|
|
879
|
+
semanticContext
|
|
880
|
+
),
|
|
881
|
+
...PcbSvgRenderer.#semanticMetadataEntries(
|
|
882
|
+
'component',
|
|
883
|
+
'components',
|
|
884
|
+
pcb?.components || [],
|
|
885
|
+
semanticContext
|
|
886
|
+
)
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Builds stable lookup maps for semantic SVG consumers.
|
|
893
|
+
* @param {object} pcb Normalized PCB model.
|
|
894
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
895
|
+
* @returns {object}
|
|
896
|
+
*/
|
|
897
|
+
static #buildSemanticLookups(pcb, semanticContext) {
|
|
898
|
+
const netsByIndex = {}
|
|
899
|
+
const netIndexByName = {}
|
|
900
|
+
const netClassesByName = {}
|
|
901
|
+
const componentsByIndex = {}
|
|
902
|
+
const componentIndexByDesignator = {}
|
|
903
|
+
const layersByKey = {}
|
|
904
|
+
const layerKeyByDisplayName = {}
|
|
905
|
+
|
|
906
|
+
for (const net of pcb?.nets || []) {
|
|
907
|
+
const netIndex = Number(net?.netIndex)
|
|
908
|
+
if (Number.isInteger(netIndex) && net?.name) {
|
|
909
|
+
netsByIndex[netIndex] = net.name
|
|
910
|
+
netIndexByName[net.name] = netIndex
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const [
|
|
915
|
+
netName,
|
|
916
|
+
classNames
|
|
917
|
+
] of semanticContext.netClassNamesByNetName) {
|
|
918
|
+
netClassesByName[netName] = [...classNames].sort((left, right) =>
|
|
919
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
for (const [
|
|
924
|
+
componentIndex,
|
|
925
|
+
component
|
|
926
|
+
] of semanticContext.componentsByIndex) {
|
|
927
|
+
componentsByIndex[componentIndex] =
|
|
928
|
+
PcbSvgRenderer.#stripEmptySemanticObject({
|
|
929
|
+
designator: component.designator,
|
|
930
|
+
uniqueId: component.uniqueId,
|
|
931
|
+
pattern: component.pattern
|
|
932
|
+
})
|
|
933
|
+
if (component.designator) {
|
|
934
|
+
componentIndexByDesignator[component.designator] =
|
|
935
|
+
componentIndex
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
for (const layer of semanticContext.layerDescriptors) {
|
|
940
|
+
layersByKey[layer.layerKey] = layer
|
|
941
|
+
layerKeyByDisplayName[layer.displayName] = layer.layerKey
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
netsByIndex,
|
|
946
|
+
netIndexByName,
|
|
947
|
+
netClassesByName,
|
|
948
|
+
componentsByIndex,
|
|
949
|
+
componentIndexByDesignator,
|
|
950
|
+
layersByKey,
|
|
951
|
+
layerKeyByDisplayName
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Builds metadata for the rendered PCB view.
|
|
957
|
+
* @param {object} pcb Normalized PCB model.
|
|
958
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
959
|
+
* @returns {object}
|
|
960
|
+
*/
|
|
961
|
+
static #buildViewMetadata(pcb, semanticContext) {
|
|
962
|
+
return {
|
|
963
|
+
kind: semanticContext.viewKind || 'top-composite',
|
|
964
|
+
board: PcbSvgRenderer.#buildBoardViewMetadata(pcb),
|
|
965
|
+
layerSet: {
|
|
966
|
+
includedLayerIds:
|
|
967
|
+
PcbSvgRenderer.#includedLayerIds(semanticContext),
|
|
968
|
+
layerView: semanticContext.layerView || undefined,
|
|
969
|
+
roles: semanticContext.layerDescriptors.map((layer) =>
|
|
970
|
+
PcbSvgRenderer.#stripEmptySemanticObject({
|
|
971
|
+
layerId: layer.layerId,
|
|
972
|
+
layerKey: layer.layerKey,
|
|
973
|
+
displayName: layer.displayName,
|
|
974
|
+
role: layer.role
|
|
975
|
+
})
|
|
976
|
+
)
|
|
977
|
+
},
|
|
978
|
+
cutouts: PcbSvgRenderer.#boardCutoutMetadata(pcb),
|
|
979
|
+
drills: [
|
|
980
|
+
...(pcb?.vias || [])
|
|
981
|
+
.filter((via) => Number(via?.holeDiameter || 0) > 0)
|
|
982
|
+
.map((via, index) =>
|
|
983
|
+
PcbSvgRenderer.#drillDescriptor(
|
|
984
|
+
'via',
|
|
985
|
+
via,
|
|
986
|
+
'pcb-via-hole-' +
|
|
987
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
988
|
+
semanticContext,
|
|
989
|
+
'vias',
|
|
990
|
+
via,
|
|
991
|
+
index
|
|
992
|
+
)
|
|
993
|
+
)
|
|
994
|
+
),
|
|
995
|
+
...(pcb?.pads || [])
|
|
996
|
+
.filter((pad) => Number(pad?.holeDiameter || 0) > 0)
|
|
997
|
+
.map((pad, index) =>
|
|
998
|
+
PcbSvgRenderer.#drillDescriptor(
|
|
999
|
+
'pad',
|
|
1000
|
+
pad,
|
|
1001
|
+
'pcb-pad-hole-' +
|
|
1002
|
+
PcbSvgRenderer.#primitiveIndex(
|
|
1003
|
+
semanticContext,
|
|
1004
|
+
'pads',
|
|
1005
|
+
pad,
|
|
1006
|
+
index
|
|
1007
|
+
)
|
|
1008
|
+
)
|
|
1009
|
+
)
|
|
1010
|
+
]
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Builds board-level view metadata.
|
|
1016
|
+
* @param {object} pcb Normalized PCB model.
|
|
1017
|
+
* @returns {object}
|
|
1018
|
+
*/
|
|
1019
|
+
static #buildBoardViewMetadata(pcb) {
|
|
1020
|
+
const outline = pcb?.boardOutline || {}
|
|
1021
|
+
const minX = Number(outline.minX || 0)
|
|
1022
|
+
const minY = Number(outline.minY || 0)
|
|
1023
|
+
const width = Number(outline.widthMil || 0)
|
|
1024
|
+
const height = Number(outline.heightMil || 0)
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
elementKey: 'pcb-board-outline',
|
|
1028
|
+
outlineOnly: PcbSvgRenderer.#isBoardOutlineOnly(pcb),
|
|
1029
|
+
centroid: {
|
|
1030
|
+
x: minX + width / 2,
|
|
1031
|
+
y: minY + height / 2
|
|
1032
|
+
},
|
|
1033
|
+
bounds: {
|
|
1034
|
+
minX,
|
|
1035
|
+
minY,
|
|
1036
|
+
maxX: minX + width,
|
|
1037
|
+
maxY: minY + height
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Builds board cutout sidecar entries.
|
|
1044
|
+
* @param {object} pcb Normalized PCB model.
|
|
1045
|
+
* @returns {object[]}
|
|
1046
|
+
*/
|
|
1047
|
+
static #boardCutoutMetadata(pcb) {
|
|
1048
|
+
const outlineCutouts = Array.isArray(pcb?.boardOutline?.cutouts)
|
|
1049
|
+
? pcb.boardOutline.cutouts
|
|
1050
|
+
: []
|
|
1051
|
+
const regionCutouts = (pcb?.boardRegions || []).filter(
|
|
1052
|
+
(region) => region?.isBoardCutout === true
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
return [...outlineCutouts, ...regionCutouts].map((cutout, index) =>
|
|
1056
|
+
PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1057
|
+
id: cutout.id || cutout.uniqueId || 'cutout-' + index,
|
|
1058
|
+
kind: cutout.kind || 'board-cutout',
|
|
1059
|
+
elementKey: 'pcb-board-cutout-' + index
|
|
1060
|
+
})
|
|
1061
|
+
)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Builds one drill sidecar entry.
|
|
1066
|
+
* @param {'pad' | 'via'} owner Drill owner kind.
|
|
1067
|
+
* @param {object} primitive Drill owner primitive.
|
|
1068
|
+
* @param {string} elementKey SVG element key.
|
|
1069
|
+
* @returns {object}
|
|
1070
|
+
*/
|
|
1071
|
+
static #drillDescriptor(owner, primitive, elementKey) {
|
|
1072
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1073
|
+
elementKey,
|
|
1074
|
+
owner,
|
|
1075
|
+
holeKind: owner,
|
|
1076
|
+
plating: PcbSvgRenderer.#drillPlating(primitive),
|
|
1077
|
+
renderState: PcbSvgRenderer.#drillRenderState(primitive),
|
|
1078
|
+
ipc4761Type:
|
|
1079
|
+
primitive?.ipc4761Type ?? primitive?.viaProtection?.ipc4761Type
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Builds sidecar entries for one primitive collection.
|
|
1085
|
+
* @param {string} primitiveKind Public primitive kind.
|
|
1086
|
+
* @param {string} collectionKey Primitive collection key.
|
|
1087
|
+
* @param {object[]} primitives Primitive records.
|
|
1088
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1089
|
+
* @returns {object[]}
|
|
1090
|
+
*/
|
|
1091
|
+
static #semanticMetadataEntries(
|
|
1092
|
+
primitiveKind,
|
|
1093
|
+
collectionKey,
|
|
1094
|
+
primitives,
|
|
1095
|
+
semanticContext
|
|
1096
|
+
) {
|
|
1097
|
+
return (primitives || []).map((primitive, fallbackIndex) => {
|
|
1098
|
+
const index = PcbSvgRenderer.#primitiveIndex(
|
|
1099
|
+
semanticContext,
|
|
1100
|
+
collectionKey,
|
|
1101
|
+
primitive,
|
|
1102
|
+
fallbackIndex
|
|
1103
|
+
)
|
|
1104
|
+
const layer = PcbSvgRenderer.#layerForPrimitive(
|
|
1105
|
+
primitive,
|
|
1106
|
+
semanticContext
|
|
1107
|
+
)
|
|
1108
|
+
const netName = PcbSvgRenderer.#netNameForPrimitive(
|
|
1109
|
+
primitive,
|
|
1110
|
+
semanticContext
|
|
1111
|
+
)
|
|
1112
|
+
const component = PcbSvgRenderer.#componentForPrimitive(
|
|
1113
|
+
primitive,
|
|
1114
|
+
semanticContext
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1118
|
+
elementKey: 'pcb-' + primitiveKind + '-' + index,
|
|
1119
|
+
primitive: primitiveKind,
|
|
1120
|
+
layerKey: layer?.layerKey,
|
|
1121
|
+
layerDisplayName: layer?.displayName,
|
|
1122
|
+
net: netName,
|
|
1123
|
+
netClasses: PcbSvgRenderer.#netClassNames(
|
|
1124
|
+
netName,
|
|
1125
|
+
semanticContext
|
|
1126
|
+
),
|
|
1127
|
+
component: component?.designator,
|
|
1128
|
+
componentIndex: Number.isInteger(
|
|
1129
|
+
Number(primitive?.componentIndex)
|
|
1130
|
+
)
|
|
1131
|
+
? Number(primitive.componentIndex)
|
|
1132
|
+
: undefined,
|
|
1133
|
+
padNumber:
|
|
1134
|
+
primitiveKind === 'pad'
|
|
1135
|
+
? PcbSvgRenderer.#padNumber(primitive)
|
|
1136
|
+
: undefined,
|
|
1137
|
+
textRole:
|
|
1138
|
+
primitiveKind === 'text'
|
|
1139
|
+
? primitive?.role || primitive?.textRole
|
|
1140
|
+
: undefined
|
|
1141
|
+
})
|
|
1142
|
+
})
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Renders semantic data attributes for one SVG element.
|
|
1147
|
+
* @param {string} primitiveKind Public primitive kind.
|
|
1148
|
+
* @param {object} primitive Primitive record.
|
|
1149
|
+
* @param {number} index Stable primitive index.
|
|
1150
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1151
|
+
* @returns {string}
|
|
1152
|
+
*/
|
|
1153
|
+
static #semanticAttributes(
|
|
1154
|
+
primitiveKind,
|
|
1155
|
+
primitive,
|
|
1156
|
+
index,
|
|
1157
|
+
semanticContext
|
|
1158
|
+
) {
|
|
1159
|
+
const layer = PcbSvgRenderer.#layerForPrimitive(
|
|
1160
|
+
primitive,
|
|
1161
|
+
semanticContext
|
|
1162
|
+
)
|
|
1163
|
+
const netName = PcbSvgRenderer.#netNameForPrimitive(
|
|
1164
|
+
primitive,
|
|
1165
|
+
semanticContext
|
|
1166
|
+
)
|
|
1167
|
+
const component = PcbSvgRenderer.#componentForPrimitive(
|
|
1168
|
+
primitive,
|
|
1169
|
+
semanticContext
|
|
1170
|
+
)
|
|
1171
|
+
const netClasses = PcbSvgRenderer.#netClassNames(
|
|
1172
|
+
netName,
|
|
1173
|
+
semanticContext
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
return PcbSvgRenderer.#renderDataAttributes({
|
|
1177
|
+
'data-primitive': primitiveKind,
|
|
1178
|
+
'data-element-key': 'pcb-' + primitiveKind + '-' + index,
|
|
1179
|
+
'data-layer-key': layer?.layerKey,
|
|
1180
|
+
'data-layer-display-name': layer?.displayName,
|
|
1181
|
+
'data-layer-id': layer?.layerId,
|
|
1182
|
+
'data-net': netName,
|
|
1183
|
+
'data-net-index': primitive?.netIndex,
|
|
1184
|
+
'data-net-class': netClasses[0],
|
|
1185
|
+
'data-net-classes': netClasses.length > 1 ? netClasses : undefined,
|
|
1186
|
+
'data-component': component?.designator,
|
|
1187
|
+
'data-component-index': component?.componentIndex,
|
|
1188
|
+
'data-component-unique-id': component?.uniqueId,
|
|
1189
|
+
'data-pad-number':
|
|
1190
|
+
primitiveKind === 'pad'
|
|
1191
|
+
? PcbSvgRenderer.#padNumber(primitive)
|
|
1192
|
+
: undefined
|
|
1193
|
+
})
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Inserts generated attributes into a simple SVG element string.
|
|
1198
|
+
* @param {string} markup SVG element markup.
|
|
1199
|
+
* @param {string} attributes Rendered attributes.
|
|
1200
|
+
* @returns {string}
|
|
1201
|
+
*/
|
|
1202
|
+
static #appendSvgAttributes(markup, attributes) {
|
|
1203
|
+
if (!attributes) {
|
|
1204
|
+
return markup
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return String(markup).replace(/(\s*\/?>)/u, attributes + '$1')
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Returns a stable primitive index from the original source collection.
|
|
1212
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1213
|
+
* @param {string} collectionKey Primitive collection key.
|
|
1214
|
+
* @param {object} primitive Primitive record.
|
|
1215
|
+
* @param {number} fallbackIndex Index in the rendered collection.
|
|
1216
|
+
* @returns {number}
|
|
1217
|
+
*/
|
|
1218
|
+
static #primitiveIndex(
|
|
1219
|
+
semanticContext,
|
|
1220
|
+
collectionKey,
|
|
1221
|
+
primitive,
|
|
1222
|
+
fallbackIndex
|
|
1223
|
+
) {
|
|
1224
|
+
const resolved =
|
|
1225
|
+
semanticContext.primitiveIndexes?.[collectionKey]?.get(primitive)
|
|
1226
|
+
|
|
1227
|
+
return Number.isInteger(resolved) ? resolved : fallbackIndex
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Renders a dictionary as SVG data attributes.
|
|
1232
|
+
* @param {Record<string, unknown>} attributes Attribute dictionary.
|
|
1233
|
+
* @returns {string}
|
|
1234
|
+
*/
|
|
1235
|
+
static #renderDataAttributes(attributes) {
|
|
1236
|
+
return Object.entries(attributes || {})
|
|
1237
|
+
.filter(([, value]) => {
|
|
1238
|
+
if (Array.isArray(value)) {
|
|
1239
|
+
return value.length > 0
|
|
1240
|
+
}
|
|
1241
|
+
return value !== null && value !== undefined && value !== ''
|
|
1242
|
+
})
|
|
1243
|
+
.map(([name, value]) => {
|
|
1244
|
+
const renderedValue = Array.isArray(value)
|
|
1245
|
+
? value.join(',')
|
|
1246
|
+
: String(value)
|
|
1247
|
+
return (
|
|
1248
|
+
' ' +
|
|
1249
|
+
name +
|
|
1250
|
+
'="' +
|
|
1251
|
+
SchematicSvgUtils.escapeHtml(renderedValue) +
|
|
1252
|
+
'"'
|
|
1253
|
+
)
|
|
1254
|
+
})
|
|
1255
|
+
.join('')
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Builds an object identity map for stable primitive indexes.
|
|
1260
|
+
* @param {object[]} records Primitive records.
|
|
1261
|
+
* @returns {Map<object, number>}
|
|
1262
|
+
*/
|
|
1263
|
+
static #objectIndexMap(records) {
|
|
1264
|
+
return new Map((records || []).map((record, index) => [record, index]))
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Builds a component lookup keyed by native component index.
|
|
1269
|
+
* @param {object[]} components Component records.
|
|
1270
|
+
* @returns {Map<number, object>}
|
|
1271
|
+
*/
|
|
1272
|
+
static #componentIndexMap(components) {
|
|
1273
|
+
const componentsByIndex = new Map()
|
|
1274
|
+
|
|
1275
|
+
for (const component of components || []) {
|
|
1276
|
+
const componentIndex = Number(component?.componentIndex)
|
|
1277
|
+
if (Number.isInteger(componentIndex)) {
|
|
1278
|
+
componentsByIndex.set(componentIndex, component)
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return componentsByIndex
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Resolves a normalized layer descriptor from a layer record.
|
|
1287
|
+
* @param {object} layer Layer record.
|
|
1288
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
1289
|
+
*/
|
|
1290
|
+
static #layerDescriptor(layer) {
|
|
1291
|
+
if (!layer || typeof layer !== 'object') {
|
|
1292
|
+
return null
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1296
|
+
layer.layerId,
|
|
1297
|
+
layer.layerCode,
|
|
1298
|
+
layer.id,
|
|
1299
|
+
layer.index
|
|
1300
|
+
])
|
|
1301
|
+
const displayName =
|
|
1302
|
+
layer.displayName || layer.name || layer.layerName || ''
|
|
1303
|
+
const layerKey = Number.isInteger(layerId)
|
|
1304
|
+
? 'L' + layerId
|
|
1305
|
+
: PcbSvgRenderer.#normalizeSemanticLookup(displayName)
|
|
1306
|
+
|
|
1307
|
+
if (!layerKey && !displayName) {
|
|
1308
|
+
return null
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return PcbSvgRenderer.#stripEmptySemanticObject({
|
|
1312
|
+
layerId,
|
|
1313
|
+
layerKey,
|
|
1314
|
+
displayName: displayName || layerKey,
|
|
1315
|
+
role:
|
|
1316
|
+
layer.role ||
|
|
1317
|
+
layer.layerRole ||
|
|
1318
|
+
PcbSvgRenderer.#inferLayerRole(displayName)
|
|
1319
|
+
})
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Returns included layer ids from semantic context.
|
|
1324
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1325
|
+
* @returns {number[]}
|
|
1326
|
+
*/
|
|
1327
|
+
static #includedLayerIds(semanticContext) {
|
|
1328
|
+
return (semanticContext?.layerDescriptors || [])
|
|
1329
|
+
.map((layer) => layer.layerId)
|
|
1330
|
+
.filter((layerId) => Number.isInteger(layerId))
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Returns true when a PCB view contains only board outline metadata.
|
|
1335
|
+
* @param {object} pcb Normalized PCB model.
|
|
1336
|
+
* @returns {boolean}
|
|
1337
|
+
*/
|
|
1338
|
+
static #isBoardOutlineOnly(pcb) {
|
|
1339
|
+
return [
|
|
1340
|
+
'polygons',
|
|
1341
|
+
'fills',
|
|
1342
|
+
'tracks',
|
|
1343
|
+
'arcs',
|
|
1344
|
+
'vias',
|
|
1345
|
+
'pads',
|
|
1346
|
+
'texts',
|
|
1347
|
+
'components',
|
|
1348
|
+
'regions',
|
|
1349
|
+
'shapeBasedRegions'
|
|
1350
|
+
].every((key) => !Array.isArray(pcb?.[key]) || pcb[key].length === 0)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Infers one broad rendering role from a layer name.
|
|
1355
|
+
* @param {string} displayName Layer display name.
|
|
1356
|
+
* @returns {string}
|
|
1357
|
+
*/
|
|
1358
|
+
static #inferLayerRole(displayName) {
|
|
1359
|
+
const normalized = String(displayName || '').toLowerCase()
|
|
1360
|
+
if (/overlay|silk/u.test(normalized)) return 'overlay'
|
|
1361
|
+
if (/paste/u.test(normalized)) return 'paste'
|
|
1362
|
+
if (/mask/u.test(normalized)) return 'mask'
|
|
1363
|
+
if (/mechanical|dimension|outline/u.test(normalized)) {
|
|
1364
|
+
return 'mechanical'
|
|
1365
|
+
}
|
|
1366
|
+
if (/drill/u.test(normalized)) return 'drill'
|
|
1367
|
+
if (/layer|copper|plane/u.test(normalized)) return 'copper'
|
|
1368
|
+
return 'other'
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Resolves drill plating metadata for SVG and sidecar output.
|
|
1373
|
+
* @param {object} primitive Drill owner primitive.
|
|
1374
|
+
* @returns {'plated' | 'non-plated'}
|
|
1375
|
+
*/
|
|
1376
|
+
static #drillPlating(primitive) {
|
|
1377
|
+
return primitive?.isPlated === false ? 'non-plated' : 'plated'
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* Resolves the visible drill state from explicit metadata and
|
|
1382
|
+
* via-protection features.
|
|
1383
|
+
* @param {object} primitive Drill owner primitive.
|
|
1384
|
+
* @returns {'open' | 'covered' | 'filled' | 'capped'}
|
|
1385
|
+
*/
|
|
1386
|
+
static #drillRenderState(primitive) {
|
|
1387
|
+
const explicit =
|
|
1388
|
+
primitive?.drillRenderState ||
|
|
1389
|
+
primitive?.renderState ||
|
|
1390
|
+
primitive?.drill?.renderState
|
|
1391
|
+
if (explicit) {
|
|
1392
|
+
return PcbSvgRenderer.#normalizeDrillRenderState(explicit)
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const featureText = (primitive?.viaProtection?.features || [])
|
|
1396
|
+
.flatMap((feature) => [feature.type, feature.material])
|
|
1397
|
+
.join(' ')
|
|
1398
|
+
.toLowerCase()
|
|
1399
|
+
|
|
1400
|
+
if (/cap/u.test(featureText)) return 'capped'
|
|
1401
|
+
if (/fill|plug/u.test(featureText)) return 'filled'
|
|
1402
|
+
if (/cover|tent|mask/u.test(featureText)) return 'covered'
|
|
1403
|
+
|
|
1404
|
+
const ipcType = Number(
|
|
1405
|
+
primitive?.ipc4761Type ?? primitive?.viaProtection?.ipc4761Type
|
|
1406
|
+
)
|
|
1407
|
+
if (ipcType === 6 || ipcType === 7) return 'capped'
|
|
1408
|
+
if (ipcType === 3 || ipcType === 4 || ipcType === 5) return 'filled'
|
|
1409
|
+
if (ipcType === 1 || ipcType === 2) return 'covered'
|
|
1410
|
+
|
|
1411
|
+
return 'open'
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Normalizes a drill render-state label.
|
|
1416
|
+
* @param {unknown} value Raw state label.
|
|
1417
|
+
* @returns {'open' | 'covered' | 'filled' | 'capped'}
|
|
1418
|
+
*/
|
|
1419
|
+
static #normalizeDrillRenderState(value) {
|
|
1420
|
+
const normalized = String(value || '').toLowerCase()
|
|
1421
|
+
if (/cap/u.test(normalized)) return 'capped'
|
|
1422
|
+
if (/fill|plug/u.test(normalized)) return 'filled'
|
|
1423
|
+
if (/cover|tent|mask/u.test(normalized)) return 'covered'
|
|
1424
|
+
return 'open'
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Resolves a layer descriptor for one primitive.
|
|
1429
|
+
* @param {object} primitive Primitive record.
|
|
1430
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1431
|
+
* @returns {{ layerId?: number, layerKey: string, displayName: string } | null}
|
|
1432
|
+
*/
|
|
1433
|
+
static #layerForPrimitive(primitive, semanticContext) {
|
|
1434
|
+
const layerId = PcbSvgRenderer.#firstFiniteNumber([
|
|
1435
|
+
primitive?.layerId,
|
|
1436
|
+
primitive?.layerCode
|
|
1437
|
+
])
|
|
1438
|
+
if (Number.isInteger(layerId)) {
|
|
1439
|
+
return (
|
|
1440
|
+
semanticContext.layersById.get(layerId) || {
|
|
1441
|
+
layerId,
|
|
1442
|
+
layerKey: 'L' + layerId,
|
|
1443
|
+
displayName:
|
|
1444
|
+
primitive?.layerName ||
|
|
1445
|
+
primitive?.layer ||
|
|
1446
|
+
'Layer ' + layerId
|
|
1447
|
+
}
|
|
1448
|
+
)
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const layerName =
|
|
1452
|
+
primitive?.layerName || primitive?.layer || primitive?.side || ''
|
|
1453
|
+
const byName = semanticContext.layersByName.get(
|
|
1454
|
+
PcbSvgRenderer.#normalizeSemanticLookup(layerName)
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
return byName || null
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Resolves a net name for one primitive.
|
|
1462
|
+
* @param {object} primitive Primitive record.
|
|
1463
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1464
|
+
* @returns {string}
|
|
1465
|
+
*/
|
|
1466
|
+
static #netNameForPrimitive(primitive, semanticContext) {
|
|
1467
|
+
if (primitive?.netName) {
|
|
1468
|
+
return String(primitive.netName)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const netIndex = Number(primitive?.netIndex)
|
|
1472
|
+
if (Number.isInteger(netIndex)) {
|
|
1473
|
+
return semanticContext.netByIndex.get(netIndex)?.name || ''
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return ''
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Resolves a component owner for one primitive.
|
|
1481
|
+
* @param {object} primitive Primitive record.
|
|
1482
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1483
|
+
* @returns {object | null}
|
|
1484
|
+
*/
|
|
1485
|
+
static #componentForPrimitive(primitive, semanticContext) {
|
|
1486
|
+
if (primitive?.designator && primitive?.pattern) {
|
|
1487
|
+
return primitive
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const componentIndex = Number(primitive?.componentIndex)
|
|
1491
|
+
if (Number.isInteger(componentIndex)) {
|
|
1492
|
+
return semanticContext.componentsByIndex.get(componentIndex) || null
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
return null
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Returns class names for one net name.
|
|
1500
|
+
* @param {string} netName Net name.
|
|
1501
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
1502
|
+
* @returns {string[]}
|
|
1503
|
+
*/
|
|
1504
|
+
static #netClassNames(netName, semanticContext) {
|
|
1505
|
+
return netName
|
|
1506
|
+
? semanticContext.netClassNamesByNetName.get(netName) || []
|
|
1507
|
+
: []
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Returns true when a class record describes nets.
|
|
1512
|
+
* @param {object} classRecord Class record.
|
|
1513
|
+
* @returns {boolean}
|
|
1514
|
+
*/
|
|
1515
|
+
static #isNetClass(classRecord) {
|
|
1516
|
+
return (
|
|
1517
|
+
classRecord?.kindName === 'net' || Number(classRecord?.kind) === 0
|
|
1518
|
+
)
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Returns a pad number-like label from pad metadata.
|
|
1523
|
+
* @param {object} pad Pad record.
|
|
1524
|
+
* @returns {string}
|
|
1525
|
+
*/
|
|
1526
|
+
static #padNumber(pad) {
|
|
1527
|
+
return String(
|
|
1528
|
+
pad?.padNumber || pad?.designator || pad?.number || pad?.name || ''
|
|
1529
|
+
)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Returns the first finite numeric value from a list.
|
|
1534
|
+
* @param {unknown[]} values Candidate values.
|
|
1535
|
+
* @returns {number | undefined}
|
|
1536
|
+
*/
|
|
1537
|
+
static #firstFiniteNumber(values) {
|
|
1538
|
+
for (const value of values) {
|
|
1539
|
+
const parsed = Number(value)
|
|
1540
|
+
if (Number.isFinite(parsed)) {
|
|
1541
|
+
return parsed
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
return undefined
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Builds a case-insensitive lookup key for semantic names.
|
|
1550
|
+
* @param {unknown} value Raw value.
|
|
1551
|
+
* @returns {string}
|
|
1552
|
+
*/
|
|
1553
|
+
static #normalizeSemanticLookup(value) {
|
|
1554
|
+
return String(value || '')
|
|
1555
|
+
.trim()
|
|
1556
|
+
.toUpperCase()
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Removes empty fields from a semantic metadata object.
|
|
1561
|
+
* @param {Record<string, unknown>} value Metadata object.
|
|
1562
|
+
* @returns {Record<string, unknown>}
|
|
1563
|
+
*/
|
|
1564
|
+
static #stripEmptySemanticObject(value) {
|
|
1565
|
+
return Object.fromEntries(
|
|
1566
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
1567
|
+
if (Array.isArray(entryValue)) {
|
|
1568
|
+
return entryValue.length > 0
|
|
1569
|
+
}
|
|
1570
|
+
return (
|
|
1571
|
+
entryValue !== null &&
|
|
1572
|
+
entryValue !== undefined &&
|
|
1573
|
+
entryValue !== ''
|
|
1574
|
+
)
|
|
1575
|
+
})
|
|
1576
|
+
)
|
|
1577
|
+
}
|
|
1578
|
+
|
|
390
1579
|
/**
|
|
391
1580
|
* Builds a best-effort board path from outline segments.
|
|
392
1581
|
* @param {Array<Record<string, number | string>>} segments
|
|
@@ -672,9 +1861,11 @@ export class PcbSvgRenderer {
|
|
|
672
1861
|
/**
|
|
673
1862
|
* Renders one through-hole pad as SVG.
|
|
674
1863
|
* @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
|
|
1864
|
+
* @param {number} index Stable pad index.
|
|
1865
|
+
* @param {object} semanticContext Semantic lookup context.
|
|
675
1866
|
* @returns {string}
|
|
676
1867
|
*/
|
|
677
|
-
static #renderPad(pad) {
|
|
1868
|
+
static #renderPad(pad, index, semanticContext) {
|
|
678
1869
|
const size = PcbSvgRenderer.#resolvePadSurfaceSize(pad)
|
|
679
1870
|
const padIsCircular = PcbSvgRenderer.#isCircularPad(pad, size)
|
|
680
1871
|
const ringRadius = Math.max(Math.max(size.width, size.height) / 2, 0.6)
|
|
@@ -702,7 +1893,7 @@ export class PcbSvgRenderer {
|
|
|
702
1893
|
PcbSvgRenderer.#resolvePadCornerRadius(pad, size)
|
|
703
1894
|
) +
|
|
704
1895
|
'" />'
|
|
705
|
-
const holeMarkup = PcbSvgRenderer.#renderPadHole(pad)
|
|
1896
|
+
const holeMarkup = PcbSvgRenderer.#renderPadHole(pad, index)
|
|
706
1897
|
|
|
707
1898
|
return (
|
|
708
1899
|
'<g class="pcb-pad pcb-pad--' +
|
|
@@ -715,7 +1906,14 @@ export class PcbSvgRenderer {
|
|
|
715
1906
|
SchematicSvgUtils.formatNumber(pad.y) +
|
|
716
1907
|
') rotate(' +
|
|
717
1908
|
SchematicSvgUtils.formatNumber(Number(pad.rotation || 0)) +
|
|
718
|
-
')"
|
|
1909
|
+
')"' +
|
|
1910
|
+
PcbSvgRenderer.#semanticAttributes(
|
|
1911
|
+
'pad',
|
|
1912
|
+
pad,
|
|
1913
|
+
index,
|
|
1914
|
+
semanticContext
|
|
1915
|
+
) +
|
|
1916
|
+
'>' +
|
|
719
1917
|
ringMarkup +
|
|
720
1918
|
holeMarkup +
|
|
721
1919
|
'</g>'
|
|
@@ -725,9 +1923,10 @@ export class PcbSvgRenderer {
|
|
|
725
1923
|
/**
|
|
726
1924
|
* Renders one pad drill hole as SVG.
|
|
727
1925
|
* @param {{ holeDiameter?: number, holeShape?: number | null, holeSlotLength?: number | null, holeRotation?: number | null }} pad
|
|
1926
|
+
* @param {number} index Stable pad index.
|
|
728
1927
|
* @returns {string}
|
|
729
1928
|
*/
|
|
730
|
-
static #renderPadHole(pad) {
|
|
1929
|
+
static #renderPadHole(pad, index) {
|
|
731
1930
|
if (Number(pad.holeDiameter || 0) <= 0) {
|
|
732
1931
|
return ''
|
|
733
1932
|
}
|
|
@@ -755,7 +1954,17 @@ export class PcbSvgRenderer {
|
|
|
755
1954
|
SchematicSvgUtils.formatNumber(holeDiameter) +
|
|
756
1955
|
'" rx="' +
|
|
757
1956
|
SchematicSvgUtils.formatNumber(holeRadius) +
|
|
758
|
-
'"
|
|
1957
|
+
'"' +
|
|
1958
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
1959
|
+
'data-primitive': 'pad-hole',
|
|
1960
|
+
'data-element-key': 'pcb-pad-hole-' + index,
|
|
1961
|
+
'data-hole-owner': 'pad',
|
|
1962
|
+
'data-hole-kind': 'pad',
|
|
1963
|
+
'data-plating': PcbSvgRenderer.#drillPlating(pad),
|
|
1964
|
+
'data-drill-render-state':
|
|
1965
|
+
PcbSvgRenderer.#drillRenderState(pad)
|
|
1966
|
+
}) +
|
|
1967
|
+
' />' +
|
|
759
1968
|
'</g>'
|
|
760
1969
|
)
|
|
761
1970
|
}
|
|
@@ -763,7 +1972,16 @@ export class PcbSvgRenderer {
|
|
|
763
1972
|
return (
|
|
764
1973
|
'<circle class="pcb-pad__hole" cx="0" cy="0" r="' +
|
|
765
1974
|
SchematicSvgUtils.formatNumber(holeRadius) +
|
|
766
|
-
'"
|
|
1975
|
+
'"' +
|
|
1976
|
+
PcbSvgRenderer.#renderDataAttributes({
|
|
1977
|
+
'data-primitive': 'pad-hole',
|
|
1978
|
+
'data-element-key': 'pcb-pad-hole-' + index,
|
|
1979
|
+
'data-hole-owner': 'pad',
|
|
1980
|
+
'data-hole-kind': 'pad',
|
|
1981
|
+
'data-plating': PcbSvgRenderer.#drillPlating(pad),
|
|
1982
|
+
'data-drill-render-state': PcbSvgRenderer.#drillRenderState(pad)
|
|
1983
|
+
}) +
|
|
1984
|
+
' />'
|
|
767
1985
|
)
|
|
768
1986
|
}
|
|
769
1987
|
|