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.
Files changed (102) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -18,6 +18,8 @@ import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs
18
18
  import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
19
19
  import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
20
20
  import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
21
+ import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
22
+ import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
21
23
 
22
24
  const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
23
25
  SchematicSvgUtils
@@ -30,13 +32,25 @@ const SECTION_HEADING_LINE_X_PADDING = 15
30
32
  * Renders normalized schematic models into presentational SVG.
31
33
  */
32
34
  export class SchematicSvgRenderer {
35
+ static #SEMANTIC_SCHEMA = 'altium-toolkit.schematic.svg.semantics.a1'
36
+
33
37
  /**
34
38
  * Renders a normalized schematic model into SVG markup.
35
39
  * @param {{ fileName?: string, summary: { title?: string }, schematic?: { sheet: { width: number, height: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }, lines: { x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, ownerIndex?: string, renderOrder?: number, recordType?: string }[], polygons?: { points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], rectangles?: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], regions?: { x: number, y: number, width: number, height: number, color: string, fill: string, renderOrder?: number }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, color: string, width: number, ownerIndex?: string, renderOrder?: number }[], directives?: { x: number, y: number, color: string, name: string, orientation?: number }[], texts: { x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }[], components: { x: number, y: number, designator: string }[], pins?: { x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }[], ports?: { x: number, y: number, width: number, height: number, name: string, fill: string, color: string, direction?: 'left' | 'right' | 'up' | 'down', shape?: 'single' | 'double' | 'plain' }[], crosses?: { x: number, y: number, size: number, color: string }[] } }} documentModel
40
+ * @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined> }} options Render options.
36
41
  * @returns {string}
37
42
  */
38
- static render(documentModel) {
39
- const schematic = documentModel?.schematic
43
+ static render(documentModel, options = {}) {
44
+ const renderOptions =
45
+ SchematicSvgRenderer.#normalizeRenderOptions(options)
46
+ const renderModel = options.projectParameters
47
+ ? SchematicProjectParameterResolver.applyToDocumentModel(
48
+ documentModel,
49
+ options.projectParameters,
50
+ { replaceText: true }
51
+ )
52
+ : documentModel
53
+ const schematic = renderModel?.schematic
40
54
  if (!schematic) {
41
55
  return '<section class="altium-renderer-empty">No schematic entities were recovered from this file.</section>'
42
56
  }
@@ -52,15 +66,22 @@ export class SchematicSvgRenderer {
52
66
  ? schematic
53
67
  : { ...schematic, sheet: renderedSheet.contentSheet }
54
68
  const allTexts = schematic.texts || []
55
- const lines = schematic.lines.slice(0, 2500)
69
+ const lines = (schematic.lines || []).slice(0, 2500)
56
70
  const polygons = (schematic.polygons || []).slice(0, 1000)
57
71
  const rectangles = (schematic.rectangles || []).slice(0, 500)
72
+ const roundedRectangles = (schematic.roundedRectangles || []).slice(
73
+ 0,
74
+ 500
75
+ )
58
76
  const regions = (schematic.regions || []).slice(0, 250)
59
77
  const ellipses = (schematic.ellipses || []).slice(0, 500)
60
78
  const arcs = (schematic.arcs || []).slice(0, 1000)
79
+ const beziers = (schematic.beziers || []).slice(0, 500)
80
+ const pies = (schematic.pies || []).slice(0, 500)
81
+ const ieeeSymbols = (schematic.ieeeSymbols || []).slice(0, 500)
61
82
  const directives = (schematic.directives || []).slice(0, 250)
62
83
  const texts = allTexts
63
- const components = schematic.components.slice(0, 180)
84
+ const components = (schematic.components || []).slice(0, 180)
64
85
  const pins = (schematic.pins || []).slice(0, 1000)
65
86
  const ports = (schematic.ports || []).slice(0, 250)
66
87
  const crosses = (schematic.crosses || []).slice(0, 250)
@@ -69,6 +90,18 @@ export class SchematicSvgRenderer {
69
90
  const authoredJunctions = (schematic.junctions || []).slice(0, 500)
70
91
  const busEntries = (schematic.busEntries || []).slice(0, 500)
71
92
  const images = (schematic.images || []).slice(0, 100)
93
+ const semanticContext =
94
+ SchematicSvgRenderer.#buildSemanticContext(schematic)
95
+ const semanticMetadata = SchematicSvgRenderer.#buildSemanticMetadata(
96
+ schematic,
97
+ semanticContext
98
+ )
99
+ const textGeometryMarkup = renderOptions.includeTextGeometrySidecar
100
+ ? SchematicSvgRenderer.#buildTextGeometryMetadataMarkup(
101
+ texts,
102
+ semanticContext
103
+ )
104
+ : ''
72
105
  const drawableComponents = components.filter(
73
106
  (component) =>
74
107
  SchematicSvgRenderer.#isDrawableSchematicComponent(component) &&
@@ -81,7 +114,7 @@ export class SchematicSvgRenderer {
81
114
  width,
82
115
  height,
83
116
  renderedSheet.sheet,
84
- documentModel?.fileName
117
+ renderModel?.fileName
85
118
  )
86
119
  const regionMarkup = SchematicRegionRenderer.buildMarkup(
87
120
  regions,
@@ -110,10 +143,18 @@ export class SchematicSvgRenderer {
110
143
  const ownerlessRectangles = rectangles.filter(
111
144
  (rectangle) => !rectangle.ownerIndex
112
145
  )
146
+ const ownerlessRoundedRectangles = roundedRectangles.filter(
147
+ (rectangle) => !rectangle.ownerIndex
148
+ )
113
149
  const ownerlessEllipses = ellipses.filter(
114
150
  (ellipse) => !ellipse.ownerIndex
115
151
  )
116
152
  const ownerlessArcs = arcs.filter((arc) => !arc.ownerIndex)
153
+ const ownerlessBeziers = beziers.filter((bezier) => !bezier.ownerIndex)
154
+ const ownerlessPies = pies.filter((pie) => !pie.ownerIndex)
155
+ const ownerlessIeeeSymbols = ieeeSymbols.filter(
156
+ (symbol) => !symbol.ownerIndex
157
+ )
117
158
  const resolvedTexts = texts.map((text) =>
118
159
  text.recordType === '17'
119
160
  ? {
@@ -128,40 +169,180 @@ export class SchematicSvgRenderer {
128
169
  : text
129
170
  )
130
171
  const polygonMarkup = ownerlessPolygons
131
- .map((polygon) =>
132
- SchematicShapeRenderer.buildPolygonMarkup(
133
- polygon,
134
- contentHeight
172
+ .map((polygon, index) =>
173
+ SchematicSvgRenderer.#appendSvgAttributes(
174
+ SchematicShapeRenderer.buildPolygonMarkup(
175
+ polygon,
176
+ contentHeight
177
+ ),
178
+ SchematicSvgRenderer.#semanticAttributes(
179
+ 'polygon',
180
+ polygon,
181
+ SchematicSvgRenderer.#primitiveIndex(
182
+ semanticContext,
183
+ 'polygons',
184
+ polygon,
185
+ index
186
+ ),
187
+ semanticContext
188
+ )
135
189
  )
136
190
  )
137
191
  .join('')
138
192
  const rectangleMarkup = ownerlessRectangles
139
- .map((rectangle) =>
140
- SchematicShapeRenderer.buildRectangleMarkup(
141
- rectangle,
142
- contentHeight
193
+ .map((rectangle, index) =>
194
+ SchematicSvgRenderer.#appendSvgAttributes(
195
+ SchematicShapeRenderer.buildRectangleMarkup(
196
+ rectangle,
197
+ contentHeight
198
+ ),
199
+ SchematicSvgRenderer.#semanticAttributes(
200
+ 'rectangle',
201
+ rectangle,
202
+ SchematicSvgRenderer.#primitiveIndex(
203
+ semanticContext,
204
+ 'rectangles',
205
+ rectangle,
206
+ index
207
+ ),
208
+ semanticContext
209
+ )
210
+ )
211
+ )
212
+ .join('')
213
+ const roundedRectangleMarkup = ownerlessRoundedRectangles
214
+ .map((rectangle, index) =>
215
+ SchematicSvgRenderer.#appendSvgAttributes(
216
+ SchematicShapeRenderer.buildRoundedRectangleMarkup(
217
+ rectangle,
218
+ contentHeight
219
+ ),
220
+ SchematicSvgRenderer.#semanticAttributes(
221
+ 'rounded-rectangle',
222
+ rectangle,
223
+ SchematicSvgRenderer.#primitiveIndex(
224
+ semanticContext,
225
+ 'roundedRectangles',
226
+ rectangle,
227
+ index
228
+ ),
229
+ semanticContext
230
+ )
143
231
  )
144
232
  )
145
233
  .join('')
146
234
  const ellipseMarkup = ownerlessEllipses
147
- .map((ellipse) =>
148
- SchematicShapeRenderer.buildEllipseMarkup(
149
- ellipse,
150
- contentHeight
235
+ .map((ellipse, index) =>
236
+ SchematicSvgRenderer.#appendSvgAttributes(
237
+ SchematicShapeRenderer.buildEllipseMarkup(
238
+ ellipse,
239
+ contentHeight
240
+ ),
241
+ SchematicSvgRenderer.#semanticAttributes(
242
+ 'ellipse',
243
+ ellipse,
244
+ SchematicSvgRenderer.#primitiveIndex(
245
+ semanticContext,
246
+ 'ellipses',
247
+ ellipse,
248
+ index
249
+ ),
250
+ semanticContext
251
+ )
151
252
  )
152
253
  )
153
254
  .join('')
154
255
  const lineMarkup = ownerlessLines
155
- .map((line) =>
256
+ .map((line, index) =>
156
257
  SchematicSvgRenderer.#buildSchematicLineMarkup(
157
258
  line,
158
- contentHeight
259
+ contentHeight,
260
+ SchematicSvgRenderer.#primitiveIndex(
261
+ semanticContext,
262
+ 'lines',
263
+ line,
264
+ index
265
+ ),
266
+ semanticContext
159
267
  )
160
268
  )
161
269
  .join('')
162
270
  const arcMarkup = ownerlessArcs
163
- .map((arc) =>
164
- SchematicShapeRenderer.buildArcMarkup(arc, contentHeight)
271
+ .map((arc, index) =>
272
+ SchematicSvgRenderer.#appendSvgAttributes(
273
+ SchematicShapeRenderer.buildArcMarkup(arc, contentHeight),
274
+ SchematicSvgRenderer.#semanticAttributes(
275
+ 'arc',
276
+ arc,
277
+ SchematicSvgRenderer.#primitiveIndex(
278
+ semanticContext,
279
+ 'arcs',
280
+ arc,
281
+ index
282
+ ),
283
+ semanticContext
284
+ )
285
+ )
286
+ )
287
+ .join('')
288
+ const bezierMarkup = ownerlessBeziers
289
+ .map((bezier, index) =>
290
+ SchematicSvgRenderer.#appendSvgAttributes(
291
+ SchematicShapeRenderer.buildBezierMarkup(
292
+ bezier,
293
+ contentHeight
294
+ ),
295
+ SchematicSvgRenderer.#semanticAttributes(
296
+ 'bezier',
297
+ bezier,
298
+ SchematicSvgRenderer.#primitiveIndex(
299
+ semanticContext,
300
+ 'beziers',
301
+ bezier,
302
+ index
303
+ ),
304
+ semanticContext
305
+ )
306
+ )
307
+ )
308
+ .join('')
309
+ const pieMarkup = ownerlessPies
310
+ .map((pie, index) =>
311
+ SchematicSvgRenderer.#appendSvgAttributes(
312
+ SchematicShapeRenderer.buildPieMarkup(pie, contentHeight),
313
+ SchematicSvgRenderer.#semanticAttributes(
314
+ 'pie',
315
+ pie,
316
+ SchematicSvgRenderer.#primitiveIndex(
317
+ semanticContext,
318
+ 'pies',
319
+ pie,
320
+ index
321
+ ),
322
+ semanticContext
323
+ )
324
+ )
325
+ )
326
+ .join('')
327
+ const ieeeSymbolMarkup = ownerlessIeeeSymbols
328
+ .map((symbol, index) =>
329
+ SchematicSvgRenderer.#appendSvgAttributes(
330
+ SchematicShapeRenderer.buildIeeeSymbolMarkup(
331
+ symbol,
332
+ contentHeight
333
+ ),
334
+ SchematicSvgRenderer.#semanticAttributes(
335
+ 'ieee-symbol',
336
+ symbol,
337
+ SchematicSvgRenderer.#primitiveIndex(
338
+ semanticContext,
339
+ 'ieeeSymbols',
340
+ symbol,
341
+ index
342
+ ),
343
+ semanticContext
344
+ )
345
+ )
165
346
  )
166
347
  .join('')
167
348
  const ownerGeometryMarkup =
@@ -169,9 +350,14 @@ export class SchematicSvgRenderer {
169
350
  lines,
170
351
  polygons,
171
352
  rectangles,
353
+ roundedRectangles,
172
354
  ellipses,
173
355
  arcs,
174
- contentHeight
356
+ beziers,
357
+ pies,
358
+ ieeeSymbols,
359
+ contentHeight,
360
+ semanticContext
175
361
  )
176
362
  const sheetSymbolMarkup =
177
363
  SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
@@ -208,24 +394,40 @@ export class SchematicSvgRenderer {
208
394
  images,
209
395
  contentHeight
210
396
  )
397
+ const markerDefsMarkup =
398
+ SchematicSvgRenderer.#buildSchematicLineMarkerDefs(lines)
211
399
 
212
400
  const textMarkup = resolvedTexts
213
- .map((text) =>
401
+ .map((text, index) =>
214
402
  SchematicSvgRenderer.#buildSchematicTextMarkup(
215
403
  text,
216
404
  contentHeight,
217
405
  lines,
218
- pins
406
+ pins,
407
+ SchematicSvgRenderer.#primitiveIndex(
408
+ semanticContext,
409
+ 'texts',
410
+ text,
411
+ index
412
+ ),
413
+ semanticContext
219
414
  )
220
415
  )
221
416
  .join('')
222
417
 
223
418
  const componentMarkup = drawableComponents
224
- .map((component) =>
419
+ .map((component, index) =>
225
420
  SchematicSvgRenderer.#buildFallbackComponentMarkup(
226
421
  component,
227
422
  contentHeight,
228
- renderedSheet.contentSheet
423
+ renderedSheet.contentSheet,
424
+ SchematicSvgRenderer.#primitiveIndex(
425
+ semanticContext,
426
+ 'components',
427
+ component,
428
+ index
429
+ ),
430
+ semanticContext
229
431
  )
230
432
  )
231
433
  .join('')
@@ -240,14 +442,27 @@ export class SchematicSvgRenderer {
240
442
  pins
241
443
  )
242
444
  const pinMarkup = pins
243
- .map((pin) =>
244
- SchematicPinSvgRenderer.buildMarkup(
245
- pin,
246
- contentHeight,
247
- renderedSheet.contentSheet,
248
- rotatedVerticalNumberOwners,
249
- explicitOwnerPinNameLabels,
250
- explicitOwnerPinLabelOffsets
445
+ .map((pin, index) =>
446
+ SchematicSvgRenderer.#appendSvgAttributes(
447
+ SchematicPinSvgRenderer.buildMarkup(
448
+ pin,
449
+ contentHeight,
450
+ renderedSheet.contentSheet,
451
+ rotatedVerticalNumberOwners,
452
+ explicitOwnerPinNameLabels,
453
+ explicitOwnerPinLabelOffsets
454
+ ),
455
+ SchematicSvgRenderer.#semanticAttributes(
456
+ 'pin',
457
+ pin,
458
+ SchematicSvgRenderer.#primitiveIndex(
459
+ semanticContext,
460
+ 'pins',
461
+ pin,
462
+ index
463
+ ),
464
+ semanticContext
465
+ )
251
466
  )
252
467
  )
253
468
  .join('')
@@ -256,11 +471,28 @@ export class SchematicSvgRenderer {
256
471
  contentHeight,
257
472
  renderedSheet.contentSheet
258
473
  )
259
- const directiveMarkup = SchematicDirectiveRenderer.buildMarkup(
260
- directives,
261
- contentHeight,
262
- renderedSheet.contentSheet
263
- )
474
+ const directiveMarkup = directives
475
+ .map((directive, index) =>
476
+ SchematicSvgRenderer.#appendSvgAttributes(
477
+ SchematicDirectiveRenderer.buildMarkup(
478
+ [directive],
479
+ contentHeight,
480
+ renderedSheet.contentSheet
481
+ ),
482
+ SchematicSvgRenderer.#semanticAttributes(
483
+ 'directive',
484
+ directive,
485
+ SchematicSvgRenderer.#primitiveIndex(
486
+ semanticContext,
487
+ 'directives',
488
+ directive,
489
+ index
490
+ ),
491
+ semanticContext
492
+ )
493
+ )
494
+ )
495
+ .join('')
264
496
  const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
265
497
  lines,
266
498
  crosses,
@@ -281,23 +513,38 @@ export class SchematicSvgRenderer {
281
513
  return (
282
514
  '<section class="svg-panel">' +
283
515
  '<header class="svg-panel__header"><h3>' +
284
- escapeHtml(documentModel?.summary?.title || 'Schematic') +
516
+ escapeHtml(renderModel?.summary?.title || 'Schematic') +
285
517
  '</h3><p>' +
286
518
  lines.length +
287
519
  ' line segments, ' +
288
520
  components.length +
289
521
  ' components</p></header>' +
290
- '<svg class="schematic-svg" viewBox="0 0 ' +
291
- formatNumber(width) +
292
- ' ' +
293
- formatNumber(height) +
294
- '" preserveAspectRatio="xMidYMid meet" aria-label="Schematic view">' +
522
+ '<svg class="schematic-svg"' +
523
+ SchematicSvgRenderer.#renderRootViewBoxAttributes(
524
+ renderOptions,
525
+ '0 0 ' + formatNumber(width) + ' ' + formatNumber(height)
526
+ ) +
527
+ ' preserveAspectRatio="xMidYMid meet" aria-label="Schematic view" data-semantic-schema="' +
528
+ SchematicSvgRenderer.#SEMANTIC_SCHEMA +
529
+ '"' +
530
+ SchematicSvgRenderer.#renderDataAttributes({
531
+ 'data-doc-id': renderOptions.documentId,
532
+ 'data-doc-ver': renderOptions.documentVersion
533
+ }) +
534
+ '>' +
295
535
  '<rect class="sheet-backdrop" x="0" y="0" width="' +
296
536
  formatNumber(width) +
297
537
  '" height="' +
298
538
  formatNumber(height) +
299
539
  '" rx="18" />' +
300
540
  contentClipMarkup +
541
+ '<metadata id="schematic-semantic-metadata" data-schema="' +
542
+ SchematicSvgRenderer.#SEMANTIC_SCHEMA +
543
+ '">' +
544
+ escapeHtml(JSON.stringify(semanticMetadata)) +
545
+ '</metadata>' +
546
+ textGeometryMarkup +
547
+ markerDefsMarkup +
301
548
  '<g class="schematic-content"' +
302
549
  ' clip-path="url(#' +
303
550
  escapeHtml(contentClipId) +
@@ -309,6 +556,7 @@ export class SchematicSvgRenderer {
309
556
  '</g>' +
310
557
  '<g class="schematic-rectangles">' +
311
558
  rectangleMarkup +
559
+ roundedRectangleMarkup +
312
560
  '</g>' +
313
561
  '<g class="schematic-ellipses">' +
314
562
  ellipseMarkup +
@@ -319,6 +567,15 @@ export class SchematicSvgRenderer {
319
567
  '<g class="schematic-arcs" stroke-linecap="round">' +
320
568
  arcMarkup +
321
569
  '</g>' +
570
+ '<g class="schematic-beziers" stroke-linecap="round">' +
571
+ bezierMarkup +
572
+ '</g>' +
573
+ '<g class="schematic-pies">' +
574
+ pieMarkup +
575
+ '</g>' +
576
+ '<g class="schematic-ieee-symbols">' +
577
+ ieeeSymbolMarkup +
578
+ '</g>' +
322
579
  '<g class="schematic-owner-geometry" stroke-linecap="round">' +
323
580
  ownerGeometryMarkup +
324
581
  '</g>' +
@@ -365,6 +622,824 @@ export class SchematicSvgRenderer {
365
622
  )
366
623
  }
367
624
 
625
+ /**
626
+ * Normalizes schematic SVG export options.
627
+ * @param {Record<string, unknown>} options Raw render options.
628
+ * @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean }}
629
+ */
630
+ static #normalizeRenderOptions(options) {
631
+ const includeViewBox =
632
+ options?.includeViewBox ?? options?.include_view_box
633
+
634
+ return {
635
+ includeViewBox: includeViewBox === false ? false : true,
636
+ documentId: String(options?.documentId || options?.docId || ''),
637
+ documentVersion: String(
638
+ options?.documentVersion || options?.documentVer || ''
639
+ ),
640
+ includeTextGeometrySidecar:
641
+ options?.includeTextGeometrySidecar === true ||
642
+ options?.textGeometry === 'sidecar'
643
+ }
644
+ }
645
+
646
+ /**
647
+ * Renders root SVG viewBox attributes according to export options.
648
+ * @param {{ includeViewBox: boolean }} options Normalized options.
649
+ * @param {string} viewBox ViewBox value.
650
+ * @returns {string}
651
+ */
652
+ static #renderRootViewBoxAttributes(options, viewBox) {
653
+ return options.includeViewBox
654
+ ? ' viewBox="' + escapeHtml(viewBox) + '"'
655
+ : ''
656
+ }
657
+
658
+ /**
659
+ * Builds optional text geometry metadata markup.
660
+ * @param {object[]} texts Text rows.
661
+ * @param {object} semanticContext Semantic context.
662
+ * @returns {string}
663
+ */
664
+ static #buildTextGeometryMetadataMarkup(texts, semanticContext) {
665
+ const metadata = TextGeometrySidecarBuilder.buildSchematic(
666
+ texts,
667
+ semanticContext.primitiveIndexes?.texts
668
+ )
669
+
670
+ return (
671
+ '<metadata id="schematic-text-geometry" data-schema="' +
672
+ TextGeometrySidecarBuilder.SCHEMA_ID +
673
+ '">' +
674
+ escapeHtml(JSON.stringify(metadata)) +
675
+ '</metadata>'
676
+ )
677
+ }
678
+
679
+ /**
680
+ * Builds reusable SVG marker definitions for authored line endpoints.
681
+ * @param {{ startMarker?: object, endMarker?: object }[]} lines Drawable lines.
682
+ * @returns {string}
683
+ */
684
+ static #buildSchematicLineMarkerDefs(lines) {
685
+ const markers = SchematicSvgRenderer.#collectSchematicLineMarkers(lines)
686
+
687
+ if (!markers.length) {
688
+ return ''
689
+ }
690
+
691
+ return (
692
+ '<defs class="schematic-line-marker-defs">' +
693
+ markers
694
+ .map((marker) =>
695
+ SchematicSvgRenderer.#buildSchematicLineMarkerDef(marker)
696
+ )
697
+ .join('') +
698
+ '</defs>'
699
+ )
700
+ }
701
+
702
+ /**
703
+ * Collects unique endpoint markers in stable order.
704
+ * @param {{ startMarker?: object, endMarker?: object }[]} lines Drawable lines.
705
+ * @returns {object[]}
706
+ */
707
+ static #collectSchematicLineMarkers(lines) {
708
+ const seen = new Set()
709
+ const markers = []
710
+
711
+ for (const line of lines || []) {
712
+ for (const marker of [line.startMarker, line.endMarker]) {
713
+ if (!marker) {
714
+ continue
715
+ }
716
+
717
+ const id = SchematicSvgRenderer.#schematicLineMarkerId(marker)
718
+ if (seen.has(id)) {
719
+ continue
720
+ }
721
+
722
+ seen.add(id)
723
+ markers.push(marker)
724
+ }
725
+ }
726
+
727
+ return markers
728
+ }
729
+
730
+ /**
731
+ * Builds one SVG marker definition.
732
+ * @param {{ shapeName?: string, size?: number }} marker Marker metadata.
733
+ * @returns {string}
734
+ */
735
+ static #buildSchematicLineMarkerDef(marker) {
736
+ const id = SchematicSvgRenderer.#schematicLineMarkerId(marker)
737
+ const shapeName = String(marker?.shapeName || '')
738
+ const fill =
739
+ shapeName === 'filled-arrow' || shapeName === 'square'
740
+ ? 'context-stroke'
741
+ : 'none'
742
+ const shape =
743
+ shapeName === 'circle'
744
+ ? '<circle cx="5" cy="5" r="3.2" fill="none" stroke="context-stroke" stroke-width="1.4" />'
745
+ : shapeName === 'square'
746
+ ? '<rect x="2" y="2" width="6" height="6" fill="context-stroke" stroke="context-stroke" stroke-width="1" />'
747
+ : '<path d="M 1 1 L 9 5 L 1 9" fill="' +
748
+ fill +
749
+ '" stroke="context-stroke" stroke-width="1.4" stroke-linejoin="round" />'
750
+
751
+ return (
752
+ '<marker id="' +
753
+ escapeHtml(id) +
754
+ '" viewBox="0 0 10 10" markerWidth="' +
755
+ formatNumber(Math.max(Number(marker?.size || 6), 1)) +
756
+ '" markerHeight="' +
757
+ formatNumber(Math.max(Number(marker?.size || 6), 1)) +
758
+ '" refX="5" refY="5" orient="auto-start-reverse">' +
759
+ shape +
760
+ '</marker>'
761
+ )
762
+ }
763
+
764
+ /**
765
+ * Builds a deterministic marker id from normalized marker metadata.
766
+ * @param {{ shapeName?: string, size?: number }} marker Marker metadata.
767
+ * @returns {string}
768
+ */
769
+ static #schematicLineMarkerId(marker) {
770
+ return (
771
+ 'schematic-marker-' +
772
+ String(marker?.shapeName || 'marker')
773
+ .trim()
774
+ .toLowerCase()
775
+ .replace(/[^a-z0-9]+/gu, '-') +
776
+ '-' +
777
+ formatNumber(Math.max(Number(marker?.size || 6), 1)).replace(
778
+ /\./gu,
779
+ '-'
780
+ )
781
+ )
782
+ }
783
+
784
+ /**
785
+ * Builds reusable semantic lookup data for one schematic render.
786
+ * @param {object} schematic Normalized schematic model.
787
+ * @returns {object}
788
+ */
789
+ static #buildSemanticContext(schematic) {
790
+ const components = schematic?.components || []
791
+ const componentsByOwnerIndex = new Map()
792
+ const componentsByDesignator = new Map()
793
+
794
+ for (const component of components) {
795
+ const ownerKey = String(component?.ownerIndex || '').trim()
796
+ if (ownerKey) {
797
+ componentsByOwnerIndex.set(ownerKey, component)
798
+ }
799
+ if (component?.designator) {
800
+ componentsByDesignator.set(component.designator, component)
801
+ }
802
+ }
803
+
804
+ const netByPrimitive = new Map()
805
+ for (const net of schematic?.nets || []) {
806
+ for (const segment of net.segments || []) {
807
+ netByPrimitive.set(segment, net)
808
+ }
809
+ for (const label of net.labels || []) {
810
+ netByPrimitive.set(label, net)
811
+ }
812
+ for (const powerPort of net.powerPorts || []) {
813
+ netByPrimitive.set(powerPort, net)
814
+ }
815
+ for (const pin of net.pins || []) {
816
+ netByPrimitive.set(pin, net)
817
+ }
818
+ for (const port of net.ports || []) {
819
+ netByPrimitive.set(port, net)
820
+ }
821
+ for (const sheetEntry of net.sheetEntries || []) {
822
+ netByPrimitive.set(sheetEntry, net)
823
+ }
824
+ }
825
+
826
+ return {
827
+ componentsByOwnerIndex,
828
+ componentsByDesignator,
829
+ netByPrimitive,
830
+ primitiveIndexes: {
831
+ lines: SchematicSvgRenderer.#objectIndexMap(
832
+ schematic?.lines || []
833
+ ),
834
+ polygons: SchematicSvgRenderer.#objectIndexMap(
835
+ schematic?.polygons || []
836
+ ),
837
+ rectangles: SchematicSvgRenderer.#objectIndexMap(
838
+ schematic?.rectangles || []
839
+ ),
840
+ roundedRectangles: SchematicSvgRenderer.#objectIndexMap(
841
+ schematic?.roundedRectangles || []
842
+ ),
843
+ ellipses: SchematicSvgRenderer.#objectIndexMap(
844
+ schematic?.ellipses || []
845
+ ),
846
+ arcs: SchematicSvgRenderer.#objectIndexMap(
847
+ schematic?.arcs || []
848
+ ),
849
+ beziers: SchematicSvgRenderer.#objectIndexMap(
850
+ schematic?.beziers || []
851
+ ),
852
+ pies: SchematicSvgRenderer.#objectIndexMap(
853
+ schematic?.pies || []
854
+ ),
855
+ ieeeSymbols: SchematicSvgRenderer.#objectIndexMap(
856
+ schematic?.ieeeSymbols || []
857
+ ),
858
+ directives: SchematicSvgRenderer.#objectIndexMap(
859
+ schematic?.directives || []
860
+ ),
861
+ texts: SchematicSvgRenderer.#objectIndexMap(
862
+ schematic?.texts || []
863
+ ),
864
+ pins: SchematicSvgRenderer.#objectIndexMap(
865
+ schematic?.pins || []
866
+ ),
867
+ ports: SchematicSvgRenderer.#objectIndexMap(
868
+ schematic?.ports || []
869
+ ),
870
+ components: SchematicSvgRenderer.#objectIndexMap(components)
871
+ }
872
+ }
873
+ }
874
+
875
+ /**
876
+ * Builds a compact JSON sidecar describing schematic SVG semantic links.
877
+ * @param {object} schematic Normalized schematic model.
878
+ * @param {object} semanticContext Semantic lookup context.
879
+ * @returns {{ schema: string, nets: object[], components: object[] }}
880
+ */
881
+ static #buildSemanticMetadata(schematic, semanticContext) {
882
+ return {
883
+ schema: SchematicSvgRenderer.#SEMANTIC_SCHEMA,
884
+ elements: SchematicSvgRenderer.#buildElementMetadata(
885
+ schematic,
886
+ semanticContext
887
+ ),
888
+ nets: (schematic?.nets || []).map((net) =>
889
+ SchematicSvgRenderer.#buildNetMetadata(net, semanticContext)
890
+ ),
891
+ components: (schematic?.components || []).map((component) =>
892
+ SchematicSvgRenderer.#buildComponentMetadata(
893
+ component,
894
+ schematic,
895
+ semanticContext
896
+ )
897
+ )
898
+ }
899
+ }
900
+
901
+ /**
902
+ * Builds one flat element sidecar for every source-addressable schematic
903
+ * primitive family that can participate in SVG review tooling.
904
+ * @param {object} schematic Normalized schematic model.
905
+ * @param {object} semanticContext Semantic lookup context.
906
+ * @returns {object[]}
907
+ */
908
+ static #buildElementMetadata(schematic, semanticContext) {
909
+ return [
910
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
911
+ schematic?.lines || [],
912
+ 'lines',
913
+ 'line',
914
+ semanticContext
915
+ ),
916
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
917
+ schematic?.polygons || [],
918
+ 'polygons',
919
+ 'polygon',
920
+ semanticContext
921
+ ),
922
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
923
+ schematic?.rectangles || [],
924
+ 'rectangles',
925
+ 'rectangle',
926
+ semanticContext
927
+ ),
928
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
929
+ schematic?.roundedRectangles || [],
930
+ 'roundedRectangles',
931
+ 'rounded-rectangle',
932
+ semanticContext
933
+ ),
934
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
935
+ schematic?.ellipses || [],
936
+ 'ellipses',
937
+ 'ellipse',
938
+ semanticContext
939
+ ),
940
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
941
+ schematic?.arcs || [],
942
+ 'arcs',
943
+ 'arc',
944
+ semanticContext
945
+ ),
946
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
947
+ schematic?.beziers || [],
948
+ 'beziers',
949
+ 'bezier',
950
+ semanticContext
951
+ ),
952
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
953
+ schematic?.pies || [],
954
+ 'pies',
955
+ 'pie',
956
+ semanticContext
957
+ ),
958
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
959
+ schematic?.ieeeSymbols || [],
960
+ 'ieeeSymbols',
961
+ 'ieee-symbol',
962
+ semanticContext
963
+ ),
964
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
965
+ schematic?.texts || [],
966
+ 'texts',
967
+ 'text',
968
+ semanticContext
969
+ ),
970
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
971
+ schematic?.components || [],
972
+ 'components',
973
+ 'component',
974
+ semanticContext
975
+ ),
976
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
977
+ schematic?.pins || [],
978
+ 'pins',
979
+ 'pin',
980
+ semanticContext
981
+ ),
982
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
983
+ schematic?.ports || [],
984
+ 'ports',
985
+ 'port',
986
+ semanticContext
987
+ ),
988
+ ...SchematicSvgRenderer.#elementMetadataForCollection(
989
+ schematic?.directives || [],
990
+ 'directives',
991
+ 'directive',
992
+ semanticContext
993
+ )
994
+ ]
995
+ }
996
+
997
+ /**
998
+ * Builds semantic metadata entries for one primitive collection.
999
+ * @param {object[]} records Primitive records.
1000
+ * @param {string} collectionKey Collection key.
1001
+ * @param {string} primitiveKind Primitive kind.
1002
+ * @param {object} semanticContext Semantic lookup context.
1003
+ * @returns {object[]}
1004
+ */
1005
+ static #elementMetadataForCollection(
1006
+ records,
1007
+ collectionKey,
1008
+ primitiveKind,
1009
+ semanticContext
1010
+ ) {
1011
+ return (records || []).map((record, fallbackIndex) => {
1012
+ const index = SchematicSvgRenderer.#primitiveIndex(
1013
+ semanticContext,
1014
+ collectionKey,
1015
+ record,
1016
+ fallbackIndex
1017
+ )
1018
+ const component =
1019
+ SchematicSvgRenderer.#componentForSchematicPrimitive(
1020
+ primitiveKind,
1021
+ record,
1022
+ semanticContext
1023
+ )
1024
+ const net = semanticContext.netByPrimitive.get(record)
1025
+
1026
+ return SchematicSvgRenderer.#stripEmptySemanticObject({
1027
+ elementKey:
1028
+ 'schematic-' +
1029
+ SchematicSvgRenderer.#elementKeyPrimitiveKind(
1030
+ primitiveKind,
1031
+ record
1032
+ ) +
1033
+ '-' +
1034
+ index,
1035
+ primitive: SchematicSvgRenderer.#metadataPrimitiveKind(
1036
+ primitiveKind,
1037
+ record
1038
+ ),
1039
+ recordId: SchematicSvgRenderer.#recordId(
1040
+ primitiveKind,
1041
+ record,
1042
+ index
1043
+ ),
1044
+ component: component?.designator,
1045
+ componentUniqueId: component?.uniqueId,
1046
+ net: net?.name,
1047
+ pin:
1048
+ primitiveKind === 'pin'
1049
+ ? SchematicSvgRenderer.#pinLabel(record)
1050
+ : undefined
1051
+ })
1052
+ })
1053
+ }
1054
+
1055
+ /**
1056
+ * Builds metadata for one schematic net.
1057
+ * @param {object} net Net record.
1058
+ * @param {object} semanticContext Semantic lookup context.
1059
+ * @returns {object}
1060
+ */
1061
+ static #buildNetMetadata(net, semanticContext) {
1062
+ const pins = []
1063
+ const components = []
1064
+ const elementKeys = [
1065
+ ...SchematicSvgRenderer.#elementKeysForObjects(
1066
+ net.segments || [],
1067
+ 'lines',
1068
+ 'line',
1069
+ semanticContext
1070
+ ),
1071
+ ...SchematicSvgRenderer.#elementKeysForObjects(
1072
+ [...(net.labels || []), ...(net.powerPorts || [])],
1073
+ 'texts',
1074
+ 'text',
1075
+ semanticContext
1076
+ ),
1077
+ ...SchematicSvgRenderer.#elementKeysForObjects(
1078
+ net.pins || [],
1079
+ 'pins',
1080
+ 'pin',
1081
+ semanticContext
1082
+ ),
1083
+ ...SchematicSvgRenderer.#elementKeysForObjects(
1084
+ net.ports || [],
1085
+ 'ports',
1086
+ 'port',
1087
+ semanticContext
1088
+ )
1089
+ ]
1090
+
1091
+ for (const pin of net.pins || []) {
1092
+ const component =
1093
+ SchematicSvgRenderer.#componentForSchematicPrimitive(
1094
+ 'pin',
1095
+ pin,
1096
+ semanticContext
1097
+ )
1098
+ if (component?.designator) {
1099
+ components.push(component.designator)
1100
+ pins.push(
1101
+ component.designator +
1102
+ ':' +
1103
+ SchematicSvgRenderer.#pinLabel(pin)
1104
+ )
1105
+ continue
1106
+ }
1107
+ const pinLabel = SchematicSvgRenderer.#pinLabel(pin)
1108
+ if (pinLabel) {
1109
+ pins.push(pinLabel)
1110
+ }
1111
+ }
1112
+
1113
+ return SchematicSvgRenderer.#stripEmptySemanticObject({
1114
+ name: net.name,
1115
+ elementKeys: SchematicSvgRenderer.#dedupe(elementKeys),
1116
+ components: SchematicSvgRenderer.#dedupe(components),
1117
+ pins: SchematicSvgRenderer.#dedupe(pins)
1118
+ })
1119
+ }
1120
+
1121
+ /**
1122
+ * Builds metadata for one schematic component.
1123
+ * @param {object} component Component record.
1124
+ * @param {object} schematic Normalized schematic model.
1125
+ * @param {object} semanticContext Semantic lookup context.
1126
+ * @returns {object}
1127
+ */
1128
+ static #buildComponentMetadata(component, schematic, semanticContext) {
1129
+ const componentIndex = SchematicSvgRenderer.#primitiveIndex(
1130
+ semanticContext,
1131
+ 'components',
1132
+ component,
1133
+ 0
1134
+ )
1135
+ const pins = (schematic?.pins || []).filter(
1136
+ (pin) =>
1137
+ SchematicSvgRenderer.#componentForSchematicPrimitive(
1138
+ 'pin',
1139
+ pin,
1140
+ semanticContext
1141
+ ) === component
1142
+ )
1143
+ const nets = pins
1144
+ .map((pin) => semanticContext.netByPrimitive.get(pin)?.name)
1145
+ .filter(Boolean)
1146
+ const pinLabels = pins
1147
+ .map((pin) => SchematicSvgRenderer.#pinLabel(pin))
1148
+ .filter(Boolean)
1149
+ const elementKeys = [
1150
+ 'schematic-component-' + componentIndex,
1151
+ ...SchematicSvgRenderer.#elementKeysForObjects(
1152
+ pins,
1153
+ 'pins',
1154
+ 'pin',
1155
+ semanticContext
1156
+ )
1157
+ ]
1158
+
1159
+ return SchematicSvgRenderer.#stripEmptySemanticObject({
1160
+ designator: component.designator,
1161
+ uniqueId: component.uniqueId,
1162
+ elementKeys: SchematicSvgRenderer.#dedupe(elementKeys),
1163
+ pins: SchematicSvgRenderer.#dedupe(pinLabels),
1164
+ nets: SchematicSvgRenderer.#dedupe(nets)
1165
+ })
1166
+ }
1167
+
1168
+ /**
1169
+ * Builds SVG data attributes for one schematic primitive.
1170
+ * @param {string} primitiveKind Public primitive kind.
1171
+ * @param {object} primitive Primitive record.
1172
+ * @param {number} index Stable primitive index.
1173
+ * @param {object | undefined} semanticContext Semantic lookup context.
1174
+ * @returns {string}
1175
+ */
1176
+ static #semanticAttributes(
1177
+ primitiveKind,
1178
+ primitive,
1179
+ index,
1180
+ semanticContext
1181
+ ) {
1182
+ if (!semanticContext) {
1183
+ return ''
1184
+ }
1185
+
1186
+ if (!SchematicSvgRenderer.#hasExplicitRecordId(primitive)) {
1187
+ return ''
1188
+ }
1189
+
1190
+ const component = SchematicSvgRenderer.#componentForSchematicPrimitive(
1191
+ primitiveKind,
1192
+ primitive,
1193
+ semanticContext
1194
+ )
1195
+ const net = semanticContext.netByPrimitive.get(primitive)
1196
+
1197
+ return SchematicSvgRenderer.#renderDataAttributes({
1198
+ 'data-primitive': primitiveKind,
1199
+ 'data-element-key': 'schematic-' + primitiveKind + '-' + index,
1200
+ 'data-record-id': SchematicSvgRenderer.#recordId(
1201
+ primitiveKind,
1202
+ primitive,
1203
+ index
1204
+ ),
1205
+ 'data-component': component?.designator,
1206
+ 'data-component-unique-id': component?.uniqueId,
1207
+ 'data-net': net?.name,
1208
+ 'data-pin':
1209
+ primitiveKind === 'pin'
1210
+ ? SchematicSvgRenderer.#pinLabel(primitive)
1211
+ : undefined
1212
+ })
1213
+ }
1214
+
1215
+ /**
1216
+ * Finds the component associated with a schematic primitive.
1217
+ * @param {string} primitiveKind Public primitive kind.
1218
+ * @param {object} primitive Primitive record.
1219
+ * @param {object} semanticContext Semantic lookup context.
1220
+ * @returns {object | null}
1221
+ */
1222
+ static #componentForSchematicPrimitive(
1223
+ primitiveKind,
1224
+ primitive,
1225
+ semanticContext
1226
+ ) {
1227
+ if (primitiveKind === 'component') {
1228
+ return primitive
1229
+ }
1230
+
1231
+ const explicitDesignator =
1232
+ primitive?.componentDesignator || primitive?.component || ''
1233
+ if (explicitDesignator) {
1234
+ return (
1235
+ semanticContext.componentsByDesignator.get(
1236
+ explicitDesignator
1237
+ ) || null
1238
+ )
1239
+ }
1240
+
1241
+ const ownerIndex = String(primitive?.ownerIndex || '').trim()
1242
+ return ownerIndex
1243
+ ? semanticContext.componentsByOwnerIndex.get(ownerIndex) || null
1244
+ : null
1245
+ }
1246
+
1247
+ /**
1248
+ * Returns element keys for a primitive object list.
1249
+ * @param {object[]} records Primitive records.
1250
+ * @param {string} collectionKey Primitive collection key.
1251
+ * @param {string} primitiveKind Public primitive kind.
1252
+ * @param {object} semanticContext Semantic lookup context.
1253
+ * @returns {string[]}
1254
+ */
1255
+ static #elementKeysForObjects(
1256
+ records,
1257
+ collectionKey,
1258
+ primitiveKind,
1259
+ semanticContext
1260
+ ) {
1261
+ return (records || [])
1262
+ .map((record) => {
1263
+ const index =
1264
+ semanticContext.primitiveIndexes?.[collectionKey]?.get(
1265
+ record
1266
+ )
1267
+ return Number.isInteger(index)
1268
+ ? 'schematic-' + primitiveKind + '-' + index
1269
+ : ''
1270
+ })
1271
+ .filter(Boolean)
1272
+ }
1273
+
1274
+ /**
1275
+ * Returns a stable primitive index from the original schematic collection.
1276
+ * @param {object} semanticContext Semantic lookup context.
1277
+ * @param {string} collectionKey Primitive collection key.
1278
+ * @param {object} primitive Primitive record.
1279
+ * @param {number} fallbackIndex Rendered fallback index.
1280
+ * @returns {number}
1281
+ */
1282
+ static #primitiveIndex(
1283
+ semanticContext,
1284
+ collectionKey,
1285
+ primitive,
1286
+ fallbackIndex
1287
+ ) {
1288
+ const resolved =
1289
+ semanticContext.primitiveIndexes?.[collectionKey]?.get(primitive)
1290
+
1291
+ return Number.isInteger(resolved) ? resolved : fallbackIndex
1292
+ }
1293
+
1294
+ /**
1295
+ * Builds an object identity map for stable primitive indexes.
1296
+ * @param {object[]} records Primitive records.
1297
+ * @returns {Map<object, number>}
1298
+ */
1299
+ static #objectIndexMap(records) {
1300
+ return new Map((records || []).map((record, index) => [record, index]))
1301
+ }
1302
+
1303
+ /**
1304
+ * Inserts generated attributes into the first SVG element in a markup
1305
+ * fragment.
1306
+ * @param {string} markup SVG markup.
1307
+ * @param {string} attributes Rendered attributes.
1308
+ * @returns {string}
1309
+ */
1310
+ static #appendSvgAttributes(markup, attributes) {
1311
+ if (!markup || !attributes) {
1312
+ return markup || ''
1313
+ }
1314
+
1315
+ return String(markup).replace(/(\s*\/?>)/u, attributes + '$1')
1316
+ }
1317
+
1318
+ /**
1319
+ * Renders a dictionary as SVG data attributes.
1320
+ * @param {Record<string, unknown>} attributes Attribute dictionary.
1321
+ * @returns {string}
1322
+ */
1323
+ static #renderDataAttributes(attributes) {
1324
+ return Object.entries(attributes || {})
1325
+ .filter(([, value]) => {
1326
+ if (Array.isArray(value)) {
1327
+ return value.length > 0
1328
+ }
1329
+ return value !== null && value !== undefined && value !== ''
1330
+ })
1331
+ .map(([name, value]) => {
1332
+ const renderedValue = Array.isArray(value)
1333
+ ? value.join(',')
1334
+ : String(value)
1335
+ return ' ' + name + '="' + escapeHtml(renderedValue) + '"'
1336
+ })
1337
+ .join('')
1338
+ }
1339
+
1340
+ /**
1341
+ * Returns a stable source record id when present, else a renderer key.
1342
+ * @param {string} primitiveKind Public primitive kind.
1343
+ * @param {object} primitive Primitive record.
1344
+ * @param {number} index Stable primitive index.
1345
+ * @returns {string}
1346
+ */
1347
+ static #recordId(primitiveKind, primitive, index) {
1348
+ const candidate =
1349
+ primitive?.recordId ??
1350
+ primitive?.sourceRecordId ??
1351
+ primitive?.sourceRecordIndex
1352
+
1353
+ return candidate === null || candidate === undefined || candidate === ''
1354
+ ? 'schematic-' + primitiveKind + '-' + index
1355
+ : String(candidate)
1356
+ }
1357
+
1358
+ /**
1359
+ * Resolves the primitive token used by SVG element keys.
1360
+ * @param {string} primitiveKind Public primitive kind.
1361
+ * @param {object} primitive Primitive record.
1362
+ * @returns {string}
1363
+ */
1364
+ static #elementKeyPrimitiveKind(primitiveKind, primitive) {
1365
+ if (primitiveKind === 'text' && primitive?.recordType === '28') {
1366
+ return 'text'
1367
+ }
1368
+
1369
+ return primitiveKind
1370
+ }
1371
+
1372
+ /**
1373
+ * Resolves the primitive token used by semantic metadata.
1374
+ * @param {string} primitiveKind Public primitive kind.
1375
+ * @param {object} primitive Primitive record.
1376
+ * @returns {string}
1377
+ */
1378
+ static #metadataPrimitiveKind(primitiveKind, primitive) {
1379
+ if (primitiveKind === 'text' && primitive?.recordType === '28') {
1380
+ return 'text-frame'
1381
+ }
1382
+
1383
+ return primitiveKind
1384
+ }
1385
+
1386
+ /**
1387
+ * Returns true when a primitive carries source record identity.
1388
+ * @param {object} primitive Primitive record.
1389
+ * @returns {boolean}
1390
+ */
1391
+ static #hasExplicitRecordId(primitive) {
1392
+ return (
1393
+ (primitive?.recordId !== null &&
1394
+ primitive?.recordId !== undefined &&
1395
+ primitive?.recordId !== '') ||
1396
+ (primitive?.sourceRecordId !== null &&
1397
+ primitive?.sourceRecordId !== undefined &&
1398
+ primitive?.sourceRecordId !== '') ||
1399
+ (primitive?.sourceRecordIndex !== null &&
1400
+ primitive?.sourceRecordIndex !== undefined &&
1401
+ primitive?.sourceRecordIndex !== '')
1402
+ )
1403
+ }
1404
+
1405
+ /**
1406
+ * Returns a displayable pin designator.
1407
+ * @param {object} pin Pin record.
1408
+ * @returns {string}
1409
+ */
1410
+ static #pinLabel(pin) {
1411
+ return String(pin?.designator || pin?.pinNumber || pin?.name || '')
1412
+ }
1413
+
1414
+ /**
1415
+ * Deduplicates values while preserving insertion order.
1416
+ * @param {unknown[]} values Candidate values.
1417
+ * @returns {unknown[]}
1418
+ */
1419
+ static #dedupe(values) {
1420
+ return [...new Set((values || []).filter(Boolean))]
1421
+ }
1422
+
1423
+ /**
1424
+ * Removes empty fields from a semantic metadata object.
1425
+ * @param {Record<string, unknown>} value Metadata object.
1426
+ * @returns {Record<string, unknown>}
1427
+ */
1428
+ static #stripEmptySemanticObject(value) {
1429
+ return Object.fromEntries(
1430
+ Object.entries(value || {}).filter(([, entryValue]) => {
1431
+ if (Array.isArray(entryValue)) {
1432
+ return entryValue.length > 0
1433
+ }
1434
+ return (
1435
+ entryValue !== null &&
1436
+ entryValue !== undefined &&
1437
+ entryValue !== ''
1438
+ )
1439
+ })
1440
+ )
1441
+ }
1442
+
368
1443
  /**
369
1444
  * Resolves the rendered dimensions and sheet metadata used by SVG output.
370
1445
  * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean } | undefined} sheet
@@ -446,22 +1521,32 @@ export class SchematicSvgRenderer {
446
1521
  * @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, renderOrder?: number }[]} lines
447
1522
  * @param {{ points: { x: number, y: number }[], ownerIndex?: string, renderOrder?: number }[]} polygons
448
1523
  * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string, renderOrder?: number }[]} rectangles
1524
+ * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string, renderOrder?: number }[]} roundedRectangles
449
1525
  * @param {{ x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string, renderOrder?: number }[]} ellipses
450
1526
  * @param {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, ownerIndex?: string, renderOrder?: number }[]} arcs
1527
+ * @param {{ segments: object[], ownerIndex?: string, renderOrder?: number }[]} beziers
1528
+ * @param {{ x: number, y: number, radius: number, radiusY?: number, startAngle: number, endAngle: number, ownerIndex?: string, renderOrder?: number }[]} pies
1529
+ * @param {{ x: number, y: number, symbolName?: string, ownerIndex?: string, renderOrder?: number }[]} ieeeSymbols
451
1530
  * @param {number} sheetHeight
1531
+ * @param {object} semanticContext Semantic lookup context.
452
1532
  * @returns {string}
453
1533
  */
454
1534
  static #buildOwnerGeometryMarkup(
455
1535
  lines,
456
1536
  polygons,
457
1537
  rectangles,
1538
+ roundedRectangles,
458
1539
  ellipses,
459
1540
  arcs,
460
- sheetHeight
1541
+ beziers,
1542
+ pies,
1543
+ ieeeSymbols,
1544
+ sheetHeight,
1545
+ semanticContext
461
1546
  ) {
462
1547
  const items = []
463
1548
 
464
- for (const polygon of polygons) {
1549
+ for (const [index, polygon] of polygons.entries()) {
465
1550
  if (!polygon.ownerIndex) {
466
1551
  continue
467
1552
  }
@@ -470,14 +1555,27 @@ export class SchematicSvgRenderer {
470
1555
  renderOrder:
471
1556
  SchematicSvgRenderer.#resolvePrimitiveRenderOrder(polygon),
472
1557
  typeOrder: 0,
473
- markup: SchematicShapeRenderer.buildPolygonMarkup(
474
- polygon,
475
- sheetHeight
1558
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1559
+ SchematicShapeRenderer.buildPolygonMarkup(
1560
+ polygon,
1561
+ sheetHeight
1562
+ ),
1563
+ SchematicSvgRenderer.#semanticAttributes(
1564
+ 'polygon',
1565
+ polygon,
1566
+ SchematicSvgRenderer.#primitiveIndex(
1567
+ semanticContext,
1568
+ 'polygons',
1569
+ polygon,
1570
+ index
1571
+ ),
1572
+ semanticContext
1573
+ )
476
1574
  )
477
1575
  })
478
1576
  }
479
1577
 
480
- for (const rectangle of rectangles) {
1578
+ for (const [index, rectangle] of rectangles.entries()) {
481
1579
  if (!rectangle.ownerIndex) {
482
1580
  continue
483
1581
  }
@@ -488,14 +1586,58 @@ export class SchematicSvgRenderer {
488
1586
  rectangle
489
1587
  ),
490
1588
  typeOrder: 1,
491
- markup: SchematicShapeRenderer.buildRectangleMarkup(
492
- rectangle,
493
- sheetHeight
1589
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1590
+ SchematicShapeRenderer.buildRectangleMarkup(
1591
+ rectangle,
1592
+ sheetHeight
1593
+ ),
1594
+ SchematicSvgRenderer.#semanticAttributes(
1595
+ 'rectangle',
1596
+ rectangle,
1597
+ SchematicSvgRenderer.#primitiveIndex(
1598
+ semanticContext,
1599
+ 'rectangles',
1600
+ rectangle,
1601
+ index
1602
+ ),
1603
+ semanticContext
1604
+ )
1605
+ )
1606
+ })
1607
+ }
1608
+
1609
+ for (const [index, rectangle] of roundedRectangles.entries()) {
1610
+ if (!rectangle.ownerIndex) {
1611
+ continue
1612
+ }
1613
+
1614
+ items.push({
1615
+ renderOrder:
1616
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(
1617
+ rectangle
1618
+ ),
1619
+ typeOrder: 1.5,
1620
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1621
+ SchematicShapeRenderer.buildRoundedRectangleMarkup(
1622
+ rectangle,
1623
+ sheetHeight
1624
+ ),
1625
+ SchematicSvgRenderer.#semanticAttributes(
1626
+ 'rounded-rectangle',
1627
+ rectangle,
1628
+ SchematicSvgRenderer.#primitiveIndex(
1629
+ semanticContext,
1630
+ 'roundedRectangles',
1631
+ rectangle,
1632
+ index
1633
+ ),
1634
+ semanticContext
1635
+ )
494
1636
  )
495
1637
  })
496
1638
  }
497
1639
 
498
- for (const ellipse of ellipses) {
1640
+ for (const [index, ellipse] of ellipses.entries()) {
499
1641
  if (!ellipse.ownerIndex) {
500
1642
  continue
501
1643
  }
@@ -504,14 +1646,27 @@ export class SchematicSvgRenderer {
504
1646
  renderOrder:
505
1647
  SchematicSvgRenderer.#resolvePrimitiveRenderOrder(ellipse),
506
1648
  typeOrder: 2,
507
- markup: SchematicShapeRenderer.buildEllipseMarkup(
508
- ellipse,
509
- sheetHeight
1649
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1650
+ SchematicShapeRenderer.buildEllipseMarkup(
1651
+ ellipse,
1652
+ sheetHeight
1653
+ ),
1654
+ SchematicSvgRenderer.#semanticAttributes(
1655
+ 'ellipse',
1656
+ ellipse,
1657
+ SchematicSvgRenderer.#primitiveIndex(
1658
+ semanticContext,
1659
+ 'ellipses',
1660
+ ellipse,
1661
+ index
1662
+ ),
1663
+ semanticContext
1664
+ )
510
1665
  )
511
1666
  })
512
1667
  }
513
1668
 
514
- for (const line of lines) {
1669
+ for (const [index, line] of lines.entries()) {
515
1670
  if (!line.ownerIndex) {
516
1671
  continue
517
1672
  }
@@ -522,12 +1677,19 @@ export class SchematicSvgRenderer {
522
1677
  typeOrder: 3,
523
1678
  markup: SchematicSvgRenderer.#buildSchematicLineMarkup(
524
1679
  line,
525
- sheetHeight
1680
+ sheetHeight,
1681
+ SchematicSvgRenderer.#primitiveIndex(
1682
+ semanticContext,
1683
+ 'lines',
1684
+ line,
1685
+ index
1686
+ ),
1687
+ semanticContext
526
1688
  )
527
1689
  })
528
1690
  }
529
1691
 
530
- for (const arc of arcs) {
1692
+ for (const [index, arc] of arcs.entries()) {
531
1693
  if (!arc.ownerIndex) {
532
1694
  continue
533
1695
  }
@@ -536,7 +1698,104 @@ export class SchematicSvgRenderer {
536
1698
  renderOrder:
537
1699
  SchematicSvgRenderer.#resolvePrimitiveRenderOrder(arc),
538
1700
  typeOrder: 4,
539
- markup: SchematicShapeRenderer.buildArcMarkup(arc, sheetHeight)
1701
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1702
+ SchematicShapeRenderer.buildArcMarkup(arc, sheetHeight),
1703
+ SchematicSvgRenderer.#semanticAttributes(
1704
+ 'arc',
1705
+ arc,
1706
+ SchematicSvgRenderer.#primitiveIndex(
1707
+ semanticContext,
1708
+ 'arcs',
1709
+ arc,
1710
+ index
1711
+ ),
1712
+ semanticContext
1713
+ )
1714
+ )
1715
+ })
1716
+ }
1717
+
1718
+ for (const [index, bezier] of beziers.entries()) {
1719
+ if (!bezier.ownerIndex) {
1720
+ continue
1721
+ }
1722
+
1723
+ items.push({
1724
+ renderOrder:
1725
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(bezier),
1726
+ typeOrder: 5,
1727
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1728
+ SchematicShapeRenderer.buildBezierMarkup(
1729
+ bezier,
1730
+ sheetHeight
1731
+ ),
1732
+ SchematicSvgRenderer.#semanticAttributes(
1733
+ 'bezier',
1734
+ bezier,
1735
+ SchematicSvgRenderer.#primitiveIndex(
1736
+ semanticContext,
1737
+ 'beziers',
1738
+ bezier,
1739
+ index
1740
+ ),
1741
+ semanticContext
1742
+ )
1743
+ )
1744
+ })
1745
+ }
1746
+
1747
+ for (const [index, pie] of pies.entries()) {
1748
+ if (!pie.ownerIndex) {
1749
+ continue
1750
+ }
1751
+
1752
+ items.push({
1753
+ renderOrder:
1754
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(pie),
1755
+ typeOrder: 6,
1756
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1757
+ SchematicShapeRenderer.buildPieMarkup(pie, sheetHeight),
1758
+ SchematicSvgRenderer.#semanticAttributes(
1759
+ 'pie',
1760
+ pie,
1761
+ SchematicSvgRenderer.#primitiveIndex(
1762
+ semanticContext,
1763
+ 'pies',
1764
+ pie,
1765
+ index
1766
+ ),
1767
+ semanticContext
1768
+ )
1769
+ )
1770
+ })
1771
+ }
1772
+
1773
+ for (const [index, symbol] of ieeeSymbols.entries()) {
1774
+ if (!symbol.ownerIndex) {
1775
+ continue
1776
+ }
1777
+
1778
+ items.push({
1779
+ renderOrder:
1780
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(symbol),
1781
+ typeOrder: 7,
1782
+ markup: SchematicSvgRenderer.#appendSvgAttributes(
1783
+ SchematicShapeRenderer.buildIeeeSymbolMarkup(
1784
+ symbol,
1785
+ sheetHeight
1786
+ ),
1787
+ SchematicSvgRenderer.#semanticAttributes(
1788
+ 'ieee-symbol',
1789
+ symbol,
1790
+ SchematicSvgRenderer.#primitiveIndex(
1791
+ semanticContext,
1792
+ 'ieeeSymbols',
1793
+ symbol,
1794
+ index
1795
+ ),
1796
+ semanticContext
1797
+ )
1798
+ )
540
1799
  })
541
1800
  }
542
1801
 
@@ -575,9 +1834,16 @@ export class SchematicSvgRenderer {
575
1834
  * the source primitive requests them.
576
1835
  * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, recordType?: string }} line
577
1836
  * @param {number} sheetHeight
1837
+ * @param {number} index Stable primitive index.
1838
+ * @param {object | undefined} semanticContext Semantic lookup context.
578
1839
  * @returns {string}
579
1840
  */
580
- static #buildSchematicLineMarkup(line, sheetHeight) {
1841
+ static #buildSchematicLineMarkup(
1842
+ line,
1843
+ sheetHeight,
1844
+ index = 0,
1845
+ semanticContext = undefined
1846
+ ) {
581
1847
  return (
582
1848
  '<line x1="' +
583
1849
  formatNumber(line.x1) +
@@ -600,6 +1866,13 @@ export class SchematicSvgRenderer {
600
1866
  ) +
601
1867
  '"' +
602
1868
  SchematicSvgRenderer.#buildSchematicLineStyleAttributes(line) +
1869
+ SchematicSvgRenderer.#buildSchematicLineMarkerAttributes(line) +
1870
+ SchematicSvgRenderer.#semanticAttributes(
1871
+ 'line',
1872
+ line,
1873
+ index,
1874
+ semanticContext
1875
+ ) +
603
1876
  ' />'
604
1877
  )
605
1878
  }
@@ -643,6 +1916,34 @@ export class SchematicSvgRenderer {
643
1916
  )
644
1917
  }
645
1918
 
1919
+ /**
1920
+ * Builds SVG marker attributes for one schematic line.
1921
+ * @param {{ startMarker?: object, endMarker?: object }} line Line primitive.
1922
+ * @returns {string}
1923
+ */
1924
+ static #buildSchematicLineMarkerAttributes(line) {
1925
+ return (
1926
+ (line.startMarker
1927
+ ? ' marker-start="url(#' +
1928
+ escapeHtml(
1929
+ SchematicSvgRenderer.#schematicLineMarkerId(
1930
+ line.startMarker
1931
+ )
1932
+ ) +
1933
+ ')"'
1934
+ : '') +
1935
+ (line.endMarker
1936
+ ? ' marker-end="url(#' +
1937
+ escapeHtml(
1938
+ SchematicSvgRenderer.#schematicLineMarkerId(
1939
+ line.endMarker
1940
+ )
1941
+ ) +
1942
+ ')"'
1943
+ : '')
1944
+ )
1945
+ }
1946
+
646
1947
  /**
647
1948
  * Builds page border and title-block chrome from sheet metadata.
648
1949
  * @param {number} width
@@ -666,9 +1967,18 @@ export class SchematicSvgRenderer {
666
1967
  * @param {number} sheetHeight
667
1968
  * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
668
1969
  * @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
1970
+ * @param {number} index Stable primitive index.
1971
+ * @param {object | undefined} semanticContext Semantic lookup context.
669
1972
  * @returns {string}
670
1973
  */
671
- static #buildSchematicTextMarkup(text, sheetHeight, lines, pins) {
1974
+ static #buildSchematicTextMarkup(
1975
+ text,
1976
+ sheetHeight,
1977
+ lines,
1978
+ pins,
1979
+ index = 0,
1980
+ semanticContext = undefined
1981
+ ) {
672
1982
  const matchedOwnerPin =
673
1983
  SchematicOwnerPinLabelLayout.findExplicitOwnerPinLabelMatch(
674
1984
  text,
@@ -676,16 +1986,32 @@ export class SchematicSvgRenderer {
676
1986
  )
677
1987
 
678
1988
  if (text.recordType === '17') {
679
- return SchematicPowerPortRenderer.buildMarkup(
680
- text,
681
- lines,
682
- pins,
683
- sheetHeight
1989
+ return SchematicSvgRenderer.#appendSvgAttributes(
1990
+ SchematicPowerPortRenderer.buildMarkup(
1991
+ text,
1992
+ lines,
1993
+ pins,
1994
+ sheetHeight
1995
+ ),
1996
+ SchematicSvgRenderer.#semanticAttributes(
1997
+ 'text',
1998
+ text,
1999
+ index,
2000
+ semanticContext
2001
+ )
684
2002
  )
685
2003
  }
686
2004
 
687
2005
  if (text.recordType === '209' || text.recordType === '28') {
688
- return SchematicNoteRenderer.buildMarkup(text, sheetHeight)
2006
+ return SchematicSvgRenderer.#appendSvgAttributes(
2007
+ SchematicNoteRenderer.buildMarkup(text, sheetHeight),
2008
+ SchematicSvgRenderer.#semanticAttributes(
2009
+ 'text',
2010
+ text,
2011
+ index,
2012
+ semanticContext
2013
+ )
2014
+ )
689
2015
  }
690
2016
  const placement = SchematicSvgRenderer.#resolveSchematicTextPlacement(
691
2017
  text,
@@ -694,21 +2020,29 @@ export class SchematicSvgRenderer {
694
2020
  matchedOwnerPin
695
2021
  )
696
2022
 
697
- return createSvgText(
698
- 'schematic-label',
699
- placement.x,
700
- placement.y,
701
- text.text,
702
- SchematicColorResolver.resolveColor(
703
- text.color,
704
- '--schematic-text-color'
2023
+ return SchematicSvgRenderer.#appendSvgAttributes(
2024
+ createSvgText(
2025
+ 'schematic-label',
2026
+ placement.x,
2027
+ placement.y,
2028
+ text.resolvedText ?? text.text,
2029
+ SchematicColorResolver.resolveColor(
2030
+ text.color,
2031
+ '--schematic-text-color'
2032
+ ),
2033
+ SchematicOwnerPinLabelLayout.resolveSchematicTextAnchor(
2034
+ text,
2035
+ placement.anchor,
2036
+ matchedOwnerPin
2037
+ ),
2038
+ SchematicTypography.buildSchematicTextRenderOptions(text)
705
2039
  ),
706
- SchematicOwnerPinLabelLayout.resolveSchematicTextAnchor(
2040
+ SchematicSvgRenderer.#semanticAttributes(
2041
+ 'text',
707
2042
  text,
708
- placement.anchor,
709
- matchedOwnerPin
710
- ),
711
- SchematicTypography.buildSchematicTextRenderOptions(text)
2043
+ index,
2044
+ semanticContext
2045
+ )
712
2046
  )
713
2047
  }
714
2048
 
@@ -1029,17 +2363,33 @@ export class SchematicSvgRenderer {
1029
2363
  * @param {{ x: number, y: number, designator?: string }} component
1030
2364
  * @param {number} sheetHeight
1031
2365
  * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
2366
+ * @param {number} index Stable primitive index.
2367
+ * @param {object | undefined} semanticContext Semantic lookup context.
1032
2368
  * @returns {string}
1033
2369
  */
1034
- static #buildFallbackComponentMarkup(component, sheetHeight, sheet) {
1035
- return createSvgText(
1036
- 'schematic-designator',
1037
- component.x + 8,
1038
- projectSchematicY(sheetHeight, component.y) - 8,
1039
- component.designator || '',
1040
- 'var(--schematic-default-ink-color)',
1041
- 'start',
1042
- SchematicTypography.buildViewerSchematicFontOptions(sheet)
2370
+ static #buildFallbackComponentMarkup(
2371
+ component,
2372
+ sheetHeight,
2373
+ sheet,
2374
+ index = 0,
2375
+ semanticContext = undefined
2376
+ ) {
2377
+ return SchematicSvgRenderer.#appendSvgAttributes(
2378
+ createSvgText(
2379
+ 'schematic-designator',
2380
+ component.x + 8,
2381
+ projectSchematicY(sheetHeight, component.y) - 8,
2382
+ component.designator || '',
2383
+ 'var(--schematic-default-ink-color)',
2384
+ 'start',
2385
+ SchematicTypography.buildViewerSchematicFontOptions(sheet)
2386
+ ),
2387
+ SchematicSvgRenderer.#semanticAttributes(
2388
+ 'component',
2389
+ component,
2390
+ index,
2391
+ semanticContext
2392
+ )
1043
2393
  )
1044
2394
  }
1045
2395