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.
Files changed (39) 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/AltiumLibraryBatchExporter.mjs +206 -0
  8. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  9. package/src/core/altium/AltiumParser.mjs +5 -2
  10. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  11. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  12. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  13. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  14. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  15. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  16. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  17. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  18. package/src/core/altium/SchematicPinParser.mjs +87 -20
  19. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  20. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  21. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  22. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  23. package/src/core/altium/SourceComponentClient.mjs +239 -0
  24. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  25. package/src/parser.mjs +8 -0
  26. package/src/styles/altium-renderers.css +6 -6
  27. package/src/ui/PcbArcUtils.mjs +19 -2
  28. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  29. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  30. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  31. package/src/ui/SchematicColorResolver.mjs +185 -0
  32. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  33. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  34. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  35. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  36. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  37. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  38. package/src/ui/SchematicShapeRenderer.mjs +37 -11
  39. package/src/ui/SchematicSvgRenderer.mjs +944 -51
@@ -24,6 +24,7 @@ export class SchematicPinSvgRenderer {
24
24
  * @param {Map<string, number>} explicitOwnerPinLabelOffsets
25
25
  * @param {Map<string, 'left' | 'right'>} compactExternalNumberLabelSides
26
26
  * @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
27
+ * @param {Set<string>} overlappingExternalNumberLabelKeys
27
28
  * @returns {string}
28
29
  */
29
30
  static buildMarkup(
@@ -34,7 +35,8 @@ export class SchematicPinSvgRenderer {
34
35
  explicitOwnerPinNameLabels,
35
36
  explicitOwnerPinLabelOffsets,
36
37
  compactExternalNumberLabelSides = new Map(),
37
- internalNumberLabelBoxes = new Map()
38
+ internalNumberLabelBoxes = new Map(),
39
+ overlappingExternalNumberLabelKeys = new Set()
38
40
  ) {
39
41
  const geometry =
40
42
  SchematicPinSvgRenderer.#projectSchematicPinGeometry(pin)
@@ -53,6 +55,11 @@ export class SchematicPinSvgRenderer {
53
55
  const labelMode = pin.labelMode || 'name-and-number'
54
56
  const outerMarkerStyle =
55
57
  SchematicPinSvgRenderer.#resolveSchematicOuterPinMarkerStyle(pin)
58
+ const markerStyle =
59
+ outerMarkerStyle ||
60
+ SchematicPinSvgRenderer.#resolveSchematicElectricalPinMarkerStyle(
61
+ pin
62
+ )
56
63
  const rotateTopNumber =
57
64
  pin.orientation === 'top' &&
58
65
  rotatedVerticalNumberOwners.has(String(pin.ownerIndex || ''))
@@ -69,6 +76,13 @@ export class SchematicPinSvgRenderer {
69
76
  null
70
77
  const internalNumberLabelBox =
71
78
  internalNumberLabelBoxes.get(String(pin.ownerIndex || '')) || null
79
+ const shouldRenderExternalNumber =
80
+ !overlappingExternalNumberLabelKeys.has(
81
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
82
+ pin.ownerIndex,
83
+ pin.designator
84
+ )
85
+ )
72
86
 
73
87
  if (pin.orientation === 'left') {
74
88
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
@@ -82,7 +96,7 @@ export class SchematicPinSvgRenderer {
82
96
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
83
97
  'schematic-pin-name',
84
98
  internalNumberPlacement.x,
85
- projectedY - 1,
99
+ projectedY + 3,
86
100
  pin,
87
101
  labelColor,
88
102
  internalNumberPlacement.anchor,
@@ -91,16 +105,14 @@ export class SchematicPinSvgRenderer {
91
105
  )
92
106
  }
93
107
 
94
- const defaultNumberX = compactExternalNumberLabelSide
95
- ? SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberX(
96
- pin,
97
- geometry
98
- )
99
- : geometry.bodyX -
100
- SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
101
- outerMarkerStyle,
102
- pin
103
- )
108
+ const defaultNumberX =
109
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberX(
110
+ pin,
111
+ geometry,
112
+ markerStyle,
113
+ compactExternalNumberLabelSide,
114
+ internalNumberLabelBox
115
+ )
104
116
  const numberX = hasExplicitOwnerPinName
105
117
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
106
118
  pin,
@@ -108,25 +120,31 @@ export class SchematicPinSvgRenderer {
108
120
  explicitOwnerPinLabelOffsets
109
121
  )
110
122
  : defaultNumberX
111
- texts.push(
112
- createSvgText(
113
- 'schematic-pin-number',
114
- numberX,
115
- projectedY - 1,
116
- pin.designator,
117
- labelColor,
118
- 'end',
119
- textOptions
123
+ if (shouldRenderExternalNumber) {
124
+ texts.push(
125
+ createSvgText(
126
+ 'schematic-pin-number',
127
+ numberX,
128
+ projectedY - 1,
129
+ pin.designator,
130
+ labelColor,
131
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberAnchor(
132
+ pin,
133
+ markerStyle
134
+ ),
135
+ textOptions
136
+ )
120
137
  )
121
- )
138
+ }
122
139
  }
123
140
 
124
141
  if (
125
142
  labelMode !== 'hidden' &&
126
143
  labelMode !== 'number-only' &&
127
- pin.name &&
128
- pin.name !== pin.designator &&
129
- !hasExplicitOwnerPinName
144
+ SchematicPinSvgRenderer.#shouldRenderHorizontalPinName(
145
+ pin,
146
+ hasExplicitOwnerPinName
147
+ )
130
148
  ) {
131
149
  texts.push(
132
150
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
@@ -158,7 +176,7 @@ export class SchematicPinSvgRenderer {
158
176
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
159
177
  'schematic-pin-name',
160
178
  internalNumberPlacement.x,
161
- projectedY - 1,
179
+ projectedY + 3,
162
180
  pin,
163
181
  labelColor,
164
182
  internalNumberPlacement.anchor,
@@ -167,16 +185,14 @@ export class SchematicPinSvgRenderer {
167
185
  )
168
186
  }
169
187
 
170
- const defaultNumberX = compactExternalNumberLabelSide
171
- ? SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberX(
172
- pin,
173
- geometry
174
- )
175
- : geometry.bodyX +
176
- SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
177
- outerMarkerStyle,
178
- pin
179
- )
188
+ const defaultNumberX =
189
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberX(
190
+ pin,
191
+ geometry,
192
+ markerStyle,
193
+ compactExternalNumberLabelSide,
194
+ internalNumberLabelBox
195
+ )
180
196
  const numberX = hasExplicitOwnerPinName
181
197
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
182
198
  pin,
@@ -184,25 +200,31 @@ export class SchematicPinSvgRenderer {
184
200
  explicitOwnerPinLabelOffsets
185
201
  )
186
202
  : defaultNumberX
187
- texts.push(
188
- createSvgText(
189
- 'schematic-pin-number',
190
- numberX,
191
- projectedY - 1,
192
- pin.designator,
193
- labelColor,
194
- 'start',
195
- textOptions
203
+ if (shouldRenderExternalNumber) {
204
+ texts.push(
205
+ createSvgText(
206
+ 'schematic-pin-number',
207
+ numberX,
208
+ projectedY - 1,
209
+ pin.designator,
210
+ labelColor,
211
+ SchematicPinSvgRenderer.#resolveExternalHorizontalNumberAnchor(
212
+ pin,
213
+ markerStyle
214
+ ),
215
+ textOptions
216
+ )
196
217
  )
197
- )
218
+ }
198
219
  }
199
220
 
200
221
  if (
201
222
  labelMode !== 'hidden' &&
202
223
  labelMode !== 'number-only' &&
203
- pin.name &&
204
- pin.name !== pin.designator &&
205
- !hasExplicitOwnerPinName
224
+ SchematicPinSvgRenderer.#shouldRenderHorizontalPinName(
225
+ pin,
226
+ hasExplicitOwnerPinName
227
+ )
206
228
  ) {
207
229
  texts.push(
208
230
  SchematicPinSvgRenderer.#buildPinNameTextMarkup(
@@ -290,12 +312,7 @@ export class SchematicPinSvgRenderer {
290
312
  '" y2="' +
291
313
  formatNumber(projectedOuterY) +
292
314
  '" stroke="' +
293
- escapeHtml(
294
- SchematicColorResolver.resolveColor(
295
- pin.color,
296
- '--schematic-accent-ink-color'
297
- )
298
- ) +
315
+ escapeHtml(SchematicPinSvgRenderer.#resolvePinStrokeColor(pin)) +
299
316
  '" />' +
300
317
  markerMarkup +
301
318
  texts.join('') +
@@ -322,6 +339,19 @@ export class SchematicPinSvgRenderer {
322
339
  )
323
340
  }
324
341
 
342
+ const electricalMarkerStyle =
343
+ SchematicPinSvgRenderer.#resolveSchematicElectricalPinMarkerStyle(
344
+ pin
345
+ )
346
+ if (electricalMarkerStyle) {
347
+ return SchematicPinSvgRenderer.#buildOuterPinMarkerMarkup(
348
+ pin,
349
+ geometry,
350
+ sheetHeight,
351
+ electricalMarkerStyle
352
+ )
353
+ }
354
+
325
355
  if (Number(pin.electrical || 0) !== 1) {
326
356
  return ''
327
357
  }
@@ -444,23 +474,50 @@ export class SchematicPinSvgRenderer {
444
474
  /**
445
475
  * Returns the horizontal pin-number clearance needed by the pin geometry.
446
476
  * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
447
- * @param {{ length?: number }} pin
477
+ * @param {{ length?: number, electrical?: number }} pin
448
478
  * @returns {number}
449
479
  */
450
480
  static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
451
481
  switch (markerStyle) {
452
482
  case 'double':
453
- return 17
483
+ return 21
454
484
  case 'cross':
455
- return 12
485
+ return 9
456
486
  case 'single-in':
457
487
  case 'single-out':
458
488
  return 8
459
489
  default:
490
+ if (Number(pin?.electrical || 0) === 1) {
491
+ return 16
492
+ }
493
+
460
494
  return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 2)
461
495
  }
462
496
  }
463
497
 
498
+ /**
499
+ * Returns whether a horizontal pin name should be rendered inside the body.
500
+ * Numeric name/designator matches are duplicate pin numbers; nonnumeric
501
+ * matches such as PAD are valid body labels.
502
+ * @param {{ name?: string, designator?: string }} pin Pin primitive.
503
+ * @param {boolean} hasExplicitOwnerPinName Whether an explicit owner text already draws the label.
504
+ * @returns {boolean}
505
+ */
506
+ static #shouldRenderHorizontalPinName(pin, hasExplicitOwnerPinName) {
507
+ const name = String(pin?.name || '').trim()
508
+ const designator = String(pin?.designator || '').trim()
509
+
510
+ if (!name || hasExplicitOwnerPinName) {
511
+ return false
512
+ }
513
+
514
+ if (name !== designator) {
515
+ return true
516
+ }
517
+
518
+ return !/^\d+$/u.test(name)
519
+ }
520
+
464
521
  /**
465
522
  * Returns the horizontal pin-name inset used inside the symbol body.
466
523
  * @param {{ length?: number }} pin
@@ -472,7 +529,7 @@ export class SchematicPinSvgRenderer {
472
529
  return 10
473
530
  }
474
531
 
475
- return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 4)
532
+ return SchematicPinSvgRenderer.#resolveLongPinInset(pin, 7)
476
533
  }
477
534
 
478
535
  /**
@@ -491,6 +548,70 @@ export class SchematicPinSvgRenderer {
491
548
  return fallback === 2 ? 10 : 8
492
549
  }
493
550
 
551
+ /**
552
+ * Places synthetic horizontal pin numbers outside the symbol body. Owner
553
+ * bodies with internal numeric labels need a stub-side offset so the
554
+ * outside contact number does not collapse onto the inside duplicate.
555
+ * @param {{ length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin
556
+ * @param {{ bodyX: number }} geometry
557
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle
558
+ * @param {'left' | 'right' | null} compactExternalNumberLabelSide
559
+ * @param {{ left: number, right: number } | null} internalNumberLabelBox
560
+ * @returns {number}
561
+ */
562
+ static #resolveExternalHorizontalNumberX(
563
+ pin,
564
+ geometry,
565
+ markerStyle,
566
+ compactExternalNumberLabelSide,
567
+ internalNumberLabelBox
568
+ ) {
569
+ const clearance =
570
+ SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
571
+ markerStyle,
572
+ pin
573
+ )
574
+ const offset =
575
+ compactExternalNumberLabelSide || internalNumberLabelBox
576
+ ? Math.max(
577
+ clearance,
578
+ SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberOffset(
579
+ pin
580
+ )
581
+ )
582
+ : clearance
583
+
584
+ if (pin.orientation === 'left') {
585
+ return geometry.bodyX - offset
586
+ }
587
+
588
+ if (pin.orientation === 'right') {
589
+ return geometry.bodyX + offset
590
+ }
591
+
592
+ return geometry.bodyX
593
+ }
594
+
595
+ /**
596
+ * Resolves the text edge used for external horizontal pin numbers.
597
+ * Double/electrical marker labels are authored from the route side back
598
+ * toward the marker, while ordinary pins extend away from the body.
599
+ * @param {{ orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number }} pin Pin primitive.
600
+ * @param {'single-in' | 'single-out' | 'double' | 'cross' | null} markerStyle Marker style.
601
+ * @returns {'start' | 'end'}
602
+ */
603
+ static #resolveExternalHorizontalNumberAnchor(pin, markerStyle) {
604
+ const routeFacing =
605
+ markerStyle === 'double' ||
606
+ (!markerStyle && Number(pin?.electrical || 0) === 1)
607
+
608
+ if (routeFacing) {
609
+ return pin.orientation === 'left' ? 'start' : 'end'
610
+ }
611
+
612
+ return pin.orientation === 'left' ? 'end' : 'start'
613
+ }
614
+
494
615
  /**
495
616
  * Places numeric-only labels just inside a rectangular owner body.
496
617
  * @param {{ name?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
@@ -517,7 +638,7 @@ export class SchematicPinSvgRenderer {
517
638
  return null
518
639
  }
519
640
 
520
- const inset = Math.min(6, Math.max(3, (right - left) / 10))
641
+ const inset = Math.min(10, Math.max(6, (right - left) / 6))
521
642
 
522
643
  return {
523
644
  x: pin.orientation === 'left' ? left + inset : right - inset,
@@ -532,8 +653,10 @@ export class SchematicPinSvgRenderer {
532
653
  * @returns {number}
533
654
  */
534
655
  static #resolveCompactExternalHorizontalNumberX(pin, geometry) {
535
- const length = Math.abs(Number(pin.length || 0))
536
- const offset = Math.max(8, Math.min(length - 6, 12))
656
+ const offset =
657
+ SchematicPinSvgRenderer.#resolveCompactExternalHorizontalNumberOffset(
658
+ pin
659
+ )
537
660
 
538
661
  if (pin.orientation === 'left') {
539
662
  return geometry.bodyX - offset
@@ -546,6 +669,17 @@ export class SchematicPinSvgRenderer {
546
669
  return geometry.bodyX
547
670
  }
548
671
 
672
+ /**
673
+ * Resolves a stub-side offset for compact external horizontal numbers.
674
+ * @param {{ length?: number }} pin
675
+ * @returns {number}
676
+ */
677
+ static #resolveCompactExternalHorizontalNumberOffset(pin) {
678
+ const length = Math.abs(Number(pin.length || 0))
679
+
680
+ return Math.max(8, Math.min(length - 6, 12))
681
+ }
682
+
549
683
  /**
550
684
  * Places compact owner-drawn vertical pin numbers on the same side as the
551
685
  * horizontal terminal contacts.
@@ -672,6 +806,39 @@ export class SchematicPinSvgRenderer {
672
806
  }
673
807
  }
674
808
 
809
+ /**
810
+ * Resolves marker styles derived from the pin electrical type. The
811
+ * bidirectional type keeps its legacy two-triangle geometry path.
812
+ * @param {{ electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
813
+ * @returns {'single-in' | 'single-out' | null}
814
+ */
815
+ static #resolveSchematicElectricalPinMarkerStyle(pin) {
816
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
817
+ return null
818
+ }
819
+
820
+ switch (Number(pin.electrical)) {
821
+ case 0:
822
+ return 'single-in'
823
+ case 2:
824
+ return 'single-out'
825
+ default:
826
+ return null
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Resolves the visible stroke for a schematic pin stub.
832
+ * @param {{ color?: string }} pin
833
+ * @returns {string}
834
+ */
835
+ static #resolvePinStrokeColor(pin) {
836
+ return SchematicColorResolver.resolveNonTextColor(
837
+ pin?.color,
838
+ '--schematic-accent-ink-color'
839
+ )
840
+ }
841
+
675
842
  /**
676
843
  * Builds one pin-name text element, including overline tspans when needed.
677
844
  * @param {string} className
@@ -231,10 +231,12 @@ export class SchematicShapeRenderer {
231
231
  * @returns {string}
232
232
  */
233
233
  static buildRectangleMarkup(rectangle, sheetHeight) {
234
- const fill =
235
- SchematicShapeRenderer.#resolveSchematicRectangleFill(rectangle)
236
234
  const preserveSourceColors =
237
235
  SchematicShapeRenderer.#isOwnerColorStrip(rectangle)
236
+ const fill = SchematicShapeRenderer.#resolveSchematicRectangleFill(
237
+ rectangle,
238
+ preserveSourceColors
239
+ )
238
240
 
239
241
  return (
240
242
  '<rect class="schematic-rectangle" x="' +
@@ -250,7 +252,7 @@ export class SchematicShapeRenderer {
250
252
  '" fill="' +
251
253
  escapeHtml(
252
254
  preserveSourceColors
253
- ? SchematicColorResolver.resolveSourceFill(
255
+ ? SchematicColorResolver.resolveMutedSourceFill(
254
256
  fill,
255
257
  '--schematic-fill-color'
256
258
  )
@@ -262,7 +264,7 @@ export class SchematicShapeRenderer {
262
264
  '" stroke="' +
263
265
  escapeHtml(
264
266
  preserveSourceColors
265
- ? SchematicColorResolver.resolveSourceColor(
267
+ ? SchematicColorResolver.resolveMutedSourceColor(
266
268
  rectangle.color,
267
269
  '--schematic-default-ink-color'
268
270
  )
@@ -290,10 +292,12 @@ export class SchematicShapeRenderer {
290
292
  */
291
293
  static buildRoundedRectangleMarkup(rectangle, sheetHeight) {
292
294
  const radius = Math.max(Number(rectangle.radius || 0), 0)
293
- const fill =
294
- SchematicShapeRenderer.#resolveSchematicRectangleFill(rectangle)
295
295
  const preserveSourceColors =
296
296
  SchematicShapeRenderer.#isOwnerColorStrip(rectangle)
297
+ const fill = SchematicShapeRenderer.#resolveSchematicRectangleFill(
298
+ rectangle,
299
+ preserveSourceColors
300
+ )
297
301
 
298
302
  return (
299
303
  '<rect class="schematic-rounded-rectangle" x="' +
@@ -313,7 +317,7 @@ export class SchematicShapeRenderer {
313
317
  '" fill="' +
314
318
  escapeHtml(
315
319
  preserveSourceColors
316
- ? SchematicColorResolver.resolveSourceFill(
320
+ ? SchematicColorResolver.resolveMutedSourceFill(
317
321
  fill,
318
322
  '--schematic-fill-color'
319
323
  )
@@ -325,7 +329,7 @@ export class SchematicShapeRenderer {
325
329
  '" stroke="' +
326
330
  escapeHtml(
327
331
  preserveSourceColors
328
- ? SchematicColorResolver.resolveSourceColor(
332
+ ? SchematicColorResolver.resolveMutedSourceColor(
329
333
  rectangle.color,
330
334
  '--schematic-default-ink-color'
331
335
  )
@@ -470,19 +474,41 @@ export class SchematicShapeRenderer {
470
474
  /**
471
475
  * Resolves the visible fill for one schematic rectangle primitive.
472
476
  * @param {{ fill: string, isSolid: boolean, transparent: boolean }} rectangle
477
+ * @param {boolean} preserveSourceFill
473
478
  * @returns {string}
474
479
  */
475
- static #resolveSchematicRectangleFill(rectangle) {
480
+ static #resolveSchematicRectangleFill(rectangle, preserveSourceFill) {
476
481
  if (rectangle.transparent || !rectangle.isSolid) {
482
+ if (
483
+ preserveSourceFill &&
484
+ SchematicShapeRenderer.#hasVisibleSourceFill(rectangle)
485
+ ) {
486
+ return rectangle.fill
487
+ }
488
+
477
489
  return 'none'
478
490
  }
479
491
 
480
492
  return rectangle.fill || 'none'
481
493
  }
482
494
 
495
+ /**
496
+ * Returns true when a transparent owner color strip still carries an
497
+ * authored fill color that encodes symbol-side rail intent.
498
+ * @param {{ fill?: string }} rectangle
499
+ * @returns {boolean}
500
+ */
501
+ static #hasVisibleSourceFill(rectangle) {
502
+ const fill = String(rectangle?.fill || '')
503
+ .trim()
504
+ .toLowerCase()
505
+
506
+ return Boolean(fill) && fill !== 'none' && fill !== 'transparent'
507
+ }
508
+
483
509
  /**
484
510
  * Returns true for narrow symbol-owned color swatches such as per-section
485
- * strips that carry literal source palette meaning.
511
+ * strips that carry source palette meaning.
486
512
  * @param {{ width: number, height: number, ownerIndex?: string }} rectangle
487
513
  * @returns {boolean}
488
514
  */
@@ -496,7 +522,7 @@ export class SchematicShapeRenderer {
496
522
  const shortSide = Math.min(width, height)
497
523
  const longSide = Math.max(width, height)
498
524
 
499
- return shortSide > 0 && shortSide <= 8 && longSide >= 40
525
+ return shortSide > 0 && shortSide <= 8 && longSide >= 20
500
526
  }
501
527
 
502
528
  /**