altium-toolkit 1.1.2 → 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.
Files changed (57) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  8. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  9. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  10. package/src/core/altium/AltiumParser.mjs +245 -10
  11. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  12. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  13. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  14. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  15. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  16. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  17. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  18. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  19. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  20. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  21. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  22. package/src/core/altium/SchematicImageParser.mjs +291 -6
  23. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  24. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  25. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  26. package/src/core/altium/SchematicPinParser.mjs +262 -24
  27. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  28. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  29. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  30. package/src/core/altium/SchematicTextParser.mjs +125 -11
  31. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  32. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  33. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  34. package/src/core/altium/SourceComponentClient.mjs +239 -0
  35. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  36. package/src/parser.mjs +8 -0
  37. package/src/styles/altium-renderers.css +6 -6
  38. package/src/ui/PcbArcUtils.mjs +19 -2
  39. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  40. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  41. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  42. package/src/ui/SchematicColorResolver.mjs +263 -0
  43. package/src/ui/SchematicContentLayout.mjs +58 -1
  44. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  45. package/src/ui/SchematicImageRenderer.mjs +125 -10
  46. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  47. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  48. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  49. package/src/ui/SchematicNoteRenderer.mjs +87 -7
  50. package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
  51. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  52. package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
  53. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  54. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  55. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  56. package/src/ui/SchematicShapeRenderer.mjs +109 -24
  57. package/src/ui/SchematicSvgRenderer.mjs +1210 -71
@@ -12,12 +12,15 @@ import { SchematicDirectiveRenderer } from './SchematicDirectiveRenderer.mjs'
12
12
  import { SchematicShapeRenderer } from './SchematicShapeRenderer.mjs'
13
13
  import { SchematicPinSvgRenderer } from './SchematicPinSvgRenderer.mjs'
14
14
  import { SchematicColorResolver } from './SchematicColorResolver.mjs'
15
+ import { SchematicLineColorResolver } from './SchematicLineColorResolver.mjs'
15
16
  import { SchematicSheetChromeRenderer } from './SchematicSheetChromeRenderer.mjs'
16
17
  import { SchematicContentLayout } from './SchematicContentLayout.mjs'
17
18
  import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs'
18
19
  import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
19
20
  import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
20
21
  import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
22
+ import { SchematicNativeFooterPartitioner } from './SchematicNativeFooterPartitioner.mjs'
23
+ import { SchematicOwnerPinMarkerLineThemer } from './SchematicOwnerPinMarkerLineThemer.mjs'
21
24
  import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
22
25
  import { SchematicRenderOpsSidecarBuilder } from './SchematicRenderOpsSidecarBuilder.mjs'
23
26
  import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
@@ -28,6 +31,11 @@ const SECTION_HEADING_MIN_FONT_SIZE = 18
28
31
  const SECTION_HEADING_BASELINE_LIFT_RATIO = 0.36
29
32
  const SECTION_HEADING_LINE_Y_TOLERANCE = 0.75
30
33
  const SECTION_HEADING_LINE_X_PADDING = 15
34
+ const BODY_TEXT_ASCENT_RATIO = 0.72
35
+ const BODY_TEXT_DESCENT_RATIO = 0.14
36
+ const BODY_TEXT_FLAT_DESCENT_RATIO = 0
37
+ const BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO = 0.3
38
+ const BODY_TEXT_DESCENDER_PATTERN = /[gjpqyQ_,;]/
31
39
 
32
40
  /**
33
41
  * Renders normalized schematic models into presentational SVG.
@@ -38,7 +46,7 @@ export class SchematicSvgRenderer {
38
46
  /**
39
47
  * Renders a normalized schematic model into SVG markup.
40
48
  * @param {{ fileName?: string, summary: { title?: string }, schematic?: { sheet: { width: number, height: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }, lines: { x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, ownerIndex?: string, renderOrder?: number, recordType?: string }[], polygons?: { points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], rectangles?: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], regions?: { x: number, y: number, width: number, height: number, color: string, fill: string, renderOrder?: number }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, color: string, width: number, ownerIndex?: string, renderOrder?: number }[], directives?: { x: number, y: number, color: string, name: string, orientation?: number }[], texts: { x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }[], components: { x: number, y: number, designator: string }[], pins?: { x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }[], ports?: { x: number, y: number, width: number, height: number, name: string, fill: string, color: string, direction?: 'left' | 'right' | 'up' | 'down', shape?: 'single' | 'double' | 'plain' }[], crosses?: { x: number, y: number, size: number, color: string }[] } }} documentModel
41
- * @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined> }} options Render options.
49
+ * @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
42
50
  * @returns {string}
43
51
  */
44
52
  static render(documentModel, options = {}) {
@@ -66,7 +74,9 @@ export class SchematicSvgRenderer {
66
74
  renderedSheet.contentSheet === schematic.sheet
67
75
  ? schematic
68
76
  : { ...schematic, sheet: renderedSheet.contentSheet }
69
- const allTexts = schematic.texts || []
77
+ const allTexts = SchematicSvgRenderer.#filterDrawableSchematicTexts(
78
+ schematic.texts || []
79
+ )
70
80
  const lines = (schematic.lines || []).slice(0, 2500)
71
81
  const polygons = (schematic.polygons || []).slice(0, 1000)
72
82
  const rectangles = (schematic.rectangles || []).slice(0, 500)
@@ -146,25 +156,6 @@ export class SchematicSvgRenderer {
146
156
  renderedSchematic,
147
157
  contentClipId
148
158
  )
149
- const ownerlessLines = lines.filter((line) => !line.ownerIndex)
150
- const ownerlessPolygons = polygons.filter(
151
- (polygon) => !polygon.ownerIndex
152
- )
153
- const ownerlessRectangles = rectangles.filter(
154
- (rectangle) => !rectangle.ownerIndex
155
- )
156
- const ownerlessRoundedRectangles = roundedRectangles.filter(
157
- (rectangle) => !rectangle.ownerIndex
158
- )
159
- const ownerlessEllipses = ellipses.filter(
160
- (ellipse) => !ellipse.ownerIndex
161
- )
162
- const ownerlessArcs = arcs.filter((arc) => !arc.ownerIndex)
163
- const ownerlessBeziers = beziers.filter((bezier) => !bezier.ownerIndex)
164
- const ownerlessPies = pies.filter((pie) => !pie.ownerIndex)
165
- const ownerlessIeeeSymbols = ieeeSymbols.filter(
166
- (symbol) => !symbol.ownerIndex
167
- )
168
159
  const resolvedTexts = texts.map((text) =>
169
160
  text.recordType === '17'
170
161
  ? {
@@ -178,6 +169,77 @@ export class SchematicSvgRenderer {
178
169
  }
179
170
  : text
180
171
  )
172
+ const partitionedPrimitives =
173
+ SchematicNativeFooterPartitioner.partitionPrimitives(
174
+ {
175
+ lines,
176
+ polygons,
177
+ rectangles,
178
+ roundedRectangles,
179
+ ellipses,
180
+ arcs,
181
+ beziers,
182
+ pies,
183
+ ieeeSymbols,
184
+ texts: resolvedTexts,
185
+ images
186
+ },
187
+ renderedSchematic.sheet
188
+ )
189
+ const contentMarkerThemedLines =
190
+ SchematicOwnerPinMarkerLineThemer.theme(
191
+ partitionedPrimitives.content.lines,
192
+ pins
193
+ )
194
+ const contentLines = SchematicSvgRenderer.#themeFooterChromeLines(
195
+ contentMarkerThemedLines,
196
+ renderedSheet.sheet,
197
+ contentHeight
198
+ )
199
+ const contentPolygons = partitionedPrimitives.content.polygons
200
+ const contentRectangles = partitionedPrimitives.content.rectangles
201
+ const contentRoundedRectangles =
202
+ partitionedPrimitives.content.roundedRectangles
203
+ const contentEllipses = partitionedPrimitives.content.ellipses
204
+ const contentArcs = partitionedPrimitives.content.arcs
205
+ const contentBeziers = partitionedPrimitives.content.beziers
206
+ const contentPies = partitionedPrimitives.content.pies
207
+ const contentIeeeSymbols = partitionedPrimitives.content.ieeeSymbols
208
+ const contentTexts = partitionedPrimitives.content.texts
209
+ const contentImages = partitionedPrimitives.content.images
210
+ const footerPrimitives = partitionedPrimitives.footer
211
+ const foregroundOwnerlessLines =
212
+ SchematicSvgRenderer.#collectForegroundOwnerlessLines(
213
+ contentLines,
214
+ contentPolygons,
215
+ contentRectangles,
216
+ contentRoundedRectangles,
217
+ contentEllipses,
218
+ contentPies
219
+ )
220
+ const ownerlessLines = contentLines.filter(
221
+ (line) => !line.ownerIndex && !foregroundOwnerlessLines.has(line)
222
+ )
223
+ const ownerlessPolygons = contentPolygons.filter(
224
+ (polygon) => !polygon.ownerIndex
225
+ )
226
+ const ownerlessRectangles = contentRectangles.filter(
227
+ (rectangle) => !rectangle.ownerIndex
228
+ )
229
+ const ownerlessRoundedRectangles = contentRoundedRectangles.filter(
230
+ (rectangle) => !rectangle.ownerIndex
231
+ )
232
+ const ownerlessEllipses = contentEllipses.filter(
233
+ (ellipse) => !ellipse.ownerIndex
234
+ )
235
+ const ownerlessArcs = contentArcs.filter((arc) => !arc.ownerIndex)
236
+ const ownerlessBeziers = contentBeziers.filter(
237
+ (bezier) => !bezier.ownerIndex
238
+ )
239
+ const ownerlessPies = contentPies.filter((pie) => !pie.ownerIndex)
240
+ const ownerlessIeeeSymbols = contentIeeeSymbols.filter(
241
+ (symbol) => !symbol.ownerIndex
242
+ )
181
243
  const polygonMarkup = ownerlessPolygons
182
244
  .map((polygon, index) =>
183
245
  SchematicSvgRenderer.#appendSvgAttributes(
@@ -357,18 +419,33 @@ export class SchematicSvgRenderer {
357
419
  .join('')
358
420
  const ownerGeometryMarkup =
359
421
  SchematicSvgRenderer.#buildOwnerGeometryMarkup(
360
- lines,
361
- polygons,
362
- rectangles,
363
- roundedRectangles,
364
- ellipses,
365
- arcs,
366
- beziers,
367
- pies,
368
- ieeeSymbols,
422
+ contentLines,
423
+ contentPolygons,
424
+ contentRectangles,
425
+ contentRoundedRectangles,
426
+ contentEllipses,
427
+ contentArcs,
428
+ contentBeziers,
429
+ contentPies,
430
+ contentIeeeSymbols,
369
431
  contentHeight,
370
432
  semanticContext
371
433
  )
434
+ const foregroundLineMarkup = [...foregroundOwnerlessLines]
435
+ .map((line, index) =>
436
+ SchematicSvgRenderer.#buildSchematicLineMarkup(
437
+ line,
438
+ contentHeight,
439
+ SchematicSvgRenderer.#primitiveIndex(
440
+ semanticContext,
441
+ 'lines',
442
+ line,
443
+ index
444
+ ),
445
+ semanticContext
446
+ )
447
+ )
448
+ .join('')
372
449
  const sheetSymbolMarkup =
373
450
  SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
374
451
  sheetSymbols,
@@ -390,7 +467,7 @@ export class SchematicSvgRenderer {
390
467
  const resolvedAuthoredJunctions =
391
468
  SchematicSvgRenderer.#resolveAuthoredSchematicJunctions(
392
469
  authoredJunctions,
393
- lines
470
+ contentLines
394
471
  )
395
472
  const authoredJunctionMarkup = resolvedAuthoredJunctions
396
473
  .map((junction) =>
@@ -401,18 +478,85 @@ export class SchematicSvgRenderer {
401
478
  )
402
479
  .join('')
403
480
  const imageMarkup = SchematicImageRenderer.buildMarkup(
404
- images,
405
- contentHeight
481
+ contentImages,
482
+ contentHeight,
483
+ {
484
+ colorizeImages: renderOptions.colorizeImages,
485
+ title:
486
+ renderModel?.summary?.title ||
487
+ renderedSchematic?.sheet?.titleBlock?.title ||
488
+ renderModel?.fileName ||
489
+ ''
490
+ }
406
491
  )
407
492
  const markerDefsMarkup =
408
- SchematicSvgRenderer.#buildSchematicLineMarkerDefs(lines)
493
+ SchematicSvgRenderer.#buildSchematicLineMarkerDefs(contentLines)
494
+ const ownerTextBodyBounds =
495
+ SchematicSvgRenderer.#collectOwnerTextBodyBounds(
496
+ contentLines,
497
+ contentPolygons,
498
+ contentRectangles,
499
+ contentRoundedRectangles,
500
+ contentEllipses,
501
+ contentArcs,
502
+ contentPies
503
+ )
409
504
 
410
- const textMarkup = resolvedTexts
505
+ const textMarkup = contentTexts
411
506
  .map((text, index) =>
412
507
  SchematicSvgRenderer.#buildSchematicTextMarkup(
413
508
  text,
414
509
  contentHeight,
415
- lines,
510
+ contentLines,
511
+ pins,
512
+ SchematicSvgRenderer.#primitiveIndex(
513
+ semanticContext,
514
+ 'texts',
515
+ text,
516
+ index
517
+ ),
518
+ semanticContext,
519
+ ownerTextBodyBounds
520
+ )
521
+ )
522
+ .join('')
523
+ const nativeFooterLines = SchematicSvgRenderer.#themeFooterChromeLines(
524
+ footerPrimitives.lines,
525
+ renderedSheet.sheet,
526
+ contentHeight
527
+ )
528
+ const nativeFooterOwnerGeometryMarkup =
529
+ SchematicSvgRenderer.#buildOwnerGeometryMarkup(
530
+ nativeFooterLines,
531
+ footerPrimitives.polygons,
532
+ footerPrimitives.rectangles,
533
+ footerPrimitives.roundedRectangles,
534
+ footerPrimitives.ellipses,
535
+ footerPrimitives.arcs,
536
+ footerPrimitives.beziers,
537
+ footerPrimitives.pies,
538
+ footerPrimitives.ieeeSymbols,
539
+ contentHeight,
540
+ semanticContext
541
+ )
542
+ const nativeFooterImageMarkup = SchematicImageRenderer.buildMarkup(
543
+ footerPrimitives.images,
544
+ contentHeight,
545
+ {
546
+ colorizeImages: renderOptions.colorizeImages,
547
+ title:
548
+ renderModel?.summary?.title ||
549
+ renderedSchematic?.sheet?.titleBlock?.title ||
550
+ renderModel?.fileName ||
551
+ ''
552
+ }
553
+ )
554
+ const nativeFooterTextMarkup = footerPrimitives.texts
555
+ .map((text, index) =>
556
+ SchematicSvgRenderer.#buildSchematicTextMarkup(
557
+ text,
558
+ contentHeight,
559
+ contentLines,
416
560
  pins,
417
561
  SchematicSvgRenderer.#primitiveIndex(
418
562
  semanticContext,
@@ -424,6 +568,36 @@ export class SchematicSvgRenderer {
424
568
  )
425
569
  )
426
570
  .join('')
571
+ const nativeFooterMargin = Math.max(
572
+ Number(renderedSchematic?.sheet?.marginWidth || 20),
573
+ 10
574
+ )
575
+ const nativeFooterOffsetX = partitionedPrimitives.footerBounds
576
+ ? Math.max(
577
+ width -
578
+ nativeFooterMargin -
579
+ partitionedPrimitives.footerBounds.maxX,
580
+ 0
581
+ )
582
+ : 0
583
+ const nativeFooterTransform =
584
+ nativeFooterOffsetX > 0.01
585
+ ? ' transform="translate(' +
586
+ formatNumber(nativeFooterOffsetX) +
587
+ ' 0)"'
588
+ : ''
589
+ const nativeFooterMarkup =
590
+ nativeFooterOwnerGeometryMarkup ||
591
+ nativeFooterImageMarkup ||
592
+ nativeFooterTextMarkup
593
+ ? '<g class="schematic-native-footer" stroke-linecap="round"' +
594
+ nativeFooterTransform +
595
+ '>' +
596
+ nativeFooterOwnerGeometryMarkup +
597
+ nativeFooterImageMarkup +
598
+ nativeFooterTextMarkup +
599
+ '</g>'
600
+ : ''
427
601
 
428
602
  const componentMarkup = drawableComponents
429
603
  .map((component, index) =>
@@ -451,6 +625,21 @@ export class SchematicSvgRenderer {
451
625
  texts,
452
626
  pins
453
627
  )
628
+ const compactExternalNumberLabelSides =
629
+ SchematicOwnerPinLabelLayout.collectCompactExternalNumberLabelSides(
630
+ pins
631
+ )
632
+ const internalNumberLabelBoxes =
633
+ SchematicOwnerPinLabelLayout.collectInternalNumberLabelBoxes(
634
+ pins,
635
+ contentRectangles
636
+ )
637
+ const overlappingExternalNumberLabelKeys =
638
+ SchematicOwnerPinLabelLayout.collectOverlappingExternalNumberLabelKeys(
639
+ pins,
640
+ contentTexts,
641
+ internalNumberLabelBoxes
642
+ )
454
643
  const pinMarkup = pins
455
644
  .map((pin, index) =>
456
645
  SchematicSvgRenderer.#appendSvgAttributes(
@@ -460,7 +649,10 @@ export class SchematicSvgRenderer {
460
649
  renderedSheet.contentSheet,
461
650
  rotatedVerticalNumberOwners,
462
651
  explicitOwnerPinNameLabels,
463
- explicitOwnerPinLabelOffsets
652
+ explicitOwnerPinLabelOffsets,
653
+ compactExternalNumberLabelSides,
654
+ internalNumberLabelBoxes,
655
+ overlappingExternalNumberLabelKeys
464
656
  ),
465
657
  SchematicSvgRenderer.#semanticAttributes(
466
658
  'pin',
@@ -504,10 +696,10 @@ export class SchematicSvgRenderer {
504
696
  )
505
697
  .join('')
506
698
  const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
507
- lines,
699
+ contentLines,
508
700
  crosses,
509
701
  ports,
510
- resolvedTexts.filter((text) => text.recordType === '17'),
702
+ contentTexts.filter((text) => text.recordType === '17'),
511
703
  contentHeight,
512
704
  resolvedAuthoredJunctions
513
705
  )
@@ -590,6 +782,9 @@ export class SchematicSvgRenderer {
590
782
  '<g class="schematic-owner-geometry" stroke-linecap="round">' +
591
783
  ownerGeometryMarkup +
592
784
  '</g>' +
785
+ '<g class="schematic-foreground-lines" stroke-linecap="round">' +
786
+ foregroundLineMarkup +
787
+ '</g>' +
593
788
  '<g class="schematic-sheet-symbols">' +
594
789
  sheetSymbolMarkup +
595
790
  '</g>' +
@@ -626,6 +821,7 @@ export class SchematicSvgRenderer {
626
821
  '</g>' +
627
822
  '</g>' +
628
823
  frameMarkup +
824
+ nativeFooterMarkup +
629
825
  '<g class="schematic-regions">' +
630
826
  regionMarkup +
631
827
  '</g>' +
@@ -636,7 +832,7 @@ export class SchematicSvgRenderer {
636
832
  /**
637
833
  * Normalizes schematic SVG export options.
638
834
  * @param {Record<string, unknown>} options Raw render options.
639
- * @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
835
+ * @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, colorizeImages: boolean, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
640
836
  */
641
837
  static #normalizeRenderOptions(options) {
642
838
  const includeViewBox =
@@ -648,6 +844,9 @@ export class SchematicSvgRenderer {
648
844
  documentVersion: String(
649
845
  options?.documentVersion || options?.documentVer || ''
650
846
  ),
847
+ colorizeImages:
848
+ options?.colorizeImages === true ||
849
+ options?.colorize_images === true,
651
850
  includeTextGeometrySidecar:
652
851
  options?.includeTextGeometrySidecar === true ||
653
852
  options?.textGeometry === 'sidecar',
@@ -660,6 +859,106 @@ export class SchematicSvgRenderer {
660
859
  }
661
860
  }
662
861
 
862
+ /**
863
+ * Removes unresolved simple Altium special-string placeholders from
864
+ * drawable text while preserving values resolved by project parameters.
865
+ * @param {object[]} texts Schematic text rows.
866
+ * @returns {object[]}
867
+ */
868
+ static #filterDrawableSchematicTexts(texts) {
869
+ return (texts || []).filter(
870
+ (text) =>
871
+ !SchematicSvgRenderer.#isUnresolvedSimpleSpecialStringText(text)
872
+ )
873
+ }
874
+
875
+ /**
876
+ * Uses sheet-frame chrome color for native footer linework so recovered
877
+ * template cells match the synthesized frame and zone separators.
878
+ * @param {object[]} lines Schematic line primitives.
879
+ * @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
880
+ * @param {number} sheetHeight Rendered sheet height for projection.
881
+ * @returns {object[]}
882
+ */
883
+ static #themeFooterChromeLines(lines, sheet, sheetHeight) {
884
+ return (lines || []).map((line) =>
885
+ SchematicSvgRenderer.#isFooterChromeLine(line, sheet, sheetHeight)
886
+ ? {
887
+ ...line,
888
+ color: 'var(--schematic-sheet-frame-stroke)'
889
+ }
890
+ : line
891
+ )
892
+ }
893
+
894
+ /**
895
+ * Returns true for neutral owner-drawn title-block lines in the bottom
896
+ * right footer band.
897
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }} line Schematic line candidate.
898
+ * @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
899
+ * @param {number} sheetHeight Rendered sheet height for projection.
900
+ * @returns {boolean}
901
+ */
902
+ static #isFooterChromeLine(line, sheet, sheetHeight) {
903
+ if (!sheet?.borderOn || !String(line?.ownerIndex || '').trim()) {
904
+ return false
905
+ }
906
+ if (
907
+ SchematicColorResolver.resolveColor(
908
+ line.color,
909
+ '--schematic-default-ink-color'
910
+ ) !== 'var(--schematic-text-color)'
911
+ ) {
912
+ return false
913
+ }
914
+
915
+ const width = Number(sheet?.width || 0)
916
+ const height = Number(sheetHeight || 0)
917
+ const x1 = Number(line?.x1)
918
+ const x2 = Number(line?.x2)
919
+ const y1 = Number(line?.y1)
920
+ const y2 = Number(line?.y2)
921
+
922
+ if (
923
+ !Number.isFinite(width) ||
924
+ !Number.isFinite(height) ||
925
+ !Number.isFinite(x1) ||
926
+ !Number.isFinite(x2) ||
927
+ !Number.isFinite(y1) ||
928
+ !Number.isFinite(y2) ||
929
+ width <= 0 ||
930
+ height <= 0
931
+ ) {
932
+ return false
933
+ }
934
+
935
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
936
+ const lineLength = Math.hypot(x2 - x1, y2 - y1)
937
+ const minChromeLineLength = Math.max(margin * 0.6, 12)
938
+
939
+ if (lineLength < minChromeLineLength) {
940
+ return false
941
+ }
942
+
943
+ const footerBandHeight = Math.max(margin * 6, 120)
944
+ const footerTop = Math.max(height - footerBandHeight, margin)
945
+ const projectedY1 = projectSchematicY(height, y1)
946
+ const projectedY2 = projectSchematicY(height, y2)
947
+ const minProjectedY = Math.min(projectedY1, projectedY2)
948
+ const maxX = Math.max(x1, x2)
949
+
950
+ return minProjectedY >= footerTop && maxX >= width * 0.5
951
+ }
952
+
953
+ /**
954
+ * Returns true when a text row still contains one bare special string.
955
+ * @param {object} text Schematic text row.
956
+ * @returns {boolean}
957
+ */
958
+ static #isUnresolvedSimpleSpecialStringText(text) {
959
+ return /^[.=][A-Za-z_][\w.-]*$/u.test(String(text?.text || '').trim())
960
+ }
961
+
663
962
  /**
664
963
  * Renders root SVG viewBox attributes according to export options.
665
964
  * @param {{ includeViewBox: boolean }} options Normalized options.
@@ -1560,6 +1859,311 @@ export class SchematicSvgRenderer {
1560
1859
  return height + margin * 2
1561
1860
  }
1562
1861
 
1862
+ /**
1863
+ * Collects ownerless line primitives that were authored after an
1864
+ * owner-linked fill they overlap. These are symbol decoration strokes
1865
+ * stored outside the owner group and must paint above the body fill.
1866
+ * @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }[]} lines Line primitives.
1867
+ * @param {{ ownerIndex?: string, renderOrder?: number, points?: { x: number, y: number }[] }[]} polygons Polygon primitives.
1868
+ * @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} rectangles Rectangle primitives.
1869
+ * @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} roundedRectangles Rounded rectangle primitives.
1870
+ * @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radiusX: number, radiusY: number }[]} ellipses Ellipse primitives.
1871
+ * @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radius: number, radiusY?: number }[]} pies Pie primitives.
1872
+ * @returns {Set<object>}
1873
+ */
1874
+ static #collectForegroundOwnerlessLines(
1875
+ lines,
1876
+ polygons,
1877
+ rectangles,
1878
+ roundedRectangles,
1879
+ ellipses,
1880
+ pies
1881
+ ) {
1882
+ const ownerFillBounds = SchematicSvgRenderer.#collectOwnerFillBounds(
1883
+ polygons,
1884
+ rectangles,
1885
+ roundedRectangles,
1886
+ ellipses,
1887
+ pies
1888
+ )
1889
+
1890
+ if (!ownerFillBounds.length) {
1891
+ return new Set()
1892
+ }
1893
+
1894
+ return new Set(
1895
+ (lines || []).filter((line) =>
1896
+ SchematicSvgRenderer.#isForegroundOwnerlessLine(
1897
+ line,
1898
+ ownerFillBounds
1899
+ )
1900
+ )
1901
+ )
1902
+ }
1903
+
1904
+ /**
1905
+ * Collects sortable bounds for owner-linked fill primitives.
1906
+ * @param {object[]} polygons Polygon primitives.
1907
+ * @param {object[]} rectangles Rectangle primitives.
1908
+ * @param {object[]} roundedRectangles Rounded rectangle primitives.
1909
+ * @param {object[]} ellipses Ellipse primitives.
1910
+ * @param {object[]} pies Pie primitives.
1911
+ * @returns {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]}
1912
+ */
1913
+ static #collectOwnerFillBounds(
1914
+ polygons,
1915
+ rectangles,
1916
+ roundedRectangles,
1917
+ ellipses,
1918
+ pies
1919
+ ) {
1920
+ return [
1921
+ ...(polygons || []),
1922
+ ...(rectangles || []),
1923
+ ...(roundedRectangles || []),
1924
+ ...(ellipses || []),
1925
+ ...(pies || [])
1926
+ ]
1927
+ .filter((primitive) => primitive?.ownerIndex)
1928
+ .map((primitive) => ({
1929
+ bounds: SchematicSvgRenderer.#primitiveBounds(primitive),
1930
+ renderOrder:
1931
+ SchematicSvgRenderer.#resolvePrimitiveRenderOrder(primitive)
1932
+ }))
1933
+ .filter(
1934
+ (entry) =>
1935
+ entry.bounds &&
1936
+ Number.isFinite(entry.renderOrder) &&
1937
+ entry.renderOrder < Number.MAX_SAFE_INTEGER
1938
+ )
1939
+ }
1940
+
1941
+ /**
1942
+ * Collects unprojected owner body envelopes used to identify text that is
1943
+ * drawn inside a component body instead of on surrounding wires or sheet
1944
+ * chrome.
1945
+ * @param {object[]} lines Line primitives.
1946
+ * @param {object[]} polygons Polygon primitives.
1947
+ * @param {object[]} rectangles Rectangle primitives.
1948
+ * @param {object[]} roundedRectangles Rounded rectangle primitives.
1949
+ * @param {object[]} ellipses Ellipse primitives.
1950
+ * @param {object[]} arcs Arc primitives.
1951
+ * @param {object[]} pies Pie primitives.
1952
+ * @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
1953
+ */
1954
+ static #collectOwnerTextBodyBounds(
1955
+ lines,
1956
+ polygons,
1957
+ rectangles,
1958
+ roundedRectangles,
1959
+ ellipses,
1960
+ arcs,
1961
+ pies
1962
+ ) {
1963
+ const ownerBounds = new Map()
1964
+
1965
+ for (const primitive of [
1966
+ ...(lines || []),
1967
+ ...(polygons || []),
1968
+ ...(rectangles || []),
1969
+ ...(roundedRectangles || []),
1970
+ ...(ellipses || []),
1971
+ ...(arcs || []),
1972
+ ...(pies || [])
1973
+ ]) {
1974
+ SchematicSvgRenderer.#extendOwnerTextBodyBounds(
1975
+ ownerBounds,
1976
+ primitive
1977
+ )
1978
+ }
1979
+
1980
+ return ownerBounds
1981
+ }
1982
+
1983
+ /**
1984
+ * Extends the body-bounds map with one owner-linked primitive.
1985
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds Owner bounds map.
1986
+ * @param {object} primitive Primitive candidate.
1987
+ * @returns {void}
1988
+ */
1989
+ static #extendOwnerTextBodyBounds(ownerBounds, primitive) {
1990
+ const ownerIndex = String(primitive?.ownerIndex || '').trim()
1991
+ const bounds = SchematicSvgRenderer.#primitiveBounds(primitive)
1992
+
1993
+ if (!ownerIndex || !bounds) {
1994
+ return
1995
+ }
1996
+
1997
+ const current = ownerBounds.get(ownerIndex)
1998
+
1999
+ if (!current) {
2000
+ ownerBounds.set(ownerIndex, { ...bounds })
2001
+ return
2002
+ }
2003
+
2004
+ current.minX = Math.min(current.minX, bounds.minX)
2005
+ current.minY = Math.min(current.minY, bounds.minY)
2006
+ current.maxX = Math.max(current.maxX, bounds.maxX)
2007
+ current.maxY = Math.max(current.maxY, bounds.maxY)
2008
+ }
2009
+
2010
+ /**
2011
+ * Returns true when one ownerless line should paint above an overlapping
2012
+ * owner fill because the native stream authored it later.
2013
+ * @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }} line Line primitive.
2014
+ * @param {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]} ownerFillBounds Fill bounds.
2015
+ * @returns {boolean}
2016
+ */
2017
+ static #isForegroundOwnerlessLine(line, ownerFillBounds) {
2018
+ if (line?.ownerIndex) {
2019
+ return false
2020
+ }
2021
+
2022
+ const renderOrder = Number(line?.renderOrder)
2023
+ if (!Number.isFinite(renderOrder)) {
2024
+ return false
2025
+ }
2026
+
2027
+ const lineBounds = SchematicSvgRenderer.#primitiveBounds(line)
2028
+ if (!lineBounds) {
2029
+ return false
2030
+ }
2031
+
2032
+ return ownerFillBounds.some(
2033
+ (fill) =>
2034
+ renderOrder > fill.renderOrder &&
2035
+ SchematicSvgRenderer.#boundsIntersect(
2036
+ lineBounds,
2037
+ fill.bounds,
2038
+ 0.5
2039
+ )
2040
+ )
2041
+ }
2042
+
2043
+ /**
2044
+ * Resolves an unprojected coordinate envelope for one schematic primitive.
2045
+ * @param {object} primitive Primitive.
2046
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2047
+ */
2048
+ static #primitiveBounds(primitive) {
2049
+ if (Array.isArray(primitive?.points) && primitive.points.length > 0) {
2050
+ return SchematicSvgRenderer.#pointBounds(primitive.points)
2051
+ }
2052
+
2053
+ if (
2054
+ Number.isFinite(Number(primitive?.x1)) &&
2055
+ Number.isFinite(Number(primitive?.y1)) &&
2056
+ Number.isFinite(Number(primitive?.x2)) &&
2057
+ Number.isFinite(Number(primitive?.y2))
2058
+ ) {
2059
+ return SchematicSvgRenderer.#pointBounds([
2060
+ { x: Number(primitive.x1), y: Number(primitive.y1) },
2061
+ { x: Number(primitive.x2), y: Number(primitive.y2) }
2062
+ ])
2063
+ }
2064
+
2065
+ if (
2066
+ Number.isFinite(Number(primitive?.x)) &&
2067
+ Number.isFinite(Number(primitive?.y)) &&
2068
+ Number.isFinite(Number(primitive?.width)) &&
2069
+ Number.isFinite(Number(primitive?.height))
2070
+ ) {
2071
+ const x = Number(primitive.x)
2072
+ const y = Number(primitive.y)
2073
+ const x2 = x + Number(primitive.width)
2074
+ const y2 = y + Number(primitive.height)
2075
+
2076
+ return {
2077
+ minX: Math.min(x, x2),
2078
+ minY: Math.min(y, y2),
2079
+ maxX: Math.max(x, x2),
2080
+ maxY: Math.max(y, y2)
2081
+ }
2082
+ }
2083
+
2084
+ if (
2085
+ Number.isFinite(Number(primitive?.x)) &&
2086
+ Number.isFinite(Number(primitive?.y)) &&
2087
+ Number.isFinite(Number(primitive?.radiusX)) &&
2088
+ Number.isFinite(Number(primitive?.radiusY))
2089
+ ) {
2090
+ const x = Number(primitive.x)
2091
+ const y = Number(primitive.y)
2092
+ const radiusX = Math.abs(Number(primitive.radiusX))
2093
+ const radiusY = Math.abs(Number(primitive.radiusY))
2094
+
2095
+ return {
2096
+ minX: x - radiusX,
2097
+ minY: y - radiusY,
2098
+ maxX: x + radiusX,
2099
+ maxY: y + radiusY
2100
+ }
2101
+ }
2102
+
2103
+ if (
2104
+ Number.isFinite(Number(primitive?.x)) &&
2105
+ Number.isFinite(Number(primitive?.y)) &&
2106
+ Number.isFinite(Number(primitive?.radius))
2107
+ ) {
2108
+ const x = Number(primitive.x)
2109
+ const y = Number(primitive.y)
2110
+ const radiusX = Math.abs(Number(primitive.radius))
2111
+ const radiusY = Math.abs(Number(primitive.radiusY ?? radiusX))
2112
+
2113
+ return {
2114
+ minX: x - radiusX,
2115
+ minY: y - radiusY,
2116
+ maxX: x + radiusX,
2117
+ maxY: y + radiusY
2118
+ }
2119
+ }
2120
+
2121
+ return null
2122
+ }
2123
+
2124
+ /**
2125
+ * Resolves bounds for a point list.
2126
+ * @param {{ x: number, y: number }[]} points Points.
2127
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2128
+ */
2129
+ static #pointBounds(points) {
2130
+ const numericPoints = (points || [])
2131
+ .map((point) => ({
2132
+ x: Number(point?.x),
2133
+ y: Number(point?.y)
2134
+ }))
2135
+ .filter(
2136
+ (point) => Number.isFinite(point.x) && Number.isFinite(point.y)
2137
+ )
2138
+
2139
+ if (!numericPoints.length) {
2140
+ return null
2141
+ }
2142
+
2143
+ return {
2144
+ minX: Math.min(...numericPoints.map((point) => point.x)),
2145
+ minY: Math.min(...numericPoints.map((point) => point.y)),
2146
+ maxX: Math.max(...numericPoints.map((point) => point.x)),
2147
+ maxY: Math.max(...numericPoints.map((point) => point.y))
2148
+ }
2149
+ }
2150
+
2151
+ /**
2152
+ * Returns true when two unprojected coordinate bounds overlap.
2153
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} left Left bounds.
2154
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} right Right bounds.
2155
+ * @param {number} tolerance Coordinate tolerance.
2156
+ * @returns {boolean}
2157
+ */
2158
+ static #boundsIntersect(left, right, tolerance = 0) {
2159
+ return (
2160
+ left.maxX >= right.minX - tolerance &&
2161
+ left.minX <= right.maxX + tolerance &&
2162
+ left.maxY >= right.minY - tolerance &&
2163
+ left.minY <= right.maxY + tolerance
2164
+ )
2165
+ }
2166
+
1563
2167
  /**
1564
2168
  * Builds interleaved owner geometry so symbol-internal primitives preserve
1565
2169
  * their recovered Altium paint order instead of batching fills ahead of all
@@ -1900,12 +2504,7 @@ export class SchematicSvgRenderer {
1900
2504
  '" y2="' +
1901
2505
  formatNumber(projectSchematicY(sheetHeight, line.y2)) +
1902
2506
  '" stroke="' +
1903
- escapeHtml(
1904
- SchematicColorResolver.resolveColor(
1905
- line.color,
1906
- '--schematic-default-ink-color'
1907
- )
1908
- ) +
2507
+ escapeHtml(SchematicLineColorResolver.resolveColor(line)) +
1909
2508
  '" stroke-width="' +
1910
2509
  formatNumber(
1911
2510
  SchematicSvgRenderer.#resolveSchematicLineWidth(line)
@@ -2015,6 +2614,7 @@ export class SchematicSvgRenderer {
2015
2614
  * @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
2016
2615
  * @param {number} index Stable primitive index.
2017
2616
  * @param {object | undefined} semanticContext Semantic lookup context.
2617
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2018
2618
  * @returns {string}
2019
2619
  */
2020
2620
  static #buildSchematicTextMarkup(
@@ -2023,7 +2623,8 @@ export class SchematicSvgRenderer {
2023
2623
  lines,
2024
2624
  pins,
2025
2625
  index = 0,
2026
- semanticContext = undefined
2626
+ semanticContext = undefined,
2627
+ ownerTextBodyBounds = new Map()
2027
2628
  ) {
2028
2629
  const matchedOwnerPin =
2029
2630
  SchematicOwnerPinLabelLayout.findExplicitOwnerPinLabelMatch(
@@ -2063,7 +2664,8 @@ export class SchematicSvgRenderer {
2063
2664
  text,
2064
2665
  sheetHeight,
2065
2666
  lines,
2066
- matchedOwnerPin
2667
+ matchedOwnerPin,
2668
+ ownerTextBodyBounds
2067
2669
  )
2068
2670
 
2069
2671
  return SchematicSvgRenderer.#appendSvgAttributes(
@@ -2098,13 +2700,15 @@ export class SchematicSvgRenderer {
2098
2700
  * @param {number} sheetHeight
2099
2701
  * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
2100
2702
  * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
2703
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2101
2704
  * @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
2102
2705
  */
2103
2706
  static #resolveSchematicTextPlacement(
2104
2707
  text,
2105
2708
  sheetHeight,
2106
2709
  lines,
2107
- matchedOwnerPin
2710
+ matchedOwnerPin,
2711
+ ownerTextBodyBounds
2108
2712
  ) {
2109
2713
  const mirroredOwnerPinPlacement =
2110
2714
  SchematicOwnerPinLabelLayout.resolveMirroredOwnerPinLabelPlacement(
@@ -2112,6 +2716,7 @@ export class SchematicSvgRenderer {
2112
2716
  matchedOwnerPin
2113
2717
  )
2114
2718
  const sourceY = mirroredOwnerPinPlacement?.y ?? text.y
2719
+ const x = mirroredOwnerPinPlacement?.x ?? text.x
2115
2720
  const projectedY = projectSchematicY(sheetHeight, sourceY)
2116
2721
  const fontSize =
2117
2722
  SchematicTypography.resolveViewerFontSize(text.fontSize) || 0
@@ -2123,14 +2728,500 @@ export class SchematicSvgRenderer {
2123
2728
  fontSize,
2124
2729
  matchedOwnerPin
2125
2730
  )
2731
+ const ownerBodyBaselineOffset =
2732
+ SchematicSvgRenderer.#resolveOwnerBodyTextBaselineOffset(
2733
+ text,
2734
+ x,
2735
+ sourceY,
2736
+ fontSize,
2737
+ matchedOwnerPin,
2738
+ ownerTextBodyBounds
2739
+ )
2740
+
2741
+ const unclampedY = projectedY - baselineLift + ownerBodyBaselineOffset
2126
2742
 
2127
2743
  return {
2128
- x: mirroredOwnerPinPlacement?.x ?? text.x,
2129
- y: projectedY - baselineLift,
2744
+ x,
2745
+ y: SchematicSvgRenderer.#clampOwnerBodyTextBaseline(
2746
+ text,
2747
+ x,
2748
+ sourceY,
2749
+ unclampedY,
2750
+ sheetHeight,
2751
+ fontSize,
2752
+ matchedOwnerPin,
2753
+ ownerTextBodyBounds,
2754
+ lines
2755
+ ),
2130
2756
  anchor: text.anchor || 'start'
2131
2757
  }
2132
2758
  }
2133
2759
 
2760
+ /**
2761
+ * Keeps free schematic body annotations inside the owner body cell they
2762
+ * were authored over, including ownerless overlay text used by some
2763
+ * symbols for truth-table and switch-state labels.
2764
+ * @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
2765
+ * @param {number} x Render X coordinate.
2766
+ * @param {number} sourceY Unprojected source Y coordinate.
2767
+ * @param {number} projectedY Candidate SVG baseline.
2768
+ * @param {number} sheetHeight Rendered sheet height.
2769
+ * @param {number} fontSize Viewer font size.
2770
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
2771
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2772
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
2773
+ * @returns {number}
2774
+ */
2775
+ static #clampOwnerBodyTextBaseline(
2776
+ text,
2777
+ x,
2778
+ sourceY,
2779
+ projectedY,
2780
+ sheetHeight,
2781
+ fontSize,
2782
+ matchedOwnerPin,
2783
+ ownerTextBodyBounds,
2784
+ lines
2785
+ ) {
2786
+ const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextCellBounds(
2787
+ text,
2788
+ x,
2789
+ sourceY,
2790
+ fontSize,
2791
+ matchedOwnerPin,
2792
+ ownerTextBodyBounds,
2793
+ lines
2794
+ )
2795
+
2796
+ if (!bounds) {
2797
+ return projectedY
2798
+ }
2799
+
2800
+ return SchematicSvgRenderer.#clampTextBaselineToBounds(
2801
+ text,
2802
+ projectedY,
2803
+ bounds,
2804
+ sheetHeight,
2805
+ fontSize
2806
+ )
2807
+ }
2808
+
2809
+ /**
2810
+ * Resolves the owner body or internal owner body cell containing text.
2811
+ * @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
2812
+ * @param {number} x Render X coordinate.
2813
+ * @param {number} sourceY Unprojected source Y coordinate.
2814
+ * @param {number} fontSize Viewer font size.
2815
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
2816
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2817
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
2818
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2819
+ */
2820
+ static #resolveOwnerBodyTextCellBounds(
2821
+ text,
2822
+ x,
2823
+ sourceY,
2824
+ fontSize,
2825
+ matchedOwnerPin,
2826
+ ownerTextBodyBounds,
2827
+ lines
2828
+ ) {
2829
+ const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextBounds(
2830
+ text,
2831
+ x,
2832
+ sourceY,
2833
+ fontSize,
2834
+ matchedOwnerPin,
2835
+ ownerTextBodyBounds
2836
+ )
2837
+
2838
+ if (!bounds) {
2839
+ return null
2840
+ }
2841
+
2842
+ return (
2843
+ SchematicSvgRenderer.#resolveOwnerBodyTextLineCellBounds(
2844
+ text,
2845
+ x,
2846
+ sourceY,
2847
+ fontSize,
2848
+ bounds,
2849
+ lines
2850
+ ) || bounds
2851
+ )
2852
+ }
2853
+
2854
+ /**
2855
+ * Resolves the owner body containing one free text primitive.
2856
+ * @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
2857
+ * @param {number} x Render X coordinate.
2858
+ * @param {number} sourceY Unprojected source Y coordinate.
2859
+ * @param {number} fontSize Viewer font size.
2860
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
2861
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2862
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2863
+ */
2864
+ static #resolveOwnerBodyTextBounds(
2865
+ text,
2866
+ x,
2867
+ sourceY,
2868
+ fontSize,
2869
+ matchedOwnerPin,
2870
+ ownerTextBodyBounds
2871
+ ) {
2872
+ if (matchedOwnerPin) return null
2873
+ if (text.recordType !== '4') return null
2874
+ if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
2875
+ return null
2876
+ }
2877
+ if (!Number.isFinite(fontSize) || fontSize <= 0) return null
2878
+
2879
+ const ownerIndex = String(text.ownerIndex || '').trim()
2880
+ const tolerance = Math.max(fontSize * 0.4, 1)
2881
+ const explicitBounds = ownerIndex
2882
+ ? ownerTextBodyBounds.get(ownerIndex)
2883
+ : null
2884
+
2885
+ if (
2886
+ explicitBounds &&
2887
+ SchematicSvgRenderer.#containsPointWithTolerance(
2888
+ explicitBounds,
2889
+ x,
2890
+ sourceY,
2891
+ tolerance
2892
+ )
2893
+ ) {
2894
+ return explicitBounds
2895
+ }
2896
+
2897
+ if (ownerIndex) {
2898
+ return null
2899
+ }
2900
+
2901
+ return SchematicSvgRenderer.#findContainingOwnerBodyBounds(
2902
+ ownerTextBodyBounds,
2903
+ x,
2904
+ sourceY,
2905
+ tolerance
2906
+ )
2907
+ }
2908
+
2909
+ /**
2910
+ * Finds the smallest owner body containing one ownerless overlay label.
2911
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
2912
+ * @param {number} x Text X coordinate.
2913
+ * @param {number} y Text baseline Y coordinate.
2914
+ * @param {number} tolerance Coordinate tolerance.
2915
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2916
+ */
2917
+ static #findContainingOwnerBodyBounds(
2918
+ ownerTextBodyBounds,
2919
+ x,
2920
+ y,
2921
+ tolerance
2922
+ ) {
2923
+ let bestBounds = null
2924
+ let bestArea = Number.POSITIVE_INFINITY
2925
+
2926
+ for (const bounds of ownerTextBodyBounds.values()) {
2927
+ if (
2928
+ !SchematicSvgRenderer.#containsPointWithTolerance(
2929
+ bounds,
2930
+ x,
2931
+ y,
2932
+ tolerance
2933
+ )
2934
+ ) {
2935
+ continue
2936
+ }
2937
+
2938
+ const area =
2939
+ Math.max(Number(bounds.maxX) - Number(bounds.minX), 0) *
2940
+ Math.max(Number(bounds.maxY) - Number(bounds.minY), 0)
2941
+
2942
+ if (area < bestArea) {
2943
+ bestArea = area
2944
+ bestBounds = bounds
2945
+ }
2946
+ }
2947
+
2948
+ return bestBounds
2949
+ }
2950
+
2951
+ /**
2952
+ * Resolves the internal horizontal-line cell containing one text label.
2953
+ * @param {number} x Text X coordinate.
2954
+ * @param {number} y Text Y coordinate.
2955
+ * @param {number} fontSize Viewer font size.
2956
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
2957
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
2958
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
2959
+ */
2960
+ static #resolveOwnerBodyTextLineCellBounds(
2961
+ text,
2962
+ x,
2963
+ y,
2964
+ fontSize,
2965
+ bounds,
2966
+ lines
2967
+ ) {
2968
+ const separators =
2969
+ SchematicSvgRenderer.#collectOwnerBodyHorizontalSeparators(
2970
+ x,
2971
+ fontSize,
2972
+ bounds,
2973
+ lines
2974
+ )
2975
+
2976
+ if (separators.length === 0) {
2977
+ return null
2978
+ }
2979
+
2980
+ const edges = [
2981
+ Number(bounds.minY),
2982
+ ...separators,
2983
+ Number(bounds.maxY)
2984
+ ].sort((left, right) => left - right)
2985
+ const tolerance = Math.max(fontSize * 0.4, 1)
2986
+ const descent =
2987
+ fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
2988
+ const sampleY = y + (fontSize * BODY_TEXT_ASCENT_RATIO - descent) / 2
2989
+
2990
+ for (let index = 0; index < edges.length - 1; index += 1) {
2991
+ const minY = edges[index]
2992
+ const maxY = edges[index + 1]
2993
+
2994
+ if (sampleY >= minY && sampleY <= maxY) {
2995
+ return {
2996
+ minX: bounds.minX,
2997
+ maxX: bounds.maxX,
2998
+ minY,
2999
+ maxY
3000
+ }
3001
+ }
3002
+ }
3003
+
3004
+ for (let index = 0; index < edges.length - 1; index += 1) {
3005
+ const minY = edges[index]
3006
+ const maxY = edges[index + 1]
3007
+
3008
+ if (sampleY >= minY - tolerance && sampleY <= maxY + tolerance) {
3009
+ return {
3010
+ minX: bounds.minX,
3011
+ maxX: bounds.maxX,
3012
+ minY,
3013
+ maxY
3014
+ }
3015
+ }
3016
+ }
3017
+
3018
+ return null
3019
+ }
3020
+
3021
+ /**
3022
+ * Collects horizontal separators that behave like internal body cell rows.
3023
+ * @param {number} x Text X coordinate.
3024
+ * @param {number} fontSize Viewer font size.
3025
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
3026
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
3027
+ * @returns {number[]}
3028
+ */
3029
+ static #collectOwnerBodyHorizontalSeparators(x, fontSize, bounds, lines) {
3030
+ const tolerance = Math.max(fontSize * 0.4, 1)
3031
+ const bodyWidth = Math.max(Number(bounds.maxX) - Number(bounds.minX), 0)
3032
+ const minSpan = Math.max(
3033
+ bodyWidth * BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO,
3034
+ fontSize * 1.5
3035
+ )
3036
+ const separators = new Set()
3037
+
3038
+ for (const line of lines || []) {
3039
+ const y1 = Number(line?.y1)
3040
+ const y2 = Number(line?.y2)
3041
+ const x1 = Number(line?.x1)
3042
+ const x2 = Number(line?.x2)
3043
+
3044
+ if (
3045
+ !Number.isFinite(x1) ||
3046
+ !Number.isFinite(x2) ||
3047
+ !Number.isFinite(y1) ||
3048
+ !Number.isFinite(y2) ||
3049
+ Math.abs(y1 - y2) > tolerance
3050
+ ) {
3051
+ continue
3052
+ }
3053
+
3054
+ const minX = Math.min(x1, x2)
3055
+ const maxX = Math.max(x1, x2)
3056
+ const span = maxX - minX
3057
+
3058
+ if (span < minSpan) continue
3059
+ if (y1 <= bounds.minY + tolerance) continue
3060
+ if (y1 >= bounds.maxY - tolerance) continue
3061
+ if (minX < bounds.minX - tolerance) continue
3062
+ if (maxX > bounds.maxX + tolerance) continue
3063
+ if (x < minX - tolerance || x > maxX + tolerance) continue
3064
+
3065
+ separators.add(Number(formatNumber(y1)))
3066
+ }
3067
+
3068
+ return [...separators].sort((left, right) => left - right)
3069
+ }
3070
+
3071
+ /**
3072
+ * Clamps a projected SVG text baseline so its visual box stays in bounds.
3073
+ * @param {{ text?: string, resolvedText?: string }} text Text primitive.
3074
+ * @param {number} y Candidate SVG baseline.
3075
+ * @param {{ minY: number, maxY: number }} bounds Unprojected source bounds.
3076
+ * @param {number} sheetHeight Rendered sheet height.
3077
+ * @param {number} fontSize Viewer font size.
3078
+ * @returns {number}
3079
+ */
3080
+ static #clampTextBaselineToBounds(text, y, bounds, sheetHeight, fontSize) {
3081
+ const top = projectSchematicY(sheetHeight, bounds.maxY)
3082
+ const bottom = projectSchematicY(sheetHeight, bounds.minY)
3083
+ const ascent = fontSize * BODY_TEXT_ASCENT_RATIO
3084
+ const descent =
3085
+ fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
3086
+ const visualHeight = ascent + descent
3087
+
3088
+ if (bottom - top < visualHeight) {
3089
+ return (top + bottom) / 2 + (ascent - descent) / 2
3090
+ }
3091
+
3092
+ let resolvedY = y
3093
+ const visualTop = resolvedY - ascent
3094
+ if (visualTop < top) {
3095
+ resolvedY += top - visualTop
3096
+ }
3097
+
3098
+ const visualBottom = resolvedY + descent
3099
+ if (visualBottom > bottom) {
3100
+ resolvedY -= visualBottom - bottom
3101
+ }
3102
+
3103
+ return resolvedY
3104
+ }
3105
+
3106
+ /**
3107
+ * Estimates the descent needed for body-cell text clamping.
3108
+ * @param {{ text?: string, resolvedText?: string }} text Text primitive.
3109
+ * @returns {number}
3110
+ */
3111
+ static #resolveBodyTextDescentRatio(text) {
3112
+ const value = String(text?.resolvedText ?? text?.text ?? '')
3113
+
3114
+ return BODY_TEXT_DESCENDER_PATTERN.test(value)
3115
+ ? BODY_TEXT_DESCENT_RATIO
3116
+ : BODY_TEXT_FLAT_DESCENT_RATIO
3117
+ }
3118
+
3119
+ /**
3120
+ * Returns true when a point falls inside bounds with tolerance.
3121
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Bounds.
3122
+ * @param {number} x X coordinate.
3123
+ * @param {number} y Y coordinate.
3124
+ * @param {number} tolerance Coordinate tolerance.
3125
+ * @returns {boolean}
3126
+ */
3127
+ static #containsPointWithTolerance(bounds, x, y, tolerance) {
3128
+ const numericX = Number(x)
3129
+ const numericY = Number(y)
3130
+
3131
+ return (
3132
+ Number.isFinite(numericX) &&
3133
+ Number.isFinite(numericY) &&
3134
+ numericX >= bounds.minX - tolerance &&
3135
+ numericX <= bounds.maxX + tolerance &&
3136
+ numericY >= bounds.minY - tolerance &&
3137
+ numericY <= bounds.maxY + tolerance
3138
+ )
3139
+ }
3140
+
3141
+ /**
3142
+ * Adds the SVG baseline correction for mirrored owner-local symbol text
3143
+ * authored with the native orientation that stores the top of the text run.
3144
+ * Baseline-oriented mirrored labels already use SVG-like y coordinates.
3145
+ * @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
3146
+ * @param {number} x Render X coordinate.
3147
+ * @param {number} sourceY Unprojected source Y coordinate.
3148
+ * @param {number} fontSize Viewer font size.
3149
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
3150
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
3151
+ * @returns {number}
3152
+ */
3153
+ static #resolveOwnerBodyTextBaselineOffset(
3154
+ text,
3155
+ x,
3156
+ sourceY,
3157
+ fontSize,
3158
+ matchedOwnerPin,
3159
+ ownerTextBodyBounds
3160
+ ) {
3161
+ if (
3162
+ !SchematicSvgRenderer.#isOwnerBodyBaselineText(
3163
+ text,
3164
+ x,
3165
+ sourceY,
3166
+ fontSize,
3167
+ matchedOwnerPin,
3168
+ ownerTextBodyBounds
3169
+ )
3170
+ ) {
3171
+ return 0
3172
+ }
3173
+
3174
+ return fontSize
3175
+ }
3176
+
3177
+ /**
3178
+ * Returns true when one text primitive is a top-origin mirrored owner-local
3179
+ * symbol annotation sitting inside its owner's drawable body.
3180
+ * @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
3181
+ * @param {number} x Render X coordinate.
3182
+ * @param {number} sourceY Unprojected source Y coordinate.
3183
+ * @param {number} fontSize Viewer font size.
3184
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
3185
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
3186
+ * @returns {boolean}
3187
+ */
3188
+ static #isOwnerBodyBaselineText(
3189
+ text,
3190
+ x,
3191
+ sourceY,
3192
+ fontSize,
3193
+ matchedOwnerPin,
3194
+ ownerTextBodyBounds
3195
+ ) {
3196
+ if (matchedOwnerPin) return false
3197
+ if (text.recordType !== '4') return false
3198
+ if (!text.isMirrored) return false
3199
+ if (Number(text.sourceOrientation ?? 0) !== 2) return false
3200
+ if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
3201
+ return false
3202
+ }
3203
+ if (!Number.isFinite(fontSize) || fontSize <= 0) return false
3204
+
3205
+ const ownerIndex = String(text.ownerIndex || '').trim()
3206
+ if (!ownerIndex) return false
3207
+
3208
+ const bounds = ownerTextBodyBounds.get(ownerIndex)
3209
+ if (!bounds) return false
3210
+
3211
+ const numericX = Number(x)
3212
+ const numericY = Number(sourceY)
3213
+ const tolerance = Math.max(fontSize * 0.4, 1)
3214
+
3215
+ return (
3216
+ Number.isFinite(numericX) &&
3217
+ Number.isFinite(numericY) &&
3218
+ numericX >= bounds.minX - tolerance &&
3219
+ numericX <= bounds.maxX + tolerance &&
3220
+ numericY >= bounds.minY - tolerance &&
3221
+ numericY <= bounds.maxY + tolerance
3222
+ )
3223
+ }
3224
+
2134
3225
  /**
2135
3226
  * Lifts large section headings clear of authored dash-dot frame baselines.
2136
3227
  * @param {{ x: number, y: number, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number }} text
@@ -2229,11 +3320,18 @@ export class SchematicSvgRenderer {
2229
3320
 
2230
3321
  /**
2231
3322
  * Builds one schematic cross marker.
2232
- * @param {{ x: number, y: number, size: number, color: string }} cross
3323
+ * @param {{ x: number, y: number, size: number, color: string, symbolName?: string }} cross
2233
3324
  * @param {number} sheetHeight
2234
3325
  * @returns {string}
2235
3326
  */
2236
3327
  static #buildSchematicCrossMarkup(cross, sheetHeight) {
3328
+ if (String(cross.symbolName || '').toLowerCase() === 'checkbox') {
3329
+ return SchematicSvgRenderer.#buildSchematicCheckboxMarkup(
3330
+ cross,
3331
+ sheetHeight
3332
+ )
3333
+ }
3334
+
2237
3335
  const x = cross.x
2238
3336
  const y = projectSchematicY(sheetHeight, cross.y)
2239
3337
  const half = Math.max(Number(cross.size || 6), 4) / 2
@@ -2273,6 +3371,64 @@ export class SchematicSvgRenderer {
2273
3371
  )
2274
3372
  }
2275
3373
 
3374
+ /**
3375
+ * Builds one checkbox-shaped no-ERC marker.
3376
+ * @param {{ x: number, y: number, size: number, color: string }} cross
3377
+ * @param {number} sheetHeight
3378
+ * @returns {string}
3379
+ */
3380
+ static #buildSchematicCheckboxMarkup(cross, sheetHeight) {
3381
+ const x = Number(cross.x)
3382
+ const y = projectSchematicY(sheetHeight, cross.y)
3383
+ const size = Math.max(Number(cross.size || 6), 4)
3384
+ const left = x - 1
3385
+ const top = y - size - 7
3386
+ const color = escapeHtml(
3387
+ SchematicColorResolver.resolveColor(
3388
+ cross.color,
3389
+ '--schematic-alert-color'
3390
+ )
3391
+ )
3392
+
3393
+ return (
3394
+ '<g class="schematic-cross schematic-cross--checkbox"><line x1="' +
3395
+ formatNumber(x) +
3396
+ '" y1="' +
3397
+ formatNumber(y) +
3398
+ '" x2="' +
3399
+ formatNumber(left) +
3400
+ '" y2="' +
3401
+ formatNumber(top + size) +
3402
+ '" stroke="' +
3403
+ color +
3404
+ '" /><rect x="' +
3405
+ formatNumber(left) +
3406
+ '" y="' +
3407
+ formatNumber(top) +
3408
+ '" width="' +
3409
+ formatNumber(size) +
3410
+ '" height="' +
3411
+ formatNumber(size) +
3412
+ '" fill="none" stroke="' +
3413
+ color +
3414
+ '" /><polyline points="' +
3415
+ formatNumber(left + size * 0.2) +
3416
+ ',' +
3417
+ formatNumber(top + size * 0.5) +
3418
+ ' ' +
3419
+ formatNumber(left + size * 0.5) +
3420
+ ',' +
3421
+ formatNumber(top + size * 0.8) +
3422
+ ' ' +
3423
+ formatNumber(left + size * 0.9666666667) +
3424
+ ',' +
3425
+ formatNumber(top + size * 0.2) +
3426
+ '" fill="none" stroke="' +
3427
+ color +
3428
+ '" /></g>'
3429
+ )
3430
+ }
3431
+
2276
3432
  /**
2277
3433
  * Builds one authored schematic junction dot.
2278
3434
  * @param {{ x: number, y: number, color: string }} junction
@@ -2287,7 +3443,7 @@ export class SchematicSvgRenderer {
2287
3443
  formatNumber(projectSchematicY(sheetHeight, junction.y)) +
2288
3444
  '" r="2.4" fill="' +
2289
3445
  escapeHtml(
2290
- SchematicColorResolver.resolveColor(
3446
+ SchematicColorResolver.resolveNonTextColor(
2291
3447
  junction.color,
2292
3448
  '--schematic-default-ink-color'
2293
3449
  )
@@ -2321,30 +3477,13 @@ export class SchematicSvgRenderer {
2321
3477
  static #resolveAuthoredSchematicJunctionColor(junction, lines) {
2322
3478
  const connectedLine = lines.find(
2323
3479
  (line) =>
2324
- SchematicSvgRenderer.#isElectricalSchematicLine(line) &&
3480
+ SchematicLineColorResolver.isElectricalLine(line) &&
2325
3481
  SchematicSvgRenderer.#schematicLineContainsPoint(line, junction)
2326
3482
  )
2327
3483
 
2328
3484
  return connectedLine?.color || junction.color
2329
3485
  }
2330
3486
 
2331
- /**
2332
- * Returns true when one normalized line can carry schematic net color.
2333
- * @param {{ ownerIndex?: string, isBus?: boolean, recordType?: string } | null | undefined} line
2334
- * @returns {boolean}
2335
- */
2336
- static #isElectricalSchematicLine(line) {
2337
- if (line?.ownerIndex || line?.isBus === true) {
2338
- return false
2339
- }
2340
-
2341
- if (!Object.prototype.hasOwnProperty.call(line || {}, 'recordType')) {
2342
- return true
2343
- }
2344
-
2345
- return !['6', '7', '26'].includes(String(line.recordType || ''))
2346
- }
2347
-
2348
3487
  /**
2349
3488
  * Returns true when one schematic line segment contains the given point.
2350
3489
  * @param {{ x1: number, y1: number, x2: number, y2: number }} line