altium-toolkit 1.1.2 → 1.1.3

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  3. package/src/core/altium/AltiumParser.mjs +240 -8
  4. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  5. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  6. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  7. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  8. package/src/core/altium/SchematicImageParser.mjs +291 -6
  9. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  10. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  11. package/src/core/altium/SchematicPinParser.mjs +175 -4
  12. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  13. package/src/core/altium/SchematicTextParser.mjs +125 -11
  14. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  15. package/src/ui/SchematicColorResolver.mjs +78 -0
  16. package/src/ui/SchematicContentLayout.mjs +58 -1
  17. package/src/ui/SchematicImageRenderer.mjs +125 -10
  18. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  19. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  20. package/src/ui/SchematicNoteRenderer.mjs +82 -6
  21. package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
  22. package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
  23. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  24. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  25. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  26. package/src/ui/SchematicShapeRenderer.mjs +82 -23
  27. package/src/ui/SchematicSvgRenderer.mjs +293 -47
@@ -0,0 +1,631 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds and cleans bitmap masks for recovered power-diagram line artwork.
7
+ */
8
+ export class SchematicPowerDiagramLineMasks {
9
+ /**
10
+ * Builds a mask for dark linework connected to the detected horizontal
11
+ * rail seeds.
12
+ * @param {{ width: number, height: number }} png PNG data.
13
+ * @param {Uint8Array} darkMask Dark source pixels.
14
+ * @param {Uint8Array} seedMask Initial rail pixels.
15
+ * @returns {Uint8Array}
16
+ */
17
+ static buildConnectedLineMask(png, darkMask, seedMask) {
18
+ const connectedMask = new Uint8Array(darkMask.length)
19
+ const gapBridgeMask = new Uint8Array(darkMask.length)
20
+ const queue = new Uint32Array(darkMask.length)
21
+ let readIndex = 0
22
+ let writeIndex = 0
23
+
24
+ for (let index = 0; index < darkMask.length; index += 1) {
25
+ if (!darkMask[index] || !seedMask[index]) continue
26
+
27
+ connectedMask[index] = 1
28
+ queue[writeIndex] = index
29
+ writeIndex += 1
30
+ }
31
+
32
+ let bridged = true
33
+
34
+ while (bridged) {
35
+ while (readIndex < writeIndex) {
36
+ const index = queue[readIndex]
37
+ readIndex += 1
38
+
39
+ const x = index % png.width
40
+ const y = Math.floor(index / png.width)
41
+ writeIndex =
42
+ SchematicPowerDiagramLineMasks.#appendConnectedNeighbors(
43
+ darkMask,
44
+ connectedMask,
45
+ queue,
46
+ png.width,
47
+ png.height,
48
+ x,
49
+ y,
50
+ writeIndex
51
+ )
52
+ }
53
+
54
+ const previousWriteIndex = writeIndex
55
+ writeIndex =
56
+ SchematicPowerDiagramLineMasks.#appendHorizontalGapRuns(
57
+ darkMask,
58
+ connectedMask,
59
+ gapBridgeMask,
60
+ queue,
61
+ png.width,
62
+ png.height,
63
+ writeIndex
64
+ )
65
+ writeIndex = SchematicPowerDiagramLineMasks.#appendVerticalGapRuns(
66
+ darkMask,
67
+ connectedMask,
68
+ gapBridgeMask,
69
+ queue,
70
+ png.width,
71
+ png.height,
72
+ writeIndex
73
+ )
74
+ bridged = writeIndex > previousWriteIndex
75
+ }
76
+
77
+ return connectedMask
78
+ }
79
+
80
+ /**
81
+ * Appends dark horizontal dash runs that are separated from connected
82
+ * linework only by a small same-row gap.
83
+ * @param {Uint8Array} darkMask Dark source pixels.
84
+ * @param {Uint8Array} connectedMask Connected pixels.
85
+ * @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
86
+ * @param {Uint32Array} queue Shared queue.
87
+ * @param {number} width Image width.
88
+ * @param {number} height Image height.
89
+ * @param {number} writeIndex Queue write index.
90
+ * @returns {number}
91
+ */
92
+ static #appendHorizontalGapRuns(
93
+ darkMask,
94
+ connectedMask,
95
+ gapBridgeMask,
96
+ queue,
97
+ width,
98
+ height,
99
+ writeIndex
100
+ ) {
101
+ const maximumGap = Math.max(8, Math.round(width * 0.015))
102
+ const minimumRun = Math.max(12, Math.round(width * 0.005))
103
+ const maximumRun = Math.max(18, Math.round(width * 0.035))
104
+
105
+ for (let y = 0; y < height; y += 1) {
106
+ const runs = SchematicPowerDiagramLineMasks.#collectHorizontalRuns(
107
+ darkMask,
108
+ connectedMask,
109
+ width,
110
+ y
111
+ )
112
+
113
+ for (let index = 0; index < runs.length; index += 1) {
114
+ if (!runs[index].connected) continue
115
+
116
+ writeIndex = SchematicPowerDiagramLineMasks.#appendGapRun(
117
+ connectedMask,
118
+ gapBridgeMask,
119
+ queue,
120
+ width,
121
+ y,
122
+ runs[index],
123
+ runs[index + 1],
124
+ maximumGap,
125
+ minimumRun,
126
+ maximumRun,
127
+ writeIndex
128
+ )
129
+ writeIndex = SchematicPowerDiagramLineMasks.#appendGapRun(
130
+ connectedMask,
131
+ gapBridgeMask,
132
+ queue,
133
+ width,
134
+ y,
135
+ runs[index],
136
+ runs[index - 1],
137
+ maximumGap,
138
+ minimumRun,
139
+ maximumRun,
140
+ writeIndex
141
+ )
142
+ }
143
+ }
144
+
145
+ return writeIndex
146
+ }
147
+
148
+ /**
149
+ * Appends dark vertical dash runs that are separated from connected
150
+ * linework only by a small same-column gap.
151
+ * @param {Uint8Array} darkMask Dark source pixels.
152
+ * @param {Uint8Array} connectedMask Connected pixels.
153
+ * @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
154
+ * @param {Uint32Array} queue Shared queue.
155
+ * @param {number} width Image width.
156
+ * @param {number} height Image height.
157
+ * @param {number} writeIndex Queue write index.
158
+ * @returns {number}
159
+ */
160
+ static #appendVerticalGapRuns(
161
+ darkMask,
162
+ connectedMask,
163
+ gapBridgeMask,
164
+ queue,
165
+ width,
166
+ height,
167
+ writeIndex
168
+ ) {
169
+ const maximumGap = Math.max(8, Math.round(height * 0.015))
170
+ const minimumRun = Math.max(12, Math.round(height * 0.005))
171
+ const maximumRun = Math.max(24, Math.round(height * 0.06))
172
+
173
+ for (let x = 0; x < width; x += 1) {
174
+ const runs = SchematicPowerDiagramLineMasks.#collectVerticalRuns(
175
+ darkMask,
176
+ gapBridgeMask,
177
+ width,
178
+ height,
179
+ x
180
+ )
181
+
182
+ for (let index = 0; index < runs.length; index += 1) {
183
+ if (!runs[index].connected) continue
184
+
185
+ writeIndex = SchematicPowerDiagramLineMasks.#appendColumnGapRun(
186
+ connectedMask,
187
+ gapBridgeMask,
188
+ queue,
189
+ width,
190
+ x,
191
+ runs[index],
192
+ runs[index + 1],
193
+ maximumGap,
194
+ minimumRun,
195
+ maximumRun,
196
+ writeIndex
197
+ )
198
+ writeIndex = SchematicPowerDiagramLineMasks.#appendColumnGapRun(
199
+ connectedMask,
200
+ gapBridgeMask,
201
+ queue,
202
+ width,
203
+ x,
204
+ runs[index],
205
+ runs[index - 1],
206
+ maximumGap,
207
+ minimumRun,
208
+ maximumRun,
209
+ writeIndex
210
+ )
211
+ }
212
+ }
213
+
214
+ return writeIndex
215
+ }
216
+
217
+ /**
218
+ * Collects dark runs on one row.
219
+ * @param {Uint8Array} darkMask Dark source pixels.
220
+ * @param {Uint8Array} connectedMask Connected pixels.
221
+ * @param {number} width Image width.
222
+ * @param {number} y Row.
223
+ * @returns {{ start: number, end: number, connected: boolean }[]}
224
+ */
225
+ static #collectHorizontalRuns(darkMask, connectedMask, width, y) {
226
+ const runs = []
227
+ let x = 0
228
+
229
+ while (x < width) {
230
+ while (x < width && !darkMask[y * width + x]) x += 1
231
+ if (x >= width) break
232
+
233
+ const start = x
234
+ let connected = false
235
+
236
+ while (x < width && darkMask[y * width + x]) {
237
+ if (connectedMask[y * width + x]) connected = true
238
+ x += 1
239
+ }
240
+
241
+ runs.push({ start, end: x - 1, connected })
242
+ }
243
+
244
+ return runs
245
+ }
246
+
247
+ /**
248
+ * Collects dark runs on one column.
249
+ * @param {Uint8Array} darkMask Dark source pixels.
250
+ * @param {Uint8Array} connectedMask Connected pixels.
251
+ * @param {number} width Image width.
252
+ * @param {number} height Image height.
253
+ * @param {number} x Column.
254
+ * @returns {{ start: number, end: number, connected: boolean }[]}
255
+ */
256
+ static #collectVerticalRuns(darkMask, connectedMask, width, height, x) {
257
+ const runs = []
258
+ let y = 0
259
+
260
+ while (y < height) {
261
+ while (y < height && !darkMask[y * width + x]) y += 1
262
+ if (y >= height) break
263
+
264
+ const start = y
265
+ let connected = false
266
+
267
+ while (y < height && darkMask[y * width + x]) {
268
+ if (connectedMask[y * width + x]) connected = true
269
+ y += 1
270
+ }
271
+
272
+ runs.push({ start, end: y - 1, connected })
273
+ }
274
+
275
+ return runs
276
+ }
277
+
278
+ /**
279
+ * Appends one target run when it is close enough to a connected source run.
280
+ * @param {Uint8Array} connectedMask Connected pixels.
281
+ * @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
282
+ * @param {Uint32Array} queue Shared queue.
283
+ * @param {number} width Image width.
284
+ * @param {number} y Row.
285
+ * @param {{ start: number, end: number }} source Source run.
286
+ * @param {{ start: number, end: number, connected: boolean } | undefined} target Target run.
287
+ * @param {number} maximumGap Maximum bridge gap.
288
+ * @param {number} minimumRun Minimum target run length.
289
+ * @param {number} maximumRun Maximum target run length.
290
+ * @param {number} writeIndex Queue write index.
291
+ * @returns {number}
292
+ */
293
+ static #appendGapRun(
294
+ connectedMask,
295
+ gapBridgeMask,
296
+ queue,
297
+ width,
298
+ y,
299
+ source,
300
+ target,
301
+ maximumGap,
302
+ minimumRun,
303
+ maximumRun,
304
+ writeIndex
305
+ ) {
306
+ if (!target || target.connected) return writeIndex
307
+
308
+ const gap =
309
+ target.start > source.end
310
+ ? target.start - source.end - 1
311
+ : source.start - target.end - 1
312
+ const runLength = target.end - target.start + 1
313
+
314
+ if (
315
+ gap > maximumGap ||
316
+ runLength < minimumRun ||
317
+ runLength > maximumRun
318
+ ) {
319
+ return writeIndex
320
+ }
321
+
322
+ for (let x = target.start; x <= target.end; x += 1) {
323
+ const targetIndex = y * width + x
324
+ if (connectedMask[targetIndex]) continue
325
+
326
+ connectedMask[targetIndex] = 1
327
+ gapBridgeMask[targetIndex] = 1
328
+ queue[writeIndex] = targetIndex
329
+ writeIndex += 1
330
+ }
331
+
332
+ return writeIndex
333
+ }
334
+
335
+ /**
336
+ * Appends one vertical target run when it is close enough to a connected
337
+ * source run.
338
+ * @param {Uint8Array} connectedMask Connected pixels.
339
+ * @param {Uint8Array} gapBridgeMask Gap-bridged line pixels.
340
+ * @param {Uint32Array} queue Shared queue.
341
+ * @param {number} width Image width.
342
+ * @param {number} x Column.
343
+ * @param {{ start: number, end: number }} source Source run.
344
+ * @param {{ start: number, end: number, connected: boolean } | undefined} target Target run.
345
+ * @param {number} maximumGap Maximum bridge gap.
346
+ * @param {number} minimumRun Minimum target run length.
347
+ * @param {number} maximumRun Maximum target run length.
348
+ * @param {number} writeIndex Queue write index.
349
+ * @returns {number}
350
+ */
351
+ static #appendColumnGapRun(
352
+ connectedMask,
353
+ gapBridgeMask,
354
+ queue,
355
+ width,
356
+ x,
357
+ source,
358
+ target,
359
+ maximumGap,
360
+ minimumRun,
361
+ maximumRun,
362
+ writeIndex
363
+ ) {
364
+ if (!target || target.connected) return writeIndex
365
+
366
+ const gap =
367
+ target.start > source.end
368
+ ? target.start - source.end - 1
369
+ : source.start - target.end - 1
370
+ const runLength = target.end - target.start + 1
371
+
372
+ if (
373
+ gap > maximumGap ||
374
+ runLength < minimumRun ||
375
+ runLength > maximumRun
376
+ ) {
377
+ return writeIndex
378
+ }
379
+
380
+ for (let y = target.start; y <= target.end; y += 1) {
381
+ const targetIndex = y * width + x
382
+ if (connectedMask[targetIndex]) continue
383
+
384
+ connectedMask[targetIndex] = 1
385
+ gapBridgeMask[targetIndex] = 1
386
+ queue[writeIndex] = targetIndex
387
+ writeIndex += 1
388
+ }
389
+
390
+ return writeIndex
391
+ }
392
+
393
+ /**
394
+ * Removes tall, narrow orange components that are more likely to be
395
+ * ordinary wire fragments than power-port labels or caps.
396
+ * @param {Uint8Array} orangeMask Orange candidate pixels.
397
+ * @param {number} width Image width.
398
+ * @param {number} height Image height.
399
+ */
400
+ static removeNarrowLineComponents(orangeMask, width, height) {
401
+ const seen = new Uint8Array(orangeMask.length)
402
+ const queue = new Uint32Array(orangeMask.length)
403
+ const maximumNarrowWidth = Math.max(8, Math.round(width * 0.0024))
404
+ const minimumTallHeight = Math.max(20, Math.round(height * 0.02))
405
+
406
+ for (let index = 0; index < orangeMask.length; index += 1) {
407
+ if (!orangeMask[index] || seen[index]) continue
408
+
409
+ const component = SchematicPowerDiagramLineMasks.#readComponent(
410
+ orangeMask,
411
+ seen,
412
+ queue,
413
+ width,
414
+ height,
415
+ index
416
+ )
417
+
418
+ if (
419
+ component.width <= maximumNarrowWidth &&
420
+ component.height >= minimumTallHeight &&
421
+ !SchematicPowerDiagramLineMasks.#hasDenseOrangeContext(
422
+ orangeMask,
423
+ width,
424
+ height,
425
+ component
426
+ )
427
+ ) {
428
+ for (let cursor = 0; cursor < component.length; cursor += 1) {
429
+ orangeMask[queue[cursor]] = 0
430
+ }
431
+ }
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Appends all eight-connected dark neighbors.
437
+ * @param {Uint8Array} darkMask Dark source pixels.
438
+ * @param {Uint8Array} connectedMask Connected pixels.
439
+ * @param {Uint32Array} queue Shared queue.
440
+ * @param {number} width Image width.
441
+ * @param {number} height Image height.
442
+ * @param {number} x Pixel x.
443
+ * @param {number} y Pixel y.
444
+ * @param {number} writeIndex Queue write index.
445
+ * @returns {number}
446
+ */
447
+ static #appendConnectedNeighbors(
448
+ darkMask,
449
+ connectedMask,
450
+ queue,
451
+ width,
452
+ height,
453
+ x,
454
+ y,
455
+ writeIndex
456
+ ) {
457
+ for (let dy = -1; dy <= 1; dy += 1) {
458
+ const nextY = y + dy
459
+ if (nextY < 0 || nextY >= height) continue
460
+
461
+ for (let dx = -1; dx <= 1; dx += 1) {
462
+ if (dx === 0 && dy === 0) continue
463
+
464
+ const nextX = x + dx
465
+ if (nextX < 0 || nextX >= width) continue
466
+
467
+ const nextIndex = nextY * width + nextX
468
+ if (!darkMask[nextIndex] || connectedMask[nextIndex]) {
469
+ continue
470
+ }
471
+
472
+ connectedMask[nextIndex] = 1
473
+ queue[writeIndex] = nextIndex
474
+ writeIndex += 1
475
+ }
476
+ }
477
+
478
+ return writeIndex
479
+ }
480
+
481
+ /**
482
+ * Reads one four-connected mask component into the shared queue.
483
+ * @param {Uint8Array} mask Source mask.
484
+ * @param {Uint8Array} seen Visited pixels.
485
+ * @param {Uint32Array} queue Shared queue.
486
+ * @param {number} width Image width.
487
+ * @param {number} height Image height.
488
+ * @param {number} startIndex Component seed.
489
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number, length: number }}
490
+ */
491
+ static #readComponent(mask, seen, queue, width, height, startIndex) {
492
+ let readIndex = 0
493
+ let writeIndex = 0
494
+ let minX = width
495
+ let minY = height
496
+ let maxX = 0
497
+ let maxY = 0
498
+
499
+ seen[startIndex] = 1
500
+ queue[writeIndex] = startIndex
501
+ writeIndex += 1
502
+
503
+ while (readIndex < writeIndex) {
504
+ const index = queue[readIndex]
505
+ readIndex += 1
506
+
507
+ const x = index % width
508
+ const y = Math.floor(index / width)
509
+ minX = Math.min(minX, x)
510
+ minY = Math.min(minY, y)
511
+ maxX = Math.max(maxX, x)
512
+ maxY = Math.max(maxY, y)
513
+
514
+ writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
515
+ mask,
516
+ seen,
517
+ queue,
518
+ width,
519
+ height,
520
+ x - 1,
521
+ y,
522
+ writeIndex
523
+ )
524
+ writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
525
+ mask,
526
+ seen,
527
+ queue,
528
+ width,
529
+ height,
530
+ x + 1,
531
+ y,
532
+ writeIndex
533
+ )
534
+ writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
535
+ mask,
536
+ seen,
537
+ queue,
538
+ width,
539
+ height,
540
+ x,
541
+ y - 1,
542
+ writeIndex
543
+ )
544
+ writeIndex = SchematicPowerDiagramLineMasks.#appendComponentPixel(
545
+ mask,
546
+ seen,
547
+ queue,
548
+ width,
549
+ height,
550
+ x,
551
+ y + 1,
552
+ writeIndex
553
+ )
554
+ }
555
+
556
+ return {
557
+ minX,
558
+ minY,
559
+ maxX,
560
+ maxY,
561
+ width: maxX - minX + 1,
562
+ height: maxY - minY + 1,
563
+ length: writeIndex
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Appends one component neighbor when it is in-bounds and unvisited.
569
+ * @param {Uint8Array} mask Source mask.
570
+ * @param {Uint8Array} seen Visited pixels.
571
+ * @param {Uint32Array} queue Shared queue.
572
+ * @param {number} width Image width.
573
+ * @param {number} height Image height.
574
+ * @param {number} x Pixel x.
575
+ * @param {number} y Pixel y.
576
+ * @param {number} writeIndex Queue write index.
577
+ * @returns {number}
578
+ */
579
+ static #appendComponentPixel(
580
+ mask,
581
+ seen,
582
+ queue,
583
+ width,
584
+ height,
585
+ x,
586
+ y,
587
+ writeIndex
588
+ ) {
589
+ if (x < 0 || x >= width || y < 0 || y >= height) return writeIndex
590
+
591
+ const index = y * width + x
592
+ if (!mask[index] || seen[index]) return writeIndex
593
+
594
+ seen[index] = 1
595
+ queue[writeIndex] = index
596
+
597
+ return writeIndex + 1
598
+ }
599
+
600
+ /**
601
+ * Returns true when a narrow component sits inside a dense orange word or
602
+ * cap context.
603
+ * @param {Uint8Array} orangeMask Orange candidate pixels.
604
+ * @param {number} width Image width.
605
+ * @param {number} height Image height.
606
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} component Component bounds.
607
+ * @returns {boolean}
608
+ */
609
+ static #hasDenseOrangeContext(orangeMask, width, height, component) {
610
+ const contextX = Math.max(24, Math.round(width * 0.045))
611
+ const contextY = Math.max(8, Math.round(height * 0.015))
612
+ const minimumDenseRowPixels = Math.max(16, Math.round(width * 0.003))
613
+ const left = Math.max(0, component.minX - contextX)
614
+ const right = Math.min(width - 1, component.maxX + contextX)
615
+ const top = Math.max(0, component.minY - contextY)
616
+ const bottom = Math.min(height - 1, component.maxY + contextY)
617
+
618
+ for (let y = top; y <= bottom; y += 1) {
619
+ let rowPixels = 0
620
+ const rowOffset = y * width
621
+
622
+ for (let x = left; x <= right; x += 1) {
623
+ if (orangeMask[rowOffset + x]) rowPixels += 1
624
+ }
625
+
626
+ if (rowPixels >= minimumDenseRowPixels) return true
627
+ }
628
+
629
+ return false
630
+ }
631
+ }
@@ -36,7 +36,7 @@ export class SchematicPowerPortRenderer {
36
36
  fontStyle: text.fontStyle
37
37
  })
38
38
  const fontSize = Number(labelOptions.fontSize || 9)
39
- const resolvedColor = SchematicColorResolver.resolveColor(
39
+ const resolvedColor = SchematicColorResolver.resolveNonTextColor(
40
40
  text.color,
41
41
  '--schematic-power-color'
42
42
  )