altium-toolkit 0.1.0

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 (82) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +20 -0
  3. package/CONTRIBUTING.md +19 -0
  4. package/LICENSE +22 -0
  5. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  6. package/LICENSES/GPL-3.0-or-later.txt +232 -0
  7. package/NOTICE.md +32 -0
  8. package/README.md +116 -0
  9. package/docs/api.md +73 -0
  10. package/docs/model-format.md +36 -0
  11. package/docs/testing.md +25 -0
  12. package/examples/README.md +47 -0
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
  14. package/examples/arduino-uno/SvgViewportController.mjs +306 -0
  15. package/examples/arduino-uno/example.mjs +480 -0
  16. package/examples/arduino-uno/index.html +163 -0
  17. package/examples/arduino-uno/styles.css +552 -0
  18. package/examples/server.mjs +212 -0
  19. package/package.json +53 -0
  20. package/spec/library-scope.md +32 -0
  21. package/src/core/BinaryReader.mjs +127 -0
  22. package/src/core/altium/AltiumLayoutParser.mjs +485 -0
  23. package/src/core/altium/AltiumParser.mjs +1007 -0
  24. package/src/core/altium/AsciiRecordParser.mjs +151 -0
  25. package/src/core/altium/ParserUtils.mjs +173 -0
  26. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
  27. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
  28. package/src/core/altium/PcbModelParser.mjs +336 -0
  29. package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
  30. package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
  31. package/src/core/altium/PcbStreamExtractor.mjs +210 -0
  32. package/src/core/altium/PrintableTextDecoder.mjs +156 -0
  33. package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
  34. package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
  35. package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
  36. package/src/core/altium/SchematicImageParser.mjs +173 -0
  37. package/src/core/altium/SchematicJunctionParser.mjs +43 -0
  38. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
  39. package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
  40. package/src/core/altium/SchematicPinParser.mjs +767 -0
  41. package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
  42. package/src/core/altium/SchematicSheetParser.mjs +241 -0
  43. package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
  44. package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
  45. package/src/core/altium/SchematicTextParser.mjs +708 -0
  46. package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
  47. package/src/core/ole/OleCompoundDocument.mjs +439 -0
  48. package/src/core/ole/OleConstants.mjs +64 -0
  49. package/src/core/ole/OleDirectoryEntry.mjs +95 -0
  50. package/src/index.mjs +7 -0
  51. package/src/parser.mjs +21 -0
  52. package/src/renderers.mjs +15 -0
  53. package/src/scene3d.mjs +9 -0
  54. package/src/styles/altium-renderers.css +358 -0
  55. package/src/ui/BomTableRenderer.mjs +46 -0
  56. package/src/ui/PcbArcUtils.mjs +189 -0
  57. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
  58. package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
  59. package/src/ui/PcbScene3dBuilder.mjs +742 -0
  60. package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
  61. package/src/ui/PcbScene3dPackages.mjs +137 -0
  62. package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
  63. package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
  64. package/src/ui/PcbSvgRenderer.mjs +906 -0
  65. package/src/ui/SchematicColorResolver.mjs +132 -0
  66. package/src/ui/SchematicContentLayout.mjs +661 -0
  67. package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
  68. package/src/ui/SchematicImageRenderer.mjs +135 -0
  69. package/src/ui/SchematicJunctionRenderer.mjs +381 -0
  70. package/src/ui/SchematicNoteRenderer.mjs +427 -0
  71. package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
  72. package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
  73. package/src/ui/SchematicPortRenderer.mjs +558 -0
  74. package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
  75. package/src/ui/SchematicRegionRenderer.mjs +94 -0
  76. package/src/ui/SchematicShapeRenderer.mjs +398 -0
  77. package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
  78. package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +756 -0
  80. package/src/ui/SchematicSvgUtils.mjs +182 -0
  81. package/src/ui/SchematicTypography.mjs +204 -0
  82. package/src/workers/altium-parser.worker.mjs +29 -0
@@ -0,0 +1,756 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
+ import { SchematicJunctionRenderer } from './SchematicJunctionRenderer.mjs'
7
+ import { SchematicPortRenderer } from './SchematicPortRenderer.mjs'
8
+ import { SchematicTypography } from './SchematicTypography.mjs'
9
+ import { SchematicPowerPortRenderer } from './SchematicPowerPortRenderer.mjs'
10
+ import { SchematicNoteRenderer } from './SchematicNoteRenderer.mjs'
11
+ import { SchematicDirectiveRenderer } from './SchematicDirectiveRenderer.mjs'
12
+ import { SchematicShapeRenderer } from './SchematicShapeRenderer.mjs'
13
+ import { SchematicPinSvgRenderer } from './SchematicPinSvgRenderer.mjs'
14
+ import { SchematicColorResolver } from './SchematicColorResolver.mjs'
15
+ import { SchematicSheetChromeRenderer } from './SchematicSheetChromeRenderer.mjs'
16
+ import { SchematicContentLayout } from './SchematicContentLayout.mjs'
17
+ import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs'
18
+ import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
19
+ import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
20
+ import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
21
+
22
+ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
23
+ SchematicSvgUtils
24
+
25
+ /**
26
+ * Renders normalized schematic models into presentational SVG.
27
+ */
28
+ export class SchematicSvgRenderer {
29
+ /**
30
+ * Renders a normalized schematic model into SVG markup.
31
+ * @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 }[], 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, 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
32
+ * @returns {string}
33
+ */
34
+ static render(documentModel) {
35
+ const schematic = documentModel?.schematic
36
+ if (!schematic) {
37
+ return '<section class="altium-renderer-empty">No schematic entities were recovered from this file.</section>'
38
+ }
39
+
40
+ const width = Math.max(schematic.sheet.width || 1000, 100)
41
+ const height = Math.max(schematic.sheet.height || 700, 100)
42
+ const allTexts = schematic.texts || []
43
+ const lines = schematic.lines.slice(0, 2500)
44
+ const polygons = (schematic.polygons || []).slice(0, 1000)
45
+ const rectangles = (schematic.rectangles || []).slice(0, 500)
46
+ const regions = (schematic.regions || []).slice(0, 250)
47
+ const ellipses = (schematic.ellipses || []).slice(0, 500)
48
+ const arcs = (schematic.arcs || []).slice(0, 1000)
49
+ const directives = (schematic.directives || []).slice(0, 250)
50
+ const texts = allTexts
51
+ const components = schematic.components.slice(0, 180)
52
+ const pins = (schematic.pins || []).slice(0, 1000)
53
+ const ports = (schematic.ports || []).slice(0, 250)
54
+ const crosses = (schematic.crosses || []).slice(0, 250)
55
+ const sheetSymbols = (schematic.sheetSymbols || []).slice(0, 250)
56
+ const sheetEntries = (schematic.sheetEntries || []).slice(0, 500)
57
+ const authoredJunctions = (schematic.junctions || []).slice(0, 500)
58
+ const busEntries = (schematic.busEntries || []).slice(0, 500)
59
+ const images = (schematic.images || []).slice(0, 100)
60
+ const drawableComponents = components.filter(
61
+ (component) =>
62
+ SchematicSvgRenderer.#isDrawableSchematicComponent(component) &&
63
+ !SchematicTypography.hasNearbyVisibleDesignatorText(
64
+ component,
65
+ allTexts
66
+ )
67
+ )
68
+ const frameMarkup = SchematicSvgRenderer.#buildSheetChromeMarkup(
69
+ width,
70
+ height,
71
+ schematic.sheet,
72
+ documentModel?.fileName
73
+ )
74
+ const regionMarkup = SchematicRegionRenderer.buildMarkup(
75
+ regions,
76
+ height
77
+ )
78
+ const contentTransform = SchematicContentLayout.buildTransform(
79
+ width,
80
+ height,
81
+ schematic
82
+ )
83
+ const contentClipId = SchematicContentLayout.buildClipId(
84
+ width,
85
+ height,
86
+ schematic
87
+ )
88
+ const contentClipMarkup = SchematicContentLayout.buildClipMarkup(
89
+ width,
90
+ height,
91
+ schematic,
92
+ contentClipId
93
+ )
94
+ const ownerlessLines = lines.filter((line) => !line.ownerIndex)
95
+ const ownerlessPolygons = polygons.filter(
96
+ (polygon) => !polygon.ownerIndex
97
+ )
98
+ const ownerlessRectangles = rectangles.filter(
99
+ (rectangle) => !rectangle.ownerIndex
100
+ )
101
+ const ownerlessEllipses = ellipses.filter(
102
+ (ellipse) => !ellipse.ownerIndex
103
+ )
104
+ const ownerlessArcs = arcs.filter((arc) => !arc.ownerIndex)
105
+ const resolvedTexts = texts.map((text) =>
106
+ text.recordType === '17'
107
+ ? {
108
+ ...text,
109
+ powerPortDirection:
110
+ SchematicPowerPortRenderer.resolveOutwardDirection(
111
+ text,
112
+ lines,
113
+ pins
114
+ )
115
+ }
116
+ : text
117
+ )
118
+ const polygonMarkup = ownerlessPolygons
119
+ .map((polygon) =>
120
+ SchematicShapeRenderer.buildPolygonMarkup(polygon, height)
121
+ )
122
+ .join('')
123
+ const rectangleMarkup = ownerlessRectangles
124
+ .map((rectangle) =>
125
+ SchematicShapeRenderer.buildRectangleMarkup(rectangle, height)
126
+ )
127
+ .join('')
128
+ const ellipseMarkup = ownerlessEllipses
129
+ .map((ellipse) =>
130
+ SchematicShapeRenderer.buildEllipseMarkup(ellipse, height)
131
+ )
132
+ .join('')
133
+ const lineMarkup = ownerlessLines
134
+ .map((line) =>
135
+ SchematicSvgRenderer.#buildSchematicLineMarkup(line, height)
136
+ )
137
+ .join('')
138
+ const arcMarkup = ownerlessArcs
139
+ .map((arc) => SchematicShapeRenderer.buildArcMarkup(arc, height))
140
+ .join('')
141
+ const ownerGeometryMarkup =
142
+ SchematicSvgRenderer.#buildOwnerGeometryMarkup(
143
+ lines,
144
+ polygons,
145
+ rectangles,
146
+ ellipses,
147
+ arcs,
148
+ height
149
+ )
150
+ const sheetSymbolMarkup =
151
+ SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
152
+ sheetSymbols,
153
+ height
154
+ )
155
+ const sheetEntryMarkup =
156
+ SchematicSheetSymbolRenderer.buildSheetEntryMarkup(
157
+ sheetEntries,
158
+ height
159
+ )
160
+ const busEntryMarkup = busEntries
161
+ .map((busEntry) =>
162
+ SchematicSvgRenderer.#buildSchematicBusEntryMarkup(
163
+ busEntry,
164
+ height
165
+ )
166
+ )
167
+ .join('')
168
+ const authoredJunctionMarkup = authoredJunctions
169
+ .map((junction) =>
170
+ SchematicSvgRenderer.#buildAuthoredSchematicJunctionMarkup(
171
+ junction,
172
+ height
173
+ )
174
+ )
175
+ .join('')
176
+ const imageMarkup = SchematicImageRenderer.buildMarkup(images, height)
177
+
178
+ const textMarkup = resolvedTexts
179
+ .map((text) =>
180
+ SchematicSvgRenderer.#buildSchematicTextMarkup(
181
+ text,
182
+ height,
183
+ lines,
184
+ pins
185
+ )
186
+ )
187
+ .join('')
188
+
189
+ const componentMarkup = drawableComponents
190
+ .map((component) =>
191
+ SchematicSvgRenderer.#buildFallbackComponentMarkup(
192
+ component,
193
+ height,
194
+ schematic.sheet
195
+ )
196
+ )
197
+ .join('')
198
+
199
+ const rotatedVerticalNumberOwners =
200
+ SchematicTypography.collectRotatedVerticalNumberOwners(pins)
201
+ const explicitOwnerPinNameLabels =
202
+ SchematicTypography.collectExplicitOwnerPinNameLabels(texts)
203
+ const explicitOwnerPinLabelOffsets =
204
+ SchematicOwnerPinLabelLayout.collectExplicitOwnerPinLabelOffsets(
205
+ texts,
206
+ pins
207
+ )
208
+ const pinMarkup = pins
209
+ .map((pin) =>
210
+ SchematicPinSvgRenderer.buildMarkup(
211
+ pin,
212
+ height,
213
+ schematic.sheet,
214
+ rotatedVerticalNumberOwners,
215
+ explicitOwnerPinNameLabels,
216
+ explicitOwnerPinLabelOffsets
217
+ )
218
+ )
219
+ .join('')
220
+ const portMarkup = SchematicPortRenderer.buildMarkup(
221
+ ports,
222
+ height,
223
+ schematic.sheet
224
+ )
225
+ const directiveMarkup = SchematicDirectiveRenderer.buildMarkup(
226
+ directives,
227
+ height,
228
+ schematic.sheet
229
+ )
230
+ const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
231
+ lines,
232
+ crosses,
233
+ ports,
234
+ resolvedTexts.filter((text) => text.recordType === '17'),
235
+ height
236
+ )
237
+ const crossMarkup = crosses
238
+ .map((cross) =>
239
+ SchematicSvgRenderer.#buildSchematicCrossMarkup(cross, height)
240
+ )
241
+ .join('')
242
+
243
+ return (
244
+ '<section class="svg-panel">' +
245
+ '<header class="svg-panel__header"><h3>' +
246
+ escapeHtml(documentModel?.summary?.title || 'Schematic') +
247
+ '</h3><p>' +
248
+ lines.length +
249
+ ' line segments, ' +
250
+ components.length +
251
+ ' components</p></header>' +
252
+ '<svg class="schematic-svg" viewBox="0 0 ' +
253
+ formatNumber(width) +
254
+ ' ' +
255
+ formatNumber(height) +
256
+ '" preserveAspectRatio="xMidYMid meet" aria-label="Schematic view">' +
257
+ '<rect class="sheet-backdrop" x="0" y="0" width="' +
258
+ formatNumber(width) +
259
+ '" height="' +
260
+ formatNumber(height) +
261
+ '" rx="18" />' +
262
+ contentClipMarkup +
263
+ '<g class="schematic-content"' +
264
+ ' clip-path="url(#' +
265
+ escapeHtml(contentClipId) +
266
+ ')"' +
267
+ contentTransform +
268
+ '>' +
269
+ '<g class="schematic-polygons">' +
270
+ polygonMarkup +
271
+ '</g>' +
272
+ '<g class="schematic-rectangles">' +
273
+ rectangleMarkup +
274
+ '</g>' +
275
+ '<g class="schematic-ellipses">' +
276
+ ellipseMarkup +
277
+ '</g>' +
278
+ '<g class="schematic-lines" stroke-linecap="round">' +
279
+ lineMarkup +
280
+ '</g>' +
281
+ '<g class="schematic-arcs" stroke-linecap="round">' +
282
+ arcMarkup +
283
+ '</g>' +
284
+ '<g class="schematic-owner-geometry" stroke-linecap="round">' +
285
+ ownerGeometryMarkup +
286
+ '</g>' +
287
+ '<g class="schematic-sheet-symbols">' +
288
+ sheetSymbolMarkup +
289
+ '</g>' +
290
+ '<g class="schematic-bus-entries" stroke-linecap="round">' +
291
+ busEntryMarkup +
292
+ '</g>' +
293
+ '<g class="schematic-images">' +
294
+ imageMarkup +
295
+ '</g>' +
296
+ '<g class="schematic-pins" stroke-linecap="round">' +
297
+ pinMarkup +
298
+ '</g>' +
299
+ '<g class="schematic-ports">' +
300
+ portMarkup +
301
+ '</g>' +
302
+ '<g class="schematic-directives">' +
303
+ directiveMarkup +
304
+ '</g>' +
305
+ '<g class="schematic-crosses" stroke-linecap="round">' +
306
+ crossMarkup +
307
+ '</g>' +
308
+ '<g class="schematic-components">' +
309
+ componentMarkup +
310
+ '</g>' +
311
+ '<g class="schematic-sheet-entries">' +
312
+ sheetEntryMarkup +
313
+ '</g>' +
314
+ '<g class="schematic-texts">' +
315
+ textMarkup +
316
+ '</g>' +
317
+ '<g class="schematic-junctions">' +
318
+ authoredJunctionMarkup +
319
+ junctionMarkup +
320
+ '</g>' +
321
+ '</g>' +
322
+ frameMarkup +
323
+ '<g class="schematic-regions">' +
324
+ regionMarkup +
325
+ '</g>' +
326
+ '</svg></section>'
327
+ )
328
+ }
329
+
330
+ /**
331
+ * Builds interleaved owner geometry so symbol-internal primitives preserve
332
+ * their recovered Altium paint order instead of batching fills ahead of all
333
+ * linework.
334
+ * @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, renderOrder?: number }[]} lines
335
+ * @param {{ points: { x: number, y: number }[], ownerIndex?: string, renderOrder?: number }[]} polygons
336
+ * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string, renderOrder?: number }[]} rectangles
337
+ * @param {{ x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string, renderOrder?: number }[]} ellipses
338
+ * @param {{ x: number, y: number, radius: number, startAngle: number, endAngle: number, ownerIndex?: string, renderOrder?: number }[]} arcs
339
+ * @param {number} sheetHeight
340
+ * @returns {string}
341
+ */
342
+ static #buildOwnerGeometryMarkup(
343
+ lines,
344
+ polygons,
345
+ rectangles,
346
+ ellipses,
347
+ arcs,
348
+ sheetHeight
349
+ ) {
350
+ const items = []
351
+
352
+ for (const polygon of polygons) {
353
+ if (!polygon.ownerIndex) {
354
+ continue
355
+ }
356
+
357
+ items.push({
358
+ renderOrder:
359
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(polygon),
360
+ typeOrder: 0,
361
+ markup: SchematicShapeRenderer.buildPolygonMarkup(
362
+ polygon,
363
+ sheetHeight
364
+ )
365
+ })
366
+ }
367
+
368
+ for (const rectangle of rectangles) {
369
+ if (!rectangle.ownerIndex) {
370
+ continue
371
+ }
372
+
373
+ items.push({
374
+ renderOrder:
375
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(
376
+ rectangle
377
+ ),
378
+ typeOrder: 1,
379
+ markup: SchematicShapeRenderer.buildRectangleMarkup(
380
+ rectangle,
381
+ sheetHeight
382
+ )
383
+ })
384
+ }
385
+
386
+ for (const ellipse of ellipses) {
387
+ if (!ellipse.ownerIndex) {
388
+ continue
389
+ }
390
+
391
+ items.push({
392
+ renderOrder:
393
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(ellipse),
394
+ typeOrder: 2,
395
+ markup: SchematicShapeRenderer.buildEllipseMarkup(
396
+ ellipse,
397
+ sheetHeight
398
+ )
399
+ })
400
+ }
401
+
402
+ for (const line of lines) {
403
+ if (!line.ownerIndex) {
404
+ continue
405
+ }
406
+
407
+ items.push({
408
+ renderOrder:
409
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(line),
410
+ typeOrder: 3,
411
+ markup: SchematicSvgRenderer.#buildSchematicLineMarkup(
412
+ line,
413
+ sheetHeight
414
+ )
415
+ })
416
+ }
417
+
418
+ for (const arc of arcs) {
419
+ if (!arc.ownerIndex) {
420
+ continue
421
+ }
422
+
423
+ items.push({
424
+ renderOrder:
425
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(arc),
426
+ typeOrder: 4,
427
+ markup: SchematicShapeRenderer.buildArcMarkup(arc, sheetHeight)
428
+ })
429
+ }
430
+
431
+ return items
432
+ .sort((left, right) => {
433
+ const renderDelta = left.renderOrder - right.renderOrder
434
+
435
+ if (renderDelta !== 0) {
436
+ return renderDelta
437
+ }
438
+
439
+ return left.typeOrder - right.typeOrder
440
+ })
441
+ .map((item) => item.markup)
442
+ .join('')
443
+ }
444
+
445
+ /**
446
+ * Resolves one sortable render-order value for an already-normalized
447
+ * schematic primitive.
448
+ * @param {{ renderOrder?: number }} primitive
449
+ * @returns {number}
450
+ */
451
+ static #resolvePrimitiveRenderOrder(primitive) {
452
+ const renderOrder = Number(primitive?.renderOrder)
453
+
454
+ if (Number.isFinite(renderOrder)) {
455
+ return renderOrder
456
+ }
457
+
458
+ return Number.MAX_SAFE_INTEGER
459
+ }
460
+
461
+ /**
462
+ * Builds one schematic line segment, preserving dashed line styles when
463
+ * the source primitive requests them.
464
+ * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean }} line
465
+ * @param {number} sheetHeight
466
+ * @returns {string}
467
+ */
468
+ static #buildSchematicLineMarkup(line, sheetHeight) {
469
+ return (
470
+ '<line x1="' +
471
+ formatNumber(line.x1) +
472
+ '" y1="' +
473
+ formatNumber(projectSchematicY(sheetHeight, line.y1)) +
474
+ '" x2="' +
475
+ formatNumber(line.x2) +
476
+ '" y2="' +
477
+ formatNumber(projectSchematicY(sheetHeight, line.y2)) +
478
+ '" stroke="' +
479
+ escapeHtml(
480
+ SchematicColorResolver.resolveColor(
481
+ line.color,
482
+ '--schematic-default-ink-color'
483
+ )
484
+ ) +
485
+ '" stroke-width="' +
486
+ formatNumber(
487
+ SchematicSvgRenderer.#resolveSchematicLineWidth(line)
488
+ ) +
489
+ '"' +
490
+ SchematicSvgRenderer.#buildSchematicLineStyleAttributes(line) +
491
+ ' />'
492
+ )
493
+ }
494
+
495
+ /**
496
+ * Resolves the visible SVG stroke width for one schematic line primitive.
497
+ * @param {{ width: number, isBus?: boolean }} line
498
+ * @returns {number}
499
+ */
500
+ static #resolveSchematicLineWidth(line) {
501
+ const baseWidth = Math.max(Number(line.width || 0), 0.8)
502
+ if (line.isBus !== true) {
503
+ return baseWidth
504
+ }
505
+ return Math.max(baseWidth * 3, 3)
506
+ }
507
+
508
+ /**
509
+ * Returns SVG stroke attributes for one schematic line style.
510
+ * @param {{ width: number, lineStyle?: number }} line
511
+ * @returns {string}
512
+ */
513
+ static #buildSchematicLineStyleAttributes(line) {
514
+ if (Number(line.lineStyle || 0) !== 1) {
515
+ return ''
516
+ }
517
+ const dashLength = Math.max(Number(line.width || 1) * 8, 8)
518
+ const gapLength = Math.max(Number(line.width || 1) * 5, 5)
519
+ return (
520
+ ' stroke-dasharray="' +
521
+ formatNumber(dashLength) +
522
+ ' ' +
523
+ formatNumber(gapLength) +
524
+ '" stroke-linecap="round"'
525
+ )
526
+ }
527
+
528
+ /**
529
+ * Builds page border and title-block chrome from sheet metadata.
530
+ * @param {number} width
531
+ * @param {number} height
532
+ * @param {{ borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, paperSize?: string, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }} sheet
533
+ * @param {string | undefined} fileName
534
+ * @returns {string}
535
+ */
536
+ static #buildSheetChromeMarkup(width, height, sheet, fileName) {
537
+ return SchematicSheetChromeRenderer.buildMarkup(
538
+ width,
539
+ height,
540
+ sheet,
541
+ fileName
542
+ )
543
+ }
544
+
545
+ /**
546
+ * Builds one free text primitive with font metadata.
547
+ * @param {{ x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
548
+ * @param {number} sheetHeight
549
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
550
+ * @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
551
+ * @returns {string}
552
+ */
553
+ static #buildSchematicTextMarkup(text, sheetHeight, lines, pins) {
554
+ const matchedOwnerPin =
555
+ SchematicOwnerPinLabelLayout.findExplicitOwnerPinLabelMatch(
556
+ text,
557
+ pins
558
+ )
559
+
560
+ if (text.recordType === '17') {
561
+ return SchematicPowerPortRenderer.buildMarkup(
562
+ text,
563
+ lines,
564
+ pins,
565
+ sheetHeight
566
+ )
567
+ }
568
+
569
+ if (text.recordType === '209' || text.recordType === '28') {
570
+ return SchematicNoteRenderer.buildMarkup(text, sheetHeight)
571
+ }
572
+ const placement = SchematicSvgRenderer.#resolveSchematicTextPlacement(
573
+ text,
574
+ sheetHeight,
575
+ matchedOwnerPin
576
+ )
577
+
578
+ return createSvgText(
579
+ 'schematic-label',
580
+ placement.x,
581
+ placement.y,
582
+ text.text,
583
+ SchematicColorResolver.resolveColor(
584
+ text.color,
585
+ '--schematic-text-color'
586
+ ),
587
+ SchematicOwnerPinLabelLayout.resolveSchematicTextAnchor(
588
+ text,
589
+ placement.anchor,
590
+ matchedOwnerPin
591
+ ),
592
+ SchematicTypography.buildSchematicTextRenderOptions(text)
593
+ )
594
+ }
595
+
596
+ /**
597
+ * Resolves final text placement for schematic free-text annotations.
598
+ * @param {{ x: number, y: number, text: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end' }} text
599
+ * @param {number} sheetHeight
600
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
601
+ * @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
602
+ */
603
+ static #resolveSchematicTextPlacement(text, sheetHeight, matchedOwnerPin) {
604
+ const mirroredOwnerPinPlacement =
605
+ SchematicOwnerPinLabelLayout.resolveMirroredOwnerPinLabelPlacement(
606
+ text,
607
+ matchedOwnerPin
608
+ )
609
+
610
+ return {
611
+ x: mirroredOwnerPinPlacement?.x ?? text.x,
612
+ y: projectSchematicY(
613
+ sheetHeight,
614
+ mirroredOwnerPinPlacement?.y ?? text.y
615
+ ),
616
+ anchor: text.anchor || 'start'
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Builds one schematic cross marker.
622
+ * @param {{ x: number, y: number, size: number, color: string }} cross
623
+ * @param {number} sheetHeight
624
+ * @returns {string}
625
+ */
626
+ static #buildSchematicCrossMarkup(cross, sheetHeight) {
627
+ const x = cross.x
628
+ const y = projectSchematicY(sheetHeight, cross.y)
629
+ const half = Math.max(Number(cross.size || 6), 4) / 2
630
+
631
+ return (
632
+ '<g class="schematic-cross"><line x1="' +
633
+ formatNumber(x - half) +
634
+ '" y1="' +
635
+ formatNumber(y - half) +
636
+ '" x2="' +
637
+ formatNumber(x + half) +
638
+ '" y2="' +
639
+ formatNumber(y + half) +
640
+ '" stroke="' +
641
+ escapeHtml(
642
+ SchematicColorResolver.resolveColor(
643
+ cross.color,
644
+ '--schematic-alert-color'
645
+ )
646
+ ) +
647
+ '" /><line x1="' +
648
+ formatNumber(x - half) +
649
+ '" y1="' +
650
+ formatNumber(y + half) +
651
+ '" x2="' +
652
+ formatNumber(x + half) +
653
+ '" y2="' +
654
+ formatNumber(y - half) +
655
+ '" stroke="' +
656
+ escapeHtml(
657
+ SchematicColorResolver.resolveColor(
658
+ cross.color,
659
+ '--schematic-alert-color'
660
+ )
661
+ ) +
662
+ '" /></g>'
663
+ )
664
+ }
665
+
666
+ /**
667
+ * Builds one authored schematic junction dot.
668
+ * @param {{ x: number, y: number, color: string }} junction
669
+ * @param {number} sheetHeight
670
+ * @returns {string}
671
+ */
672
+ static #buildAuthoredSchematicJunctionMarkup(junction, sheetHeight) {
673
+ return (
674
+ '<circle class="schematic-authored-junction" cx="' +
675
+ formatNumber(junction.x) +
676
+ '" cy="' +
677
+ formatNumber(projectSchematicY(sheetHeight, junction.y)) +
678
+ '" r="2.4" fill="' +
679
+ escapeHtml(
680
+ SchematicColorResolver.resolveColor(
681
+ junction.color,
682
+ '--schematic-default-ink-color'
683
+ )
684
+ ) +
685
+ '" />'
686
+ )
687
+ }
688
+
689
+ /**
690
+ * Builds one schematic bus-entry line marker.
691
+ * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number }} busEntry
692
+ * @param {number} sheetHeight
693
+ * @returns {string}
694
+ */
695
+ static #buildSchematicBusEntryMarkup(busEntry, sheetHeight) {
696
+ return (
697
+ '<line class="schematic-bus-entry" x1="' +
698
+ formatNumber(busEntry.x1) +
699
+ '" y1="' +
700
+ formatNumber(projectSchematicY(sheetHeight, busEntry.y1)) +
701
+ '" x2="' +
702
+ formatNumber(busEntry.x2) +
703
+ '" y2="' +
704
+ formatNumber(projectSchematicY(sheetHeight, busEntry.y2)) +
705
+ '" stroke="' +
706
+ escapeHtml(
707
+ SchematicColorResolver.resolveColor(
708
+ busEntry.color,
709
+ '--schematic-default-ink-color'
710
+ )
711
+ ) +
712
+ '" stroke-width="' +
713
+ formatNumber(Math.max(Number(busEntry.width || 1), 0.8)) +
714
+ '" />'
715
+ )
716
+ }
717
+
718
+ /**
719
+ * Builds one synthetic designator label for a fallback component
720
+ * placement without the old marker circle.
721
+ * @param {{ x: number, y: number, designator?: string }} component
722
+ * @param {number} sheetHeight
723
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
724
+ * @returns {string}
725
+ */
726
+ static #buildFallbackComponentMarkup(component, sheetHeight, sheet) {
727
+ return createSvgText(
728
+ 'schematic-designator',
729
+ component.x + 8,
730
+ projectSchematicY(sheetHeight, component.y) - 8,
731
+ component.designator || '',
732
+ 'var(--schematic-default-ink-color)',
733
+ 'start',
734
+ SchematicTypography.buildViewerSchematicFontOptions(sheet)
735
+ )
736
+ }
737
+
738
+ /**
739
+ * Returns true when a component has enough placement data to draw a
740
+ * fallback designator label.
741
+ * @param {{ x?: number, y?: number, designator?: string }} component
742
+ * @returns {boolean}
743
+ */
744
+ static #isDrawableSchematicComponent(component) {
745
+ if (!component) return false
746
+
747
+ const hasCoordinates =
748
+ Number.isFinite(component.x) &&
749
+ Number.isFinite(component.y) &&
750
+ (component.x !== 0 || component.y !== 0)
751
+ const hasResolvedDesignator =
752
+ Boolean(component.designator) && component.designator !== 'U?'
753
+
754
+ return hasCoordinates && hasResolvedDesignator
755
+ }
756
+ }