altium-toolkit 1.1.3 → 1.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +5 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +87 -20
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +37 -11
- package/src/ui/SchematicSvgRenderer.mjs +944 -51
|
@@ -11,7 +11,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
11
11
|
* Resolves one native-facing pin text placement in renderer coordinates
|
|
12
12
|
* before sheet Y projection. The returned `yOffset` is applied after
|
|
13
13
|
* projection, matching SVG text baseline behavior.
|
|
14
|
-
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number }} pin
|
|
14
|
+
* @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number, electrical?: number }} pin
|
|
15
15
|
* @param {'name' | 'number'} labelKind
|
|
16
16
|
* @param {{ labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', rotateTopNumber?: boolean }} [options]
|
|
17
17
|
* @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
|
|
@@ -19,7 +19,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
19
19
|
static resolveNativePinTextPlacement(pin, labelKind, options = {}) {
|
|
20
20
|
const labelMode = options.labelMode || 'name-and-number'
|
|
21
21
|
const markerStyle =
|
|
22
|
-
SchematicOwnerPinLabelLayout.#
|
|
22
|
+
SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
|
|
23
23
|
|
|
24
24
|
if (labelKind === 'number') {
|
|
25
25
|
return SchematicOwnerPinLabelLayout.#resolveNumberPlacement(
|
|
@@ -200,6 +200,67 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
200
200
|
return boxes
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Collects internally numbered pins whose external number would overlap a
|
|
205
|
+
* visible route text label on the same horizontal lane.
|
|
206
|
+
* @param {{ ownerIndex?: string, name?: string, designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', electrical?: number, symbolOuter?: number }[]} pins
|
|
207
|
+
* @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number }[]} texts
|
|
208
|
+
* @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
|
|
209
|
+
* @returns {Set<string>}
|
|
210
|
+
*/
|
|
211
|
+
static collectOverlappingExternalNumberLabelKeys(
|
|
212
|
+
pins,
|
|
213
|
+
texts,
|
|
214
|
+
internalNumberLabelBoxes
|
|
215
|
+
) {
|
|
216
|
+
const textBounds = (texts || [])
|
|
217
|
+
.map((text) =>
|
|
218
|
+
SchematicOwnerPinLabelLayout.#estimateHorizontalTextBounds(text)
|
|
219
|
+
)
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
const keys = new Set()
|
|
222
|
+
|
|
223
|
+
if (textBounds.length === 0) {
|
|
224
|
+
return keys
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const pin of pins || []) {
|
|
228
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
229
|
+
if (!ownerIndex || !internalNumberLabelBoxes.has(ownerIndex)) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!SchematicOwnerPinLabelLayout.#isInternalNumberLabelPin(pin)) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const numberBounds =
|
|
238
|
+
SchematicOwnerPinLabelLayout.#estimateExternalNumberBounds(pin)
|
|
239
|
+
if (!numberBounds) {
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
textBounds.some((bounds) =>
|
|
245
|
+
SchematicOwnerPinLabelLayout.#boundsOverlap(
|
|
246
|
+
numberBounds,
|
|
247
|
+
bounds,
|
|
248
|
+
1.5
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
) {
|
|
252
|
+
keys.add(
|
|
253
|
+
SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
|
|
254
|
+
ownerIndex,
|
|
255
|
+
pin.designator
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return keys
|
|
262
|
+
}
|
|
263
|
+
|
|
203
264
|
/**
|
|
204
265
|
* Resolves the final SVG text anchor for one schematic free-text label.
|
|
205
266
|
* Mirrored rotated owner pin-name labels need the opposite text edge so
|
|
@@ -455,6 +516,133 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
455
516
|
return /^(?:[DS]|[GS]\d*)$/i.test(String(name || '').trim())
|
|
456
517
|
}
|
|
457
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Estimates a route text label's source-coordinate visual bounds.
|
|
521
|
+
* @param {{ ownerIndex?: string, text?: string, resolvedText?: string, x?: number, y?: number, anchor?: 'start' | 'middle' | 'end', fontSize?: number, hidden?: boolean, rotation?: number } | null} text
|
|
522
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
|
|
523
|
+
*/
|
|
524
|
+
static #estimateHorizontalTextBounds(text) {
|
|
525
|
+
if (!text || text.hidden || text.ownerIndex) return null
|
|
526
|
+
if (Number(text.rotation || 0) !== 0) return null
|
|
527
|
+
|
|
528
|
+
const label = String(text.resolvedText ?? text.text ?? '').trim()
|
|
529
|
+
const x = Number(text.x)
|
|
530
|
+
const y = Number(text.y)
|
|
531
|
+
if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
532
|
+
|
|
533
|
+
const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize(
|
|
534
|
+
text.fontSize
|
|
535
|
+
)
|
|
536
|
+
const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
|
|
537
|
+
label,
|
|
538
|
+
fontSize
|
|
539
|
+
)
|
|
540
|
+
const anchor = text.anchor || 'start'
|
|
541
|
+
const minX =
|
|
542
|
+
anchor === 'end'
|
|
543
|
+
? x - width
|
|
544
|
+
: anchor === 'middle'
|
|
545
|
+
? x - width / 2
|
|
546
|
+
: x
|
|
547
|
+
const maxX =
|
|
548
|
+
anchor === 'end'
|
|
549
|
+
? x
|
|
550
|
+
: anchor === 'middle'
|
|
551
|
+
? x + width / 2
|
|
552
|
+
: x + width
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
minX,
|
|
556
|
+
maxX,
|
|
557
|
+
minY: y - fontSize * 0.7,
|
|
558
|
+
maxY: y + fontSize * 0.35
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Estimates one external pin number's source-coordinate bounds.
|
|
564
|
+
* @param {{ designator?: string, length?: number, x: number, y: number, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number }} pin
|
|
565
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number } | null}
|
|
566
|
+
*/
|
|
567
|
+
static #estimateExternalNumberBounds(pin) {
|
|
568
|
+
if (pin.orientation !== 'left' && pin.orientation !== 'right') {
|
|
569
|
+
return null
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const label = String(pin.designator || '').trim()
|
|
573
|
+
const x = Number(pin.x)
|
|
574
|
+
const y = Number(pin.y)
|
|
575
|
+
if (!label || !Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
576
|
+
|
|
577
|
+
const markerStyle =
|
|
578
|
+
SchematicOwnerPinLabelLayout.#resolvePinMarkerStyle(pin)
|
|
579
|
+
const clearance =
|
|
580
|
+
SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberClearance(
|
|
581
|
+
markerStyle,
|
|
582
|
+
pin
|
|
583
|
+
)
|
|
584
|
+
const offset = Math.max(
|
|
585
|
+
clearance,
|
|
586
|
+
SchematicOwnerPinLabelLayout.#resolveCompactExternalHorizontalNumberOffset(
|
|
587
|
+
pin
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
const numberX = pin.orientation === 'left' ? x - offset : x + offset
|
|
591
|
+
const anchor =
|
|
592
|
+
SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
593
|
+
pin,
|
|
594
|
+
markerStyle
|
|
595
|
+
)
|
|
596
|
+
const fontSize = SchematicOwnerPinLabelLayout.#resolveViewerFontSize()
|
|
597
|
+
const width = SchematicOwnerPinLabelLayout.#estimateTextWidth(
|
|
598
|
+
label,
|
|
599
|
+
fontSize
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
minX: anchor === 'end' ? numberX - width : numberX,
|
|
604
|
+
maxX: anchor === 'end' ? numberX : numberX + width,
|
|
605
|
+
minY: y - fontSize * 0.7,
|
|
606
|
+
maxY: y + fontSize * 0.35
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Returns the viewer-adjusted schematic font size used for collision
|
|
612
|
+
* estimates.
|
|
613
|
+
* @param {number | undefined} fontSize Source font size.
|
|
614
|
+
* @returns {number}
|
|
615
|
+
*/
|
|
616
|
+
static #resolveViewerFontSize(fontSize = 10) {
|
|
617
|
+
return Math.max(Number(fontSize || 10) - 1, 6)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Estimates one rendered text run width.
|
|
622
|
+
* @param {string} text Text content.
|
|
623
|
+
* @param {number} fontSize Viewer font size.
|
|
624
|
+
* @returns {number}
|
|
625
|
+
*/
|
|
626
|
+
static #estimateTextWidth(text, fontSize) {
|
|
627
|
+
return Math.max(String(text || '').length * fontSize * 0.62, fontSize)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Returns true when two source-coordinate boxes overlap.
|
|
632
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} first First bounds.
|
|
633
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} second Second bounds.
|
|
634
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
635
|
+
* @returns {boolean}
|
|
636
|
+
*/
|
|
637
|
+
static #boundsOverlap(first, second, tolerance) {
|
|
638
|
+
return (
|
|
639
|
+
first.minX <= second.maxX + tolerance &&
|
|
640
|
+
first.maxX >= second.minX - tolerance &&
|
|
641
|
+
first.minY <= second.maxY + tolerance &&
|
|
642
|
+
first.maxY >= second.minY - tolerance
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
458
646
|
/**
|
|
459
647
|
* Moves left/right pin numbers outward by the same horizontal correction
|
|
460
648
|
* already applied to their explicit owner pin-name labels.
|
|
@@ -506,7 +694,10 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
506
694
|
pin
|
|
507
695
|
),
|
|
508
696
|
yOffset: -1,
|
|
509
|
-
anchor:
|
|
697
|
+
anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
698
|
+
pin,
|
|
699
|
+
markerStyle
|
|
700
|
+
),
|
|
510
701
|
rotation: 0
|
|
511
702
|
}
|
|
512
703
|
case 'right':
|
|
@@ -518,7 +709,10 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
518
709
|
pin
|
|
519
710
|
),
|
|
520
711
|
yOffset: -1,
|
|
521
|
-
anchor:
|
|
712
|
+
anchor: SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberAnchor(
|
|
713
|
+
pin,
|
|
714
|
+
markerStyle
|
|
715
|
+
),
|
|
522
716
|
rotation: 0
|
|
523
717
|
}
|
|
524
718
|
case 'top':
|
|
@@ -594,23 +788,45 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
594
788
|
/**
|
|
595
789
|
* Returns the horizontal pin-number clearance needed by the pin geometry.
|
|
596
790
|
* @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
|
|
597
|
-
* @param {{ length?: number }} pin
|
|
791
|
+
* @param {{ length?: number, electrical?: number }} pin
|
|
598
792
|
* @returns {number}
|
|
599
793
|
*/
|
|
600
794
|
static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
|
|
601
795
|
switch (markerStyle) {
|
|
602
796
|
case 'double':
|
|
603
|
-
return
|
|
797
|
+
return 21
|
|
604
798
|
case 'cross':
|
|
605
|
-
return
|
|
799
|
+
return 9
|
|
606
800
|
case 'single-in':
|
|
607
801
|
case 'single-out':
|
|
608
802
|
return 8
|
|
609
803
|
default:
|
|
804
|
+
if (Number(pin?.electrical || 0) === 1) {
|
|
805
|
+
return 16
|
|
806
|
+
}
|
|
807
|
+
|
|
610
808
|
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 2)
|
|
611
809
|
}
|
|
612
810
|
}
|
|
613
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Resolves the text edge used for horizontal pin numbers.
|
|
814
|
+
* @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin Pin primitive.
|
|
815
|
+
* @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle Marker style.
|
|
816
|
+
* @returns {'start' | 'end'}
|
|
817
|
+
*/
|
|
818
|
+
static #resolveHorizontalPinNumberAnchor(pin, markerStyle) {
|
|
819
|
+
const routeFacing =
|
|
820
|
+
markerStyle === 'double' ||
|
|
821
|
+
(!markerStyle && Number(pin?.electrical || 0) === 1)
|
|
822
|
+
|
|
823
|
+
if (routeFacing) {
|
|
824
|
+
return pin.orientation === 'left' ? 'start' : 'end'
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return pin.orientation === 'left' ? 'end' : 'start'
|
|
828
|
+
}
|
|
829
|
+
|
|
614
830
|
/**
|
|
615
831
|
* Returns the horizontal pin-name inset used inside the symbol body.
|
|
616
832
|
* @param {{ length?: number }} pin
|
|
@@ -622,7 +838,7 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
622
838
|
return 10
|
|
623
839
|
}
|
|
624
840
|
|
|
625
|
-
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin,
|
|
841
|
+
return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 7)
|
|
626
842
|
}
|
|
627
843
|
|
|
628
844
|
/**
|
|
@@ -641,6 +857,18 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
641
857
|
return fallback === 2 ? 10 : 8
|
|
642
858
|
}
|
|
643
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Resolves the stub-side offset used when a pin already has an internal
|
|
862
|
+
* numeric label in its owner body.
|
|
863
|
+
* @param {{ length?: number }} pin Pin primitive.
|
|
864
|
+
* @returns {number}
|
|
865
|
+
*/
|
|
866
|
+
static #resolveCompactExternalHorizontalNumberOffset(pin) {
|
|
867
|
+
const length = Math.abs(Number(pin.length || 0))
|
|
868
|
+
|
|
869
|
+
return Math.max(8, Math.min(length - 6, 12))
|
|
870
|
+
}
|
|
871
|
+
|
|
644
872
|
/**
|
|
645
873
|
* Resolves one authored outer pin marker style from the stored symbol flag.
|
|
646
874
|
* @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
@@ -665,4 +893,37 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
665
893
|
return null
|
|
666
894
|
}
|
|
667
895
|
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Resolves the marker style that contributes to native text clearance.
|
|
899
|
+
* @param {{ symbolOuter?: number, electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
900
|
+
* @returns {'single-in' | 'single-out' | 'double' | 'cross' | null}
|
|
901
|
+
*/
|
|
902
|
+
static #resolvePinMarkerStyle(pin) {
|
|
903
|
+
return (
|
|
904
|
+
SchematicOwnerPinLabelLayout.#resolveOuterPinMarkerStyle(pin) ||
|
|
905
|
+
SchematicOwnerPinLabelLayout.#resolveElectricalPinMarkerStyle(pin)
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Resolves electrical pin marker styles. Bidirectional pins keep the
|
|
911
|
+
* existing route-facing number placement through the explicit check.
|
|
912
|
+
* @param {{ electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
913
|
+
* @returns {'single-in' | 'single-out' | null}
|
|
914
|
+
*/
|
|
915
|
+
static #resolveElectricalPinMarkerStyle(pin) {
|
|
916
|
+
if (pin.orientation !== 'left' && pin.orientation !== 'right') {
|
|
917
|
+
return null
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
switch (Number(pin.electrical)) {
|
|
921
|
+
case 0:
|
|
922
|
+
return 'single-in'
|
|
923
|
+
case 2:
|
|
924
|
+
return 'single-out'
|
|
925
|
+
default:
|
|
926
|
+
return null
|
|
927
|
+
}
|
|
928
|
+
}
|
|
668
929
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
const COMPACT_PIN_MAX_LENGTH = 15
|
|
6
|
+
const MARKER_LINE_BASE_TOLERANCE = 6
|
|
7
|
+
const MARKER_LINE_BASE_MAX_LENGTH = 8
|
|
8
|
+
const MARKER_LINE_COLOR = 'var(--schematic-accent-ink-color)'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes compact owner-authored marker strokes around hidden pin stubs.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicOwnerPinMarkerLineThemer {
|
|
14
|
+
/**
|
|
15
|
+
* Applies schematic pin color to compact owner marker strokes.
|
|
16
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }[]} lines Line primitives.
|
|
17
|
+
* @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string }[]} pins Pin primitives.
|
|
18
|
+
* @returns {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }[]}
|
|
19
|
+
*/
|
|
20
|
+
static theme(lines, pins) {
|
|
21
|
+
const anchorsByOwner =
|
|
22
|
+
SchematicOwnerPinMarkerLineThemer.#buildCompactPinAnchors(pins)
|
|
23
|
+
|
|
24
|
+
if (anchorsByOwner.size === 0) {
|
|
25
|
+
return lines || []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (lines || []).map((line) =>
|
|
29
|
+
SchematicOwnerPinMarkerLineThemer.#isCompactMarkerLine(
|
|
30
|
+
line,
|
|
31
|
+
anchorsByOwner
|
|
32
|
+
)
|
|
33
|
+
? { ...line, color: MARKER_LINE_COLOR }
|
|
34
|
+
: line
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds lookup anchors for compact unnamed owner pins.
|
|
40
|
+
* @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string }[]} pins Pin primitives.
|
|
41
|
+
* @returns {Map<string, { x: number, y: number, tolerance: number, maxLineLength: number }[]>}
|
|
42
|
+
*/
|
|
43
|
+
static #buildCompactPinAnchors(pins) {
|
|
44
|
+
const anchorsByOwner = new Map()
|
|
45
|
+
|
|
46
|
+
for (const pin of pins || []) {
|
|
47
|
+
if (!SchematicOwnerPinMarkerLineThemer.#isCompactMarkerPin(pin)) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
52
|
+
const length = Math.abs(Number(pin.length || 0))
|
|
53
|
+
const anchor = {
|
|
54
|
+
x: Number(pin.x),
|
|
55
|
+
y: Number(pin.y),
|
|
56
|
+
tolerance: Math.max(MARKER_LINE_BASE_TOLERANCE, length * 0.6),
|
|
57
|
+
maxLineLength: Math.max(
|
|
58
|
+
MARKER_LINE_BASE_MAX_LENGTH,
|
|
59
|
+
length * 0.8
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!anchorsByOwner.has(ownerIndex)) {
|
|
64
|
+
anchorsByOwner.set(ownerIndex, [])
|
|
65
|
+
}
|
|
66
|
+
anchorsByOwner.get(ownerIndex).push(anchor)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return anchorsByOwner
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns true for compact unnamed owner pins that are marker anchors.
|
|
74
|
+
* @param {{ x?: number, y?: number, length?: number, name?: string, designator?: string, electrical?: number, ownerIndex?: string } | null | undefined} pin Pin primitive.
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
static #isCompactMarkerPin(pin) {
|
|
78
|
+
const ownerIndex = String(pin?.ownerIndex || '').trim()
|
|
79
|
+
const name = String(pin?.name || '').trim()
|
|
80
|
+
const designator = String(pin?.designator || '').trim()
|
|
81
|
+
const length = Math.abs(Number(pin?.length || 0))
|
|
82
|
+
const electrical = Number(pin?.electrical)
|
|
83
|
+
const x = Number(pin?.x)
|
|
84
|
+
const y = Number(pin?.y)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
Boolean(ownerIndex) &&
|
|
88
|
+
!name &&
|
|
89
|
+
/^\d+$/.test(designator) &&
|
|
90
|
+
Number.isFinite(x) &&
|
|
91
|
+
Number.isFinite(y) &&
|
|
92
|
+
length > 0 &&
|
|
93
|
+
length <= COMPACT_PIN_MAX_LENGTH &&
|
|
94
|
+
(!Number.isFinite(electrical) || electrical === 4)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns true when one owner line is compact marker linework near a pin.
|
|
100
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, ownerIndex?: string } | null | undefined} line Line primitive.
|
|
101
|
+
* @param {Map<string, { x: number, y: number, tolerance: number, maxLineLength: number }[]>} anchorsByOwner Marker anchors by owner.
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
static #isCompactMarkerLine(line, anchorsByOwner) {
|
|
105
|
+
const ownerIndex = String(line?.ownerIndex || '').trim()
|
|
106
|
+
const anchors = anchorsByOwner.get(ownerIndex)
|
|
107
|
+
|
|
108
|
+
if (!anchors?.length) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pointA = {
|
|
113
|
+
x: Number(line?.x1),
|
|
114
|
+
y: Number(line?.y1)
|
|
115
|
+
}
|
|
116
|
+
const pointB = {
|
|
117
|
+
x: Number(line?.x2),
|
|
118
|
+
y: Number(line?.y2)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
!Number.isFinite(pointA.x) ||
|
|
123
|
+
!Number.isFinite(pointA.y) ||
|
|
124
|
+
!Number.isFinite(pointB.x) ||
|
|
125
|
+
!Number.isFinite(pointB.y)
|
|
126
|
+
) {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lineLength = SchematicOwnerPinMarkerLineThemer.#distance(
|
|
131
|
+
pointA,
|
|
132
|
+
pointB
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return anchors.some(
|
|
136
|
+
(anchor) =>
|
|
137
|
+
lineLength > 0 &&
|
|
138
|
+
lineLength <= anchor.maxLineLength &&
|
|
139
|
+
SchematicOwnerPinMarkerLineThemer.#distance(pointA, anchor) <=
|
|
140
|
+
anchor.tolerance &&
|
|
141
|
+
SchematicOwnerPinMarkerLineThemer.#distance(pointB, anchor) <=
|
|
142
|
+
anchor.tolerance
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Measures planar distance between two schematic points.
|
|
148
|
+
* @param {{ x: number, y: number }} pointA First point.
|
|
149
|
+
* @param {{ x: number, y: number }} pointB Second point.
|
|
150
|
+
* @returns {number}
|
|
151
|
+
*/
|
|
152
|
+
static #distance(pointA, pointB) {
|
|
153
|
+
return Math.hypot(pointA.x - pointB.x, pointA.y - pointB.y)
|
|
154
|
+
}
|
|
155
|
+
}
|