altium-toolkit 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  3. package/src/core/altium/AltiumParser.mjs +240 -8
  4. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  5. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  6. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  7. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  8. package/src/core/altium/SchematicImageParser.mjs +291 -6
  9. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  10. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  11. package/src/core/altium/SchematicPinParser.mjs +175 -4
  12. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  13. package/src/core/altium/SchematicTextParser.mjs +125 -11
  14. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  15. package/src/ui/SchematicColorResolver.mjs +78 -0
  16. package/src/ui/SchematicContentLayout.mjs +58 -1
  17. package/src/ui/SchematicImageRenderer.mjs +125 -10
  18. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  19. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  20. package/src/ui/SchematicNoteRenderer.mjs +82 -6
  21. package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
  22. package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
  23. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  24. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  25. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  26. package/src/ui/SchematicShapeRenderer.mjs +82 -23
  27. package/src/ui/SchematicSvgRenderer.mjs +293 -47
@@ -157,7 +157,7 @@ export class SchematicShapeRenderer {
157
157
  pie.startAngle,
158
158
  pie.endAngle
159
159
  )
160
- const sweep = delta >= 0 ? 0 : 1
160
+ const sweep = SchematicShapeRenderer.#resolvePieSweep(delta)
161
161
  const center = {
162
162
  x: Number(pie.x) || 0,
163
163
  y: projectSchematicY(sheetHeight, Number(pie.y) || 0)
@@ -226,11 +226,16 @@ export class SchematicShapeRenderer {
226
226
 
227
227
  /**
228
228
  * Builds one schematic rectangle primitive.
229
- * @param {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number }} rectangle
229
+ * @param {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number, ownerIndex?: string }} rectangle
230
230
  * @param {number} sheetHeight
231
231
  * @returns {string}
232
232
  */
233
233
  static buildRectangleMarkup(rectangle, sheetHeight) {
234
+ const fill =
235
+ SchematicShapeRenderer.#resolveSchematicRectangleFill(rectangle)
236
+ const preserveSourceColors =
237
+ SchematicShapeRenderer.#isOwnerColorStrip(rectangle)
238
+
234
239
  return (
235
240
  '<rect class="schematic-rectangle" x="' +
236
241
  formatNumber(rectangle.x) +
@@ -244,19 +249,27 @@ export class SchematicShapeRenderer {
244
249
  formatNumber(rectangle.height) +
245
250
  '" fill="' +
246
251
  escapeHtml(
247
- SchematicColorResolver.resolveFill(
248
- SchematicShapeRenderer.#resolveSchematicRectangleFill(
249
- rectangle
250
- ),
251
- '--schematic-fill-color'
252
- )
252
+ preserveSourceColors
253
+ ? SchematicColorResolver.resolveSourceFill(
254
+ fill,
255
+ '--schematic-fill-color'
256
+ )
257
+ : SchematicColorResolver.resolveFill(
258
+ fill,
259
+ '--schematic-fill-color'
260
+ )
253
261
  ) +
254
262
  '" stroke="' +
255
263
  escapeHtml(
256
- SchematicColorResolver.resolveColor(
257
- rectangle.color,
258
- '--schematic-default-ink-color'
259
- )
264
+ preserveSourceColors
265
+ ? SchematicColorResolver.resolveSourceColor(
266
+ rectangle.color,
267
+ '--schematic-default-ink-color'
268
+ )
269
+ : SchematicColorResolver.resolveColor(
270
+ rectangle.color,
271
+ '--schematic-default-ink-color'
272
+ )
260
273
  ) +
261
274
  '" stroke-width="' +
262
275
  formatNumber(Math.max(rectangle.lineWidth || 1, 0.8)) +
@@ -271,12 +284,16 @@ export class SchematicShapeRenderer {
271
284
 
272
285
  /**
273
286
  * Builds one schematic rounded-rectangle primitive.
274
- * @param {{ x: number, y: number, width: number, height: number, radius?: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number }} rectangle
287
+ * @param {{ x: number, y: number, width: number, height: number, radius?: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, lineStyle?: number, ownerIndex?: string }} rectangle
275
288
  * @param {number} sheetHeight
276
289
  * @returns {string}
277
290
  */
278
291
  static buildRoundedRectangleMarkup(rectangle, sheetHeight) {
279
292
  const radius = Math.max(Number(rectangle.radius || 0), 0)
293
+ const fill =
294
+ SchematicShapeRenderer.#resolveSchematicRectangleFill(rectangle)
295
+ const preserveSourceColors =
296
+ SchematicShapeRenderer.#isOwnerColorStrip(rectangle)
280
297
 
281
298
  return (
282
299
  '<rect class="schematic-rounded-rectangle" x="' +
@@ -295,19 +312,27 @@ export class SchematicShapeRenderer {
295
312
  formatNumber(radius) +
296
313
  '" fill="' +
297
314
  escapeHtml(
298
- SchematicColorResolver.resolveFill(
299
- SchematicShapeRenderer.#resolveSchematicRectangleFill(
300
- rectangle
301
- ),
302
- '--schematic-fill-color'
303
- )
315
+ preserveSourceColors
316
+ ? SchematicColorResolver.resolveSourceFill(
317
+ fill,
318
+ '--schematic-fill-color'
319
+ )
320
+ : SchematicColorResolver.resolveFill(
321
+ fill,
322
+ '--schematic-fill-color'
323
+ )
304
324
  ) +
305
325
  '" stroke="' +
306
326
  escapeHtml(
307
- SchematicColorResolver.resolveColor(
308
- rectangle.color,
309
- '--schematic-default-ink-color'
310
- )
327
+ preserveSourceColors
328
+ ? SchematicColorResolver.resolveSourceColor(
329
+ rectangle.color,
330
+ '--schematic-default-ink-color'
331
+ )
332
+ : SchematicColorResolver.resolveColor(
333
+ rectangle.color,
334
+ '--schematic-default-ink-color'
335
+ )
311
336
  ) +
312
337
  '" stroke-width="' +
313
338
  formatNumber(Math.max(rectangle.lineWidth || 1, 0.8)) +
@@ -455,6 +480,25 @@ export class SchematicShapeRenderer {
455
480
  return rectangle.fill || 'none'
456
481
  }
457
482
 
483
+ /**
484
+ * Returns true for narrow symbol-owned color swatches such as per-section
485
+ * strips that carry literal source palette meaning.
486
+ * @param {{ width: number, height: number, ownerIndex?: string }} rectangle
487
+ * @returns {boolean}
488
+ */
489
+ static #isOwnerColorStrip(rectangle) {
490
+ if (!rectangle?.ownerIndex) {
491
+ return false
492
+ }
493
+
494
+ const width = Math.abs(Number(rectangle.width || 0))
495
+ const height = Math.abs(Number(rectangle.height || 0))
496
+ const shortSide = Math.min(width, height)
497
+ const longSide = Math.max(width, height)
498
+
499
+ return shortSide > 0 && shortSide <= 8 && longSide >= 40
500
+ }
501
+
458
502
  /**
459
503
  * Resolves the visible fill for one schematic ellipse primitive.
460
504
  * @param {{ fill: string, isSolid: boolean, transparent: boolean }} ellipse
@@ -766,4 +810,19 @@ export class SchematicShapeRenderer {
766
810
 
767
811
  return delta
768
812
  }
813
+
814
+ /**
815
+ * Resolves SVG sweep direction for filled Altium pie primitives.
816
+ * @param {number} delta Normalized source angle delta.
817
+ * @returns {number}
818
+ */
819
+ static #resolvePieSweep(delta) {
820
+ const magnitude = Math.abs(Number(delta) || 0)
821
+
822
+ if (Math.abs(magnitude - 180) <= 0.001) {
823
+ return 0
824
+ }
825
+
826
+ return delta >= 0 ? 0 : 1
827
+ }
769
828
  }
@@ -18,6 +18,7 @@ import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs
18
18
  import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
19
19
  import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
20
20
  import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
21
+ import { SchematicNativeFooterPartitioner } from './SchematicNativeFooterPartitioner.mjs'
21
22
  import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
22
23
  import { SchematicRenderOpsSidecarBuilder } from './SchematicRenderOpsSidecarBuilder.mjs'
23
24
  import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
@@ -38,7 +39,7 @@ export class SchematicSvgRenderer {
38
39
  /**
39
40
  * Renders a normalized schematic model into SVG markup.
40
41
  * @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.
42
+ * @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
42
43
  * @returns {string}
43
44
  */
44
45
  static render(documentModel, options = {}) {
@@ -66,7 +67,9 @@ export class SchematicSvgRenderer {
66
67
  renderedSheet.contentSheet === schematic.sheet
67
68
  ? schematic
68
69
  : { ...schematic, sheet: renderedSheet.contentSheet }
69
- const allTexts = schematic.texts || []
70
+ const allTexts = SchematicSvgRenderer.#filterDrawableSchematicTexts(
71
+ schematic.texts || []
72
+ )
70
73
  const lines = (schematic.lines || []).slice(0, 2500)
71
74
  const polygons = (schematic.polygons || []).slice(0, 1000)
72
75
  const rectangles = (schematic.rectangles || []).slice(0, 500)
@@ -146,25 +149,6 @@ export class SchematicSvgRenderer {
146
149
  renderedSchematic,
147
150
  contentClipId
148
151
  )
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
152
  const resolvedTexts = texts.map((text) =>
169
153
  text.recordType === '17'
170
154
  ? {
@@ -178,6 +162,61 @@ export class SchematicSvgRenderer {
178
162
  }
179
163
  : text
180
164
  )
165
+ const partitionedPrimitives =
166
+ SchematicNativeFooterPartitioner.partitionPrimitives(
167
+ {
168
+ lines,
169
+ polygons,
170
+ rectangles,
171
+ roundedRectangles,
172
+ ellipses,
173
+ arcs,
174
+ beziers,
175
+ pies,
176
+ ieeeSymbols,
177
+ texts: resolvedTexts,
178
+ images
179
+ },
180
+ renderedSchematic.sheet
181
+ )
182
+ const contentLines = SchematicSvgRenderer.#themeFooterChromeLines(
183
+ partitionedPrimitives.content.lines,
184
+ renderedSheet.sheet,
185
+ contentHeight
186
+ )
187
+ const contentPolygons = partitionedPrimitives.content.polygons
188
+ const contentRectangles = partitionedPrimitives.content.rectangles
189
+ const contentRoundedRectangles =
190
+ partitionedPrimitives.content.roundedRectangles
191
+ const contentEllipses = partitionedPrimitives.content.ellipses
192
+ const contentArcs = partitionedPrimitives.content.arcs
193
+ const contentBeziers = partitionedPrimitives.content.beziers
194
+ const contentPies = partitionedPrimitives.content.pies
195
+ const contentIeeeSymbols = partitionedPrimitives.content.ieeeSymbols
196
+ const contentTexts = partitionedPrimitives.content.texts
197
+ const contentImages = partitionedPrimitives.content.images
198
+ const footerPrimitives = partitionedPrimitives.footer
199
+ const ownerlessLines = contentLines.filter((line) => !line.ownerIndex)
200
+ const ownerlessPolygons = contentPolygons.filter(
201
+ (polygon) => !polygon.ownerIndex
202
+ )
203
+ const ownerlessRectangles = contentRectangles.filter(
204
+ (rectangle) => !rectangle.ownerIndex
205
+ )
206
+ const ownerlessRoundedRectangles = contentRoundedRectangles.filter(
207
+ (rectangle) => !rectangle.ownerIndex
208
+ )
209
+ const ownerlessEllipses = contentEllipses.filter(
210
+ (ellipse) => !ellipse.ownerIndex
211
+ )
212
+ const ownerlessArcs = contentArcs.filter((arc) => !arc.ownerIndex)
213
+ const ownerlessBeziers = contentBeziers.filter(
214
+ (bezier) => !bezier.ownerIndex
215
+ )
216
+ const ownerlessPies = contentPies.filter((pie) => !pie.ownerIndex)
217
+ const ownerlessIeeeSymbols = contentIeeeSymbols.filter(
218
+ (symbol) => !symbol.ownerIndex
219
+ )
181
220
  const polygonMarkup = ownerlessPolygons
182
221
  .map((polygon, index) =>
183
222
  SchematicSvgRenderer.#appendSvgAttributes(
@@ -357,15 +396,15 @@ export class SchematicSvgRenderer {
357
396
  .join('')
358
397
  const ownerGeometryMarkup =
359
398
  SchematicSvgRenderer.#buildOwnerGeometryMarkup(
360
- lines,
361
- polygons,
362
- rectangles,
363
- roundedRectangles,
364
- ellipses,
365
- arcs,
366
- beziers,
367
- pies,
368
- ieeeSymbols,
399
+ contentLines,
400
+ contentPolygons,
401
+ contentRectangles,
402
+ contentRoundedRectangles,
403
+ contentEllipses,
404
+ contentArcs,
405
+ contentBeziers,
406
+ contentPies,
407
+ contentIeeeSymbols,
369
408
  contentHeight,
370
409
  semanticContext
371
410
  )
@@ -390,7 +429,7 @@ export class SchematicSvgRenderer {
390
429
  const resolvedAuthoredJunctions =
391
430
  SchematicSvgRenderer.#resolveAuthoredSchematicJunctions(
392
431
  authoredJunctions,
393
- lines
432
+ contentLines
394
433
  )
395
434
  const authoredJunctionMarkup = resolvedAuthoredJunctions
396
435
  .map((junction) =>
@@ -401,18 +440,74 @@ export class SchematicSvgRenderer {
401
440
  )
402
441
  .join('')
403
442
  const imageMarkup = SchematicImageRenderer.buildMarkup(
404
- images,
405
- contentHeight
443
+ contentImages,
444
+ contentHeight,
445
+ {
446
+ colorizeImages: renderOptions.colorizeImages,
447
+ title:
448
+ renderModel?.summary?.title ||
449
+ renderedSchematic?.sheet?.titleBlock?.title ||
450
+ renderModel?.fileName ||
451
+ ''
452
+ }
406
453
  )
407
454
  const markerDefsMarkup =
408
- SchematicSvgRenderer.#buildSchematicLineMarkerDefs(lines)
455
+ SchematicSvgRenderer.#buildSchematicLineMarkerDefs(contentLines)
409
456
 
410
- const textMarkup = resolvedTexts
457
+ const textMarkup = contentTexts
411
458
  .map((text, index) =>
412
459
  SchematicSvgRenderer.#buildSchematicTextMarkup(
413
460
  text,
414
461
  contentHeight,
415
- lines,
462
+ contentLines,
463
+ pins,
464
+ SchematicSvgRenderer.#primitiveIndex(
465
+ semanticContext,
466
+ 'texts',
467
+ text,
468
+ index
469
+ ),
470
+ semanticContext
471
+ )
472
+ )
473
+ .join('')
474
+ const nativeFooterLines = SchematicSvgRenderer.#themeFooterChromeLines(
475
+ footerPrimitives.lines,
476
+ renderedSheet.sheet,
477
+ contentHeight
478
+ )
479
+ const nativeFooterOwnerGeometryMarkup =
480
+ SchematicSvgRenderer.#buildOwnerGeometryMarkup(
481
+ nativeFooterLines,
482
+ footerPrimitives.polygons,
483
+ footerPrimitives.rectangles,
484
+ footerPrimitives.roundedRectangles,
485
+ footerPrimitives.ellipses,
486
+ footerPrimitives.arcs,
487
+ footerPrimitives.beziers,
488
+ footerPrimitives.pies,
489
+ footerPrimitives.ieeeSymbols,
490
+ contentHeight,
491
+ semanticContext
492
+ )
493
+ const nativeFooterImageMarkup = SchematicImageRenderer.buildMarkup(
494
+ footerPrimitives.images,
495
+ contentHeight,
496
+ {
497
+ colorizeImages: renderOptions.colorizeImages,
498
+ title:
499
+ renderModel?.summary?.title ||
500
+ renderedSchematic?.sheet?.titleBlock?.title ||
501
+ renderModel?.fileName ||
502
+ ''
503
+ }
504
+ )
505
+ const nativeFooterTextMarkup = footerPrimitives.texts
506
+ .map((text, index) =>
507
+ SchematicSvgRenderer.#buildSchematicTextMarkup(
508
+ text,
509
+ contentHeight,
510
+ contentLines,
416
511
  pins,
417
512
  SchematicSvgRenderer.#primitiveIndex(
418
513
  semanticContext,
@@ -424,6 +519,36 @@ export class SchematicSvgRenderer {
424
519
  )
425
520
  )
426
521
  .join('')
522
+ const nativeFooterMargin = Math.max(
523
+ Number(renderedSchematic?.sheet?.marginWidth || 20),
524
+ 10
525
+ )
526
+ const nativeFooterOffsetX = partitionedPrimitives.footerBounds
527
+ ? Math.max(
528
+ width -
529
+ nativeFooterMargin -
530
+ partitionedPrimitives.footerBounds.maxX,
531
+ 0
532
+ )
533
+ : 0
534
+ const nativeFooterTransform =
535
+ nativeFooterOffsetX > 0.01
536
+ ? ' transform="translate(' +
537
+ formatNumber(nativeFooterOffsetX) +
538
+ ' 0)"'
539
+ : ''
540
+ const nativeFooterMarkup =
541
+ nativeFooterOwnerGeometryMarkup ||
542
+ nativeFooterImageMarkup ||
543
+ nativeFooterTextMarkup
544
+ ? '<g class="schematic-native-footer" stroke-linecap="round"' +
545
+ nativeFooterTransform +
546
+ '>' +
547
+ nativeFooterOwnerGeometryMarkup +
548
+ nativeFooterImageMarkup +
549
+ nativeFooterTextMarkup +
550
+ '</g>'
551
+ : ''
427
552
 
428
553
  const componentMarkup = drawableComponents
429
554
  .map((component, index) =>
@@ -451,6 +576,15 @@ export class SchematicSvgRenderer {
451
576
  texts,
452
577
  pins
453
578
  )
579
+ const compactExternalNumberLabelSides =
580
+ SchematicOwnerPinLabelLayout.collectCompactExternalNumberLabelSides(
581
+ pins
582
+ )
583
+ const internalNumberLabelBoxes =
584
+ SchematicOwnerPinLabelLayout.collectInternalNumberLabelBoxes(
585
+ pins,
586
+ contentRectangles
587
+ )
454
588
  const pinMarkup = pins
455
589
  .map((pin, index) =>
456
590
  SchematicSvgRenderer.#appendSvgAttributes(
@@ -460,7 +594,9 @@ export class SchematicSvgRenderer {
460
594
  renderedSheet.contentSheet,
461
595
  rotatedVerticalNumberOwners,
462
596
  explicitOwnerPinNameLabels,
463
- explicitOwnerPinLabelOffsets
597
+ explicitOwnerPinLabelOffsets,
598
+ compactExternalNumberLabelSides,
599
+ internalNumberLabelBoxes
464
600
  ),
465
601
  SchematicSvgRenderer.#semanticAttributes(
466
602
  'pin',
@@ -504,10 +640,10 @@ export class SchematicSvgRenderer {
504
640
  )
505
641
  .join('')
506
642
  const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
507
- lines,
643
+ contentLines,
508
644
  crosses,
509
645
  ports,
510
- resolvedTexts.filter((text) => text.recordType === '17'),
646
+ contentTexts.filter((text) => text.recordType === '17'),
511
647
  contentHeight,
512
648
  resolvedAuthoredJunctions
513
649
  )
@@ -626,6 +762,7 @@ export class SchematicSvgRenderer {
626
762
  '</g>' +
627
763
  '</g>' +
628
764
  frameMarkup +
765
+ nativeFooterMarkup +
629
766
  '<g class="schematic-regions">' +
630
767
  regionMarkup +
631
768
  '</g>' +
@@ -636,7 +773,7 @@ export class SchematicSvgRenderer {
636
773
  /**
637
774
  * Normalizes schematic SVG export options.
638
775
  * @param {Record<string, unknown>} options Raw render options.
639
- * @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
776
+ * @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, colorizeImages: boolean, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
640
777
  */
641
778
  static #normalizeRenderOptions(options) {
642
779
  const includeViewBox =
@@ -648,6 +785,9 @@ export class SchematicSvgRenderer {
648
785
  documentVersion: String(
649
786
  options?.documentVersion || options?.documentVer || ''
650
787
  ),
788
+ colorizeImages:
789
+ options?.colorizeImages === true ||
790
+ options?.colorize_images === true,
651
791
  includeTextGeometrySidecar:
652
792
  options?.includeTextGeometrySidecar === true ||
653
793
  options?.textGeometry === 'sidecar',
@@ -660,6 +800,97 @@ export class SchematicSvgRenderer {
660
800
  }
661
801
  }
662
802
 
803
+ /**
804
+ * Removes unresolved simple Altium special-string placeholders from
805
+ * drawable text while preserving values resolved by project parameters.
806
+ * @param {object[]} texts Schematic text rows.
807
+ * @returns {object[]}
808
+ */
809
+ static #filterDrawableSchematicTexts(texts) {
810
+ return (texts || []).filter(
811
+ (text) =>
812
+ !SchematicSvgRenderer.#isUnresolvedSimpleSpecialStringText(text)
813
+ )
814
+ }
815
+
816
+ /**
817
+ * Uses sheet-frame chrome color for native footer linework so recovered
818
+ * template cells match the synthesized frame and zone separators.
819
+ * @param {object[]} lines Schematic line primitives.
820
+ * @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
821
+ * @param {number} sheetHeight Rendered sheet height for projection.
822
+ * @returns {object[]}
823
+ */
824
+ static #themeFooterChromeLines(lines, sheet, sheetHeight) {
825
+ return (lines || []).map((line) =>
826
+ SchematicSvgRenderer.#isFooterChromeLine(line, sheet, sheetHeight)
827
+ ? {
828
+ ...line,
829
+ color: 'var(--schematic-sheet-frame-stroke)'
830
+ }
831
+ : line
832
+ )
833
+ }
834
+
835
+ /**
836
+ * Returns true for neutral owner-drawn title-block lines in the bottom
837
+ * right footer band.
838
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }} line Schematic line candidate.
839
+ * @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
840
+ * @param {number} sheetHeight Rendered sheet height for projection.
841
+ * @returns {boolean}
842
+ */
843
+ static #isFooterChromeLine(line, sheet, sheetHeight) {
844
+ if (!sheet?.borderOn || !String(line?.ownerIndex || '').trim()) {
845
+ return false
846
+ }
847
+ if (
848
+ SchematicSvgRenderer.#resolveSchematicLineColor(line) !==
849
+ 'var(--schematic-text-color)'
850
+ ) {
851
+ return false
852
+ }
853
+
854
+ const width = Number(sheet?.width || 0)
855
+ const height = Number(sheetHeight || 0)
856
+ const x1 = Number(line?.x1)
857
+ const x2 = Number(line?.x2)
858
+ const y1 = Number(line?.y1)
859
+ const y2 = Number(line?.y2)
860
+
861
+ if (
862
+ !Number.isFinite(width) ||
863
+ !Number.isFinite(height) ||
864
+ !Number.isFinite(x1) ||
865
+ !Number.isFinite(x2) ||
866
+ !Number.isFinite(y1) ||
867
+ !Number.isFinite(y2) ||
868
+ width <= 0 ||
869
+ height <= 0
870
+ ) {
871
+ return false
872
+ }
873
+
874
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
875
+ const footerBandHeight = Math.max(margin * 6, 120)
876
+ const footerTop = Math.max(height - footerBandHeight, margin)
877
+ const projectedY1 = projectSchematicY(height, y1)
878
+ const projectedY2 = projectSchematicY(height, y2)
879
+ const minProjectedY = Math.min(projectedY1, projectedY2)
880
+ const maxX = Math.max(x1, x2)
881
+
882
+ return minProjectedY >= footerTop && maxX >= width * 0.5
883
+ }
884
+
885
+ /**
886
+ * Returns true when a text row still contains one bare special string.
887
+ * @param {object} text Schematic text row.
888
+ * @returns {boolean}
889
+ */
890
+ static #isUnresolvedSimpleSpecialStringText(text) {
891
+ return /^[.=][A-Za-z_][\w.-]*$/u.test(String(text?.text || '').trim())
892
+ }
893
+
663
894
  /**
664
895
  * Renders root SVG viewBox attributes according to export options.
665
896
  * @param {{ includeViewBox: boolean }} options Normalized options.
@@ -1900,12 +2131,7 @@ export class SchematicSvgRenderer {
1900
2131
  '" y2="' +
1901
2132
  formatNumber(projectSchematicY(sheetHeight, line.y2)) +
1902
2133
  '" stroke="' +
1903
- escapeHtml(
1904
- SchematicColorResolver.resolveColor(
1905
- line.color,
1906
- '--schematic-default-ink-color'
1907
- )
1908
- ) +
2134
+ escapeHtml(SchematicSvgRenderer.#resolveSchematicLineColor(line)) +
1909
2135
  '" stroke-width="' +
1910
2136
  formatNumber(
1911
2137
  SchematicSvgRenderer.#resolveSchematicLineWidth(line)
@@ -1936,6 +2162,26 @@ export class SchematicSvgRenderer {
1936
2162
  return Math.max(baseWidth * 3, 3)
1937
2163
  }
1938
2164
 
2165
+ /**
2166
+ * Resolves line stroke color, treating black electrical wires as themed
2167
+ * schematic connectivity while leaving graphic linework source-colored.
2168
+ * @param {{ color: string, ownerIndex?: string, isBus?: boolean, recordType?: string }} line
2169
+ * @returns {string}
2170
+ */
2171
+ static #resolveSchematicLineColor(line) {
2172
+ if (SchematicSvgRenderer.#isElectricalSchematicLine(line)) {
2173
+ return SchematicColorResolver.resolveNonTextColor(
2174
+ line.color,
2175
+ '--schematic-default-ink-color'
2176
+ )
2177
+ }
2178
+
2179
+ return SchematicColorResolver.resolveColor(
2180
+ line.color,
2181
+ '--schematic-default-ink-color'
2182
+ )
2183
+ }
2184
+
1939
2185
  /**
1940
2186
  * Returns SVG stroke attributes for one schematic line style.
1941
2187
  * @param {{ width: number, lineStyle?: number }} line
@@ -2287,7 +2533,7 @@ export class SchematicSvgRenderer {
2287
2533
  formatNumber(projectSchematicY(sheetHeight, junction.y)) +
2288
2534
  '" r="2.4" fill="' +
2289
2535
  escapeHtml(
2290
- SchematicColorResolver.resolveColor(
2536
+ SchematicColorResolver.resolveNonTextColor(
2291
2537
  junction.color,
2292
2538
  '--schematic-default-ink-color'
2293
2539
  )