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,574 @@
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 { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+ import { SchematicTypography } from './SchematicTypography.mjs'
8
+
9
+ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
10
+ SchematicSvgUtils
11
+
12
+ /**
13
+ * Builds schematic power-port symbols from normalized text records.
14
+ */
15
+ export class SchematicPowerPortRenderer {
16
+ /**
17
+ * Renders one power-port symbol and label.
18
+ * @param {{ x: number, y: number, text: string, color: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, anchor?: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right' }} text
19
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
20
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
21
+ * @param {number} sheetHeight
22
+ * @returns {string}
23
+ */
24
+ static buildMarkup(text, lines, pins, sheetHeight) {
25
+ const x = text.x
26
+ const y = projectSchematicY(sheetHeight, text.y)
27
+ const direction = SchematicPowerPortRenderer.resolveOutwardDirection(
28
+ text,
29
+ lines,
30
+ pins
31
+ )
32
+ const labelOptions = SchematicTypography.withViewerFontSize({
33
+ fontSize: Number(text.fontSize || 10),
34
+ fontFamily: text.fontFamily,
35
+ fontWeight: text.fontWeight
36
+ })
37
+ const fontSize = Number(labelOptions.fontSize || 9)
38
+ const resolvedColor = SchematicColorResolver.resolveColor(
39
+ text.color,
40
+ '--schematic-power-color'
41
+ )
42
+
43
+ if (Number(text.style) === 4) {
44
+ return (
45
+ '<g class="schematic-power-port schematic-power-port--ground" stroke-linecap="round">' +
46
+ SchematicPowerPortRenderer.#buildGroundLines(
47
+ x,
48
+ y,
49
+ direction,
50
+ resolvedColor
51
+ ) +
52
+ SchematicPowerPortRenderer.#buildDirectionalLabel(
53
+ text,
54
+ direction,
55
+ x,
56
+ y,
57
+ fontSize,
58
+ labelOptions,
59
+ resolvedColor
60
+ ) +
61
+ '</g>'
62
+ )
63
+ }
64
+
65
+ return (
66
+ '<g class="schematic-power-port schematic-power-port--rail" stroke-linecap="round">' +
67
+ SchematicPowerPortRenderer.#buildRailLine(
68
+ x,
69
+ y,
70
+ direction,
71
+ resolvedColor
72
+ ) +
73
+ SchematicPowerPortRenderer.#buildDirectionalLabel(
74
+ text,
75
+ direction,
76
+ x,
77
+ y,
78
+ fontSize,
79
+ labelOptions,
80
+ resolvedColor
81
+ ) +
82
+ '</g>'
83
+ )
84
+ }
85
+
86
+ /**
87
+ * Picks the symbol direction away from the attached wire or pin stub.
88
+ * @param {{ x: number, y: number, style?: number, powerPortDirection?: 'up' | 'down' | 'left' | 'right' }} text
89
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
90
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
91
+ * @returns {'up' | 'down' | 'left' | 'right'}
92
+ */
93
+ static resolveOutwardDirection(text, lines, pins) {
94
+ if (text.powerPortDirection) {
95
+ return text.powerPortDirection
96
+ }
97
+
98
+ const candidates = []
99
+
100
+ for (const line of lines) {
101
+ const lineDirection =
102
+ SchematicPowerPortRenderer.#resolveLineDirection(text, line)
103
+
104
+ if (lineDirection) {
105
+ candidates.push(lineDirection)
106
+ }
107
+ }
108
+
109
+ for (const pin of pins) {
110
+ const pinDirection =
111
+ SchematicPowerPortRenderer.#resolvePinDirection(text, pin)
112
+
113
+ if (pinDirection) {
114
+ candidates.push(pinDirection)
115
+ }
116
+ }
117
+
118
+ if (!candidates.length) {
119
+ return Number(text.style) === 4 ? 'down' : 'up'
120
+ }
121
+
122
+ const counts = new Map()
123
+
124
+ for (const candidate of candidates) {
125
+ counts.set(candidate, (counts.get(candidate) || 0) + 1)
126
+ }
127
+
128
+ return [...counts.entries()].sort((left, right) => {
129
+ const countDelta = right[1] - left[1]
130
+
131
+ if (countDelta !== 0) {
132
+ return countDelta
133
+ }
134
+
135
+ return (
136
+ SchematicPowerPortRenderer.#resolveDirectionPriority(
137
+ text,
138
+ right[0]
139
+ ) -
140
+ SchematicPowerPortRenderer.#resolveDirectionPriority(
141
+ text,
142
+ left[0]
143
+ )
144
+ )
145
+ })[0][0]
146
+ }
147
+
148
+ /**
149
+ * Breaks directional ties for symbols whose connectivity touches more than
150
+ * one branch at the same attachment point.
151
+ * @param {{ style?: number }} text
152
+ * @param {'up' | 'down' | 'left' | 'right'} direction
153
+ * @returns {number}
154
+ */
155
+ static #resolveDirectionPriority(text, direction) {
156
+ if (Number(text.style) !== 4) {
157
+ return 0
158
+ }
159
+
160
+ switch (direction) {
161
+ case 'down':
162
+ return 2
163
+ case 'up':
164
+ return 1
165
+ default:
166
+ return 0
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Resolves one attached line segment into the direction the symbol should
172
+ * point away from.
173
+ * @param {{ x: number, y: number }} text
174
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} line
175
+ * @returns {'up' | 'down' | 'left' | 'right' | null}
176
+ */
177
+ static #resolveLineDirection(text, line) {
178
+ if (
179
+ Math.abs(line.x1 - text.x) <= 2 &&
180
+ Math.abs(line.y1 - text.y) <= 2
181
+ ) {
182
+ return SchematicPowerPortRenderer.#invertDirection(
183
+ SchematicPowerPortRenderer.#directionFromPointDelta(
184
+ line.x2 - text.x,
185
+ line.y2 - text.y
186
+ )
187
+ )
188
+ }
189
+
190
+ if (
191
+ Math.abs(line.x2 - text.x) <= 2 &&
192
+ Math.abs(line.y2 - text.y) <= 2
193
+ ) {
194
+ return SchematicPowerPortRenderer.#invertDirection(
195
+ SchematicPowerPortRenderer.#directionFromPointDelta(
196
+ line.x1 - text.x,
197
+ line.y1 - text.y
198
+ )
199
+ )
200
+ }
201
+
202
+ return null
203
+ }
204
+
205
+ /**
206
+ * Resolves one attached pin stub into the direction the symbol should
207
+ * point away from.
208
+ * @param {{ x: number, y: number }} text
209
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
210
+ * @returns {'up' | 'down' | 'left' | 'right' | null}
211
+ */
212
+ static #resolvePinDirection(text, pin) {
213
+ const endpoint =
214
+ SchematicPowerPortRenderer.#projectPinOuterEndpoint(pin)
215
+
216
+ if (
217
+ !endpoint ||
218
+ Math.abs(endpoint.x - text.x) > 2 ||
219
+ Math.abs(endpoint.y - text.y) > 2
220
+ ) {
221
+ return null
222
+ }
223
+
224
+ return SchematicPowerPortRenderer.#invertDirection(
225
+ SchematicPowerPortRenderer.#directionFromPointDelta(
226
+ pin.x - text.x,
227
+ pin.y - text.y
228
+ )
229
+ )
230
+ }
231
+
232
+ /**
233
+ * Maps one delta into the dominant cardinal direction.
234
+ * @param {number} dx
235
+ * @param {number} dy
236
+ * @returns {'up' | 'down' | 'left' | 'right'}
237
+ */
238
+ static #directionFromPointDelta(dx, dy) {
239
+ if (Math.abs(dx) >= Math.abs(dy)) {
240
+ return dx < 0 ? 'left' : 'right'
241
+ }
242
+
243
+ return dy < 0 ? 'down' : 'up'
244
+ }
245
+
246
+ /**
247
+ * Flips one cardinal direction.
248
+ * @param {'up' | 'down' | 'left' | 'right'} direction
249
+ * @returns {'up' | 'down' | 'left' | 'right'}
250
+ */
251
+ static #invertDirection(direction) {
252
+ switch (direction) {
253
+ case 'up':
254
+ return 'down'
255
+ case 'down':
256
+ return 'up'
257
+ case 'left':
258
+ return 'right'
259
+ default:
260
+ return 'left'
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Projects one pin into its outer connection endpoint.
266
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
267
+ * @returns {{ x: number, y: number } | null}
268
+ */
269
+ static #projectPinOuterEndpoint(pin) {
270
+ switch (pin.orientation) {
271
+ case 'left':
272
+ return { x: pin.x - pin.length, y: pin.y }
273
+ case 'right':
274
+ return { x: pin.x + pin.length, y: pin.y }
275
+ case 'top':
276
+ return { x: pin.x, y: pin.y + pin.length }
277
+ case 'bottom':
278
+ return { x: pin.x, y: pin.y - pin.length }
279
+ default:
280
+ return null
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Builds the ground symbol linework for one direction.
286
+ * @param {number} x
287
+ * @param {number} y
288
+ * @param {'up' | 'down' | 'left' | 'right'} direction
289
+ * @param {string} color
290
+ * @returns {string}
291
+ */
292
+ static #buildGroundLines(x, y, direction, color) {
293
+ const stroke = escapeHtml(color)
294
+
295
+ if (direction === 'up') {
296
+ return (
297
+ '<line x1="' +
298
+ formatNumber(x) +
299
+ '" y1="' +
300
+ formatNumber(y) +
301
+ '" x2="' +
302
+ formatNumber(x) +
303
+ '" y2="' +
304
+ formatNumber(y - 7) +
305
+ '" stroke="' +
306
+ stroke +
307
+ '" /><line x1="' +
308
+ formatNumber(x - 7) +
309
+ '" y1="' +
310
+ formatNumber(y - 7) +
311
+ '" x2="' +
312
+ formatNumber(x + 7) +
313
+ '" y2="' +
314
+ formatNumber(y - 7) +
315
+ '" stroke="' +
316
+ stroke +
317
+ '" /><line x1="' +
318
+ formatNumber(x - 5) +
319
+ '" y1="' +
320
+ formatNumber(y - 10) +
321
+ '" x2="' +
322
+ formatNumber(x + 5) +
323
+ '" y2="' +
324
+ formatNumber(y - 10) +
325
+ '" stroke="' +
326
+ stroke +
327
+ '" /><line x1="' +
328
+ formatNumber(x - 3) +
329
+ '" y1="' +
330
+ formatNumber(y - 13) +
331
+ '" x2="' +
332
+ formatNumber(x + 3) +
333
+ '" y2="' +
334
+ formatNumber(y - 13) +
335
+ '" stroke="' +
336
+ stroke +
337
+ '" />'
338
+ )
339
+ }
340
+
341
+ if (direction === 'right') {
342
+ return (
343
+ '<line x1="' +
344
+ formatNumber(x) +
345
+ '" y1="' +
346
+ formatNumber(y) +
347
+ '" x2="' +
348
+ formatNumber(x + 7) +
349
+ '" y2="' +
350
+ formatNumber(y) +
351
+ '" stroke="' +
352
+ stroke +
353
+ '" /><line x1="' +
354
+ formatNumber(x + 7) +
355
+ '" y1="' +
356
+ formatNumber(y - 7) +
357
+ '" x2="' +
358
+ formatNumber(x + 7) +
359
+ '" y2="' +
360
+ formatNumber(y + 7) +
361
+ '" stroke="' +
362
+ stroke +
363
+ '" /><line x1="' +
364
+ formatNumber(x + 10) +
365
+ '" y1="' +
366
+ formatNumber(y - 5) +
367
+ '" x2="' +
368
+ formatNumber(x + 10) +
369
+ '" y2="' +
370
+ formatNumber(y + 5) +
371
+ '" stroke="' +
372
+ stroke +
373
+ '" /><line x1="' +
374
+ formatNumber(x + 13) +
375
+ '" y1="' +
376
+ formatNumber(y - 3) +
377
+ '" x2="' +
378
+ formatNumber(x + 13) +
379
+ '" y2="' +
380
+ formatNumber(y + 3) +
381
+ '" stroke="' +
382
+ stroke +
383
+ '" />'
384
+ )
385
+ }
386
+
387
+ if (direction === 'left') {
388
+ return (
389
+ '<line x1="' +
390
+ formatNumber(x) +
391
+ '" y1="' +
392
+ formatNumber(y) +
393
+ '" x2="' +
394
+ formatNumber(x - 7) +
395
+ '" y2="' +
396
+ formatNumber(y) +
397
+ '" stroke="' +
398
+ stroke +
399
+ '" /><line x1="' +
400
+ formatNumber(x - 7) +
401
+ '" y1="' +
402
+ formatNumber(y - 7) +
403
+ '" x2="' +
404
+ formatNumber(x - 7) +
405
+ '" y2="' +
406
+ formatNumber(y + 7) +
407
+ '" stroke="' +
408
+ stroke +
409
+ '" /><line x1="' +
410
+ formatNumber(x - 10) +
411
+ '" y1="' +
412
+ formatNumber(y - 5) +
413
+ '" x2="' +
414
+ formatNumber(x - 10) +
415
+ '" y2="' +
416
+ formatNumber(y + 5) +
417
+ '" stroke="' +
418
+ stroke +
419
+ '" /><line x1="' +
420
+ formatNumber(x - 13) +
421
+ '" y1="' +
422
+ formatNumber(y - 3) +
423
+ '" x2="' +
424
+ formatNumber(x - 13) +
425
+ '" y2="' +
426
+ formatNumber(y + 3) +
427
+ '" stroke="' +
428
+ stroke +
429
+ '" />'
430
+ )
431
+ }
432
+
433
+ return (
434
+ '<line x1="' +
435
+ formatNumber(x) +
436
+ '" y1="' +
437
+ formatNumber(y) +
438
+ '" x2="' +
439
+ formatNumber(x) +
440
+ '" y2="' +
441
+ formatNumber(y + 7) +
442
+ '" stroke="' +
443
+ stroke +
444
+ '" /><line x1="' +
445
+ formatNumber(x - 7) +
446
+ '" y1="' +
447
+ formatNumber(y + 7) +
448
+ '" x2="' +
449
+ formatNumber(x + 7) +
450
+ '" y2="' +
451
+ formatNumber(y + 7) +
452
+ '" stroke="' +
453
+ stroke +
454
+ '" /><line x1="' +
455
+ formatNumber(x - 5) +
456
+ '" y1="' +
457
+ formatNumber(y + 10) +
458
+ '" x2="' +
459
+ formatNumber(x + 5) +
460
+ '" y2="' +
461
+ formatNumber(y + 10) +
462
+ '" stroke="' +
463
+ stroke +
464
+ '" /><line x1="' +
465
+ formatNumber(x - 3) +
466
+ '" y1="' +
467
+ formatNumber(y + 13) +
468
+ '" x2="' +
469
+ formatNumber(x + 3) +
470
+ '" y2="' +
471
+ formatNumber(y + 13) +
472
+ '" stroke="' +
473
+ stroke +
474
+ '" />'
475
+ )
476
+ }
477
+
478
+ /**
479
+ * Builds the rail power-port linework for one direction.
480
+ * @param {number} x
481
+ * @param {number} y
482
+ * @param {'up' | 'down' | 'left' | 'right'} direction
483
+ * @param {string} color
484
+ * @returns {string}
485
+ */
486
+ static #buildRailLine(x, y, direction, color) {
487
+ const stroke = escapeHtml(color)
488
+ const x2 =
489
+ direction === 'left' ? x - 12 : direction === 'right' ? x + 12 : x
490
+ const y2 =
491
+ direction === 'up' ? y - 12 : direction === 'down' ? y + 12 : y
492
+
493
+ return (
494
+ '<line x1="' +
495
+ formatNumber(x) +
496
+ '" y1="' +
497
+ formatNumber(y) +
498
+ '" x2="' +
499
+ formatNumber(x2) +
500
+ '" y2="' +
501
+ formatNumber(y2) +
502
+ '" stroke="' +
503
+ stroke +
504
+ '" />'
505
+ )
506
+ }
507
+
508
+ /**
509
+ * Places the power-port label beyond the symbol linework.
510
+ * @param {{ text: string }} text
511
+ * @param {'up' | 'down' | 'left' | 'right'} direction
512
+ * @param {number} x
513
+ * @param {number} y
514
+ * @param {number} fontSize
515
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number }} labelOptions
516
+ * @param {string} color
517
+ * @returns {string}
518
+ */
519
+ static #buildDirectionalLabel(
520
+ text,
521
+ direction,
522
+ x,
523
+ y,
524
+ fontSize,
525
+ labelOptions,
526
+ color
527
+ ) {
528
+ if (direction === 'up') {
529
+ return createSvgText(
530
+ 'schematic-power-port-label',
531
+ x,
532
+ y - 16,
533
+ text.text,
534
+ color,
535
+ 'middle',
536
+ labelOptions
537
+ )
538
+ }
539
+
540
+ if (direction === 'right') {
541
+ return createSvgText(
542
+ 'schematic-power-port-label',
543
+ x + 18,
544
+ y + fontSize * 0.36,
545
+ text.text,
546
+ color,
547
+ 'start',
548
+ labelOptions
549
+ )
550
+ }
551
+
552
+ if (direction === 'left') {
553
+ return createSvgText(
554
+ 'schematic-power-port-label',
555
+ x - 18,
556
+ y + fontSize * 0.36,
557
+ text.text,
558
+ color,
559
+ 'end',
560
+ labelOptions
561
+ )
562
+ }
563
+
564
+ return createSvgText(
565
+ 'schematic-power-port-label',
566
+ x,
567
+ y + 25,
568
+ text.text,
569
+ color,
570
+ 'middle',
571
+ labelOptions
572
+ )
573
+ }
574
+ }
@@ -0,0 +1,94 @@
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 { SchematicColorResolver } from './SchematicColorResolver.mjs'
7
+
8
+ const { escapeHtml, formatNumber, projectSchematicY } = SchematicSvgUtils
9
+
10
+ /**
11
+ * Renders authored sheet overlay regions into SVG markup.
12
+ */
13
+ export class SchematicRegionRenderer {
14
+ /**
15
+ * Builds markup for authored sheet overlay regions.
16
+ * @param {{ x: number, y: number, width: number, height: number, fill: string, renderOrder?: number }[]} regions
17
+ * @param {number} sheetHeight
18
+ * @returns {string}
19
+ */
20
+ static buildMarkup(regions, sheetHeight) {
21
+ return regions
22
+ .slice()
23
+ .sort(
24
+ (left, right) =>
25
+ Number(left?.renderOrder || 0) -
26
+ Number(right?.renderOrder || 0)
27
+ )
28
+ .map((region) =>
29
+ SchematicRegionRenderer.#buildRegionMarkup(region, sheetHeight)
30
+ )
31
+ .join('')
32
+ }
33
+
34
+ /**
35
+ * Builds one authored sheet overlay region.
36
+ * @param {{ x: number, y: number, width: number, height: number, fill: string }} region
37
+ * @param {number} sheetHeight
38
+ * @returns {string}
39
+ */
40
+ static #buildRegionMarkup(region, sheetHeight) {
41
+ const x = Number(region?.x || 0)
42
+ const width = Math.max(Number(region?.width || 0), 0)
43
+ const height = Math.max(Number(region?.height || 0), 0)
44
+ const y = projectSchematicY(
45
+ sheetHeight,
46
+ Number(region?.y || 0) + height
47
+ )
48
+ const fill = SchematicColorResolver.resolveFill(
49
+ region?.fill,
50
+ '--schematic-fill-light-color',
51
+ true
52
+ )
53
+ const markerPoints = [
54
+ [x + 4, y + 4],
55
+ [x + 16, y + 4],
56
+ [x + 4, y + 16]
57
+ ]
58
+
59
+ return (
60
+ '<g class="schematic-region">' +
61
+ '<rect class="schematic-region__fill" x="' +
62
+ formatNumber(x) +
63
+ '" y="' +
64
+ formatNumber(y) +
65
+ '" width="' +
66
+ formatNumber(width) +
67
+ '" height="' +
68
+ formatNumber(height) +
69
+ '" fill="' +
70
+ escapeHtml(fill) +
71
+ '" fill-opacity="0.72" />' +
72
+ '<rect class="schematic-region__border" x="' +
73
+ formatNumber(x) +
74
+ '" y="' +
75
+ formatNumber(y) +
76
+ '" width="' +
77
+ formatNumber(width) +
78
+ '" height="' +
79
+ formatNumber(height) +
80
+ '" fill="none" stroke="var(--schematic-alert-color)" stroke-width="1" />' +
81
+ '<polygon class="schematic-region__marker" points="' +
82
+ escapeHtml(
83
+ markerPoints
84
+ .map(
85
+ ([pointX, pointY]) =>
86
+ formatNumber(pointX) + ',' + formatNumber(pointY)
87
+ )
88
+ .join(' ')
89
+ ) +
90
+ '" fill="var(--schematic-alert-color)" fill-opacity="0.25" stroke="var(--schematic-alert-color)" stroke-width="1" />' +
91
+ '</g>'
92
+ )
93
+ }
94
+ }