altium-toolkit 1.0.2 → 1.0.8

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.
@@ -21,6 +21,10 @@ import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
21
21
 
22
22
  const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
23
23
  SchematicSvgUtils
24
+ const SECTION_HEADING_MIN_FONT_SIZE = 18
25
+ const SECTION_HEADING_BASELINE_LIFT_RATIO = 0.36
26
+ const SECTION_HEADING_LINE_Y_TOLERANCE = 0.75
27
+ const SECTION_HEADING_LINE_X_PADDING = 15
24
28
 
25
29
  /**
26
30
  * Renders normalized schematic models into presentational SVG.
@@ -28,7 +32,7 @@ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
28
32
  export class SchematicSvgRenderer {
29
33
  /**
30
34
  * Renders a normalized schematic model into SVG markup.
31
- * @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, 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
35
+ * @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
32
36
  * @returns {string}
33
37
  */
34
38
  static render(documentModel) {
@@ -37,8 +41,16 @@ export class SchematicSvgRenderer {
37
41
  return '<section class="altium-renderer-empty">No schematic entities were recovered from this file.</section>'
38
42
  }
39
43
 
40
- const width = Math.max(schematic.sheet.width || 1000, 100)
41
- const height = Math.max(schematic.sheet.height || 700, 100)
44
+ const renderedSheet = SchematicSvgRenderer.#resolveRenderedSheet(
45
+ schematic.sheet
46
+ )
47
+ const width = renderedSheet.width
48
+ const height = renderedSheet.height
49
+ const contentHeight = renderedSheet.contentHeight
50
+ const renderedSchematic =
51
+ renderedSheet.contentSheet === schematic.sheet
52
+ ? schematic
53
+ : { ...schematic, sheet: renderedSheet.contentSheet }
42
54
  const allTexts = schematic.texts || []
43
55
  const lines = schematic.lines.slice(0, 2500)
44
56
  const polygons = (schematic.polygons || []).slice(0, 1000)
@@ -68,27 +80,27 @@ export class SchematicSvgRenderer {
68
80
  const frameMarkup = SchematicSvgRenderer.#buildSheetChromeMarkup(
69
81
  width,
70
82
  height,
71
- schematic.sheet,
83
+ renderedSheet.sheet,
72
84
  documentModel?.fileName
73
85
  )
74
86
  const regionMarkup = SchematicRegionRenderer.buildMarkup(
75
87
  regions,
76
- height
88
+ contentHeight
77
89
  )
78
90
  const contentTransform = SchematicContentLayout.buildTransform(
79
91
  width,
80
- height,
81
- schematic
92
+ contentHeight,
93
+ renderedSchematic
82
94
  )
83
95
  const contentClipId = SchematicContentLayout.buildClipId(
84
96
  width,
85
97
  height,
86
- schematic
98
+ renderedSchematic
87
99
  )
88
100
  const contentClipMarkup = SchematicContentLayout.buildClipMarkup(
89
101
  width,
90
102
  height,
91
- schematic,
103
+ renderedSchematic,
92
104
  contentClipId
93
105
  )
94
106
  const ownerlessLines = lines.filter((line) => !line.ownerIndex)
@@ -117,26 +129,40 @@ export class SchematicSvgRenderer {
117
129
  )
118
130
  const polygonMarkup = ownerlessPolygons
119
131
  .map((polygon) =>
120
- SchematicShapeRenderer.buildPolygonMarkup(polygon, height)
132
+ SchematicShapeRenderer.buildPolygonMarkup(
133
+ polygon,
134
+ contentHeight
135
+ )
121
136
  )
122
137
  .join('')
123
138
  const rectangleMarkup = ownerlessRectangles
124
139
  .map((rectangle) =>
125
- SchematicShapeRenderer.buildRectangleMarkup(rectangle, height)
140
+ SchematicShapeRenderer.buildRectangleMarkup(
141
+ rectangle,
142
+ contentHeight
143
+ )
126
144
  )
127
145
  .join('')
128
146
  const ellipseMarkup = ownerlessEllipses
129
147
  .map((ellipse) =>
130
- SchematicShapeRenderer.buildEllipseMarkup(ellipse, height)
148
+ SchematicShapeRenderer.buildEllipseMarkup(
149
+ ellipse,
150
+ contentHeight
151
+ )
131
152
  )
132
153
  .join('')
133
154
  const lineMarkup = ownerlessLines
134
155
  .map((line) =>
135
- SchematicSvgRenderer.#buildSchematicLineMarkup(line, height)
156
+ SchematicSvgRenderer.#buildSchematicLineMarkup(
157
+ line,
158
+ contentHeight
159
+ )
136
160
  )
137
161
  .join('')
138
162
  const arcMarkup = ownerlessArcs
139
- .map((arc) => SchematicShapeRenderer.buildArcMarkup(arc, height))
163
+ .map((arc) =>
164
+ SchematicShapeRenderer.buildArcMarkup(arc, contentHeight)
165
+ )
140
166
  .join('')
141
167
  const ownerGeometryMarkup =
142
168
  SchematicSvgRenderer.#buildOwnerGeometryMarkup(
@@ -145,41 +171,49 @@ export class SchematicSvgRenderer {
145
171
  rectangles,
146
172
  ellipses,
147
173
  arcs,
148
- height
174
+ contentHeight
149
175
  )
150
176
  const sheetSymbolMarkup =
151
177
  SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
152
178
  sheetSymbols,
153
- height
179
+ contentHeight
154
180
  )
155
181
  const sheetEntryMarkup =
156
182
  SchematicSheetSymbolRenderer.buildSheetEntryMarkup(
157
183
  sheetEntries,
158
- height
184
+ contentHeight
159
185
  )
160
186
  const busEntryMarkup = busEntries
161
187
  .map((busEntry) =>
162
188
  SchematicSvgRenderer.#buildSchematicBusEntryMarkup(
163
189
  busEntry,
164
- height
190
+ contentHeight
165
191
  )
166
192
  )
167
193
  .join('')
168
- const authoredJunctionMarkup = authoredJunctions
194
+ const resolvedAuthoredJunctions =
195
+ SchematicSvgRenderer.#resolveAuthoredSchematicJunctions(
196
+ authoredJunctions,
197
+ lines
198
+ )
199
+ const authoredJunctionMarkup = resolvedAuthoredJunctions
169
200
  .map((junction) =>
170
201
  SchematicSvgRenderer.#buildAuthoredSchematicJunctionMarkup(
171
202
  junction,
172
- height
203
+ contentHeight
173
204
  )
174
205
  )
175
206
  .join('')
176
- const imageMarkup = SchematicImageRenderer.buildMarkup(images, height)
207
+ const imageMarkup = SchematicImageRenderer.buildMarkup(
208
+ images,
209
+ contentHeight
210
+ )
177
211
 
178
212
  const textMarkup = resolvedTexts
179
213
  .map((text) =>
180
214
  SchematicSvgRenderer.#buildSchematicTextMarkup(
181
215
  text,
182
- height,
216
+ contentHeight,
183
217
  lines,
184
218
  pins
185
219
  )
@@ -190,8 +224,8 @@ export class SchematicSvgRenderer {
190
224
  .map((component) =>
191
225
  SchematicSvgRenderer.#buildFallbackComponentMarkup(
192
226
  component,
193
- height,
194
- schematic.sheet
227
+ contentHeight,
228
+ renderedSheet.contentSheet
195
229
  )
196
230
  )
197
231
  .join('')
@@ -209,8 +243,8 @@ export class SchematicSvgRenderer {
209
243
  .map((pin) =>
210
244
  SchematicPinSvgRenderer.buildMarkup(
211
245
  pin,
212
- height,
213
- schematic.sheet,
246
+ contentHeight,
247
+ renderedSheet.contentSheet,
214
248
  rotatedVerticalNumberOwners,
215
249
  explicitOwnerPinNameLabels,
216
250
  explicitOwnerPinLabelOffsets
@@ -219,24 +253,28 @@ export class SchematicSvgRenderer {
219
253
  .join('')
220
254
  const portMarkup = SchematicPortRenderer.buildMarkup(
221
255
  ports,
222
- height,
223
- schematic.sheet
256
+ contentHeight,
257
+ renderedSheet.contentSheet
224
258
  )
225
259
  const directiveMarkup = SchematicDirectiveRenderer.buildMarkup(
226
260
  directives,
227
- height,
228
- schematic.sheet
261
+ contentHeight,
262
+ renderedSheet.contentSheet
229
263
  )
230
264
  const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
231
265
  lines,
232
266
  crosses,
233
267
  ports,
234
268
  resolvedTexts.filter((text) => text.recordType === '17'),
235
- height
269
+ contentHeight,
270
+ resolvedAuthoredJunctions
236
271
  )
237
272
  const crossMarkup = crosses
238
273
  .map((cross) =>
239
- SchematicSvgRenderer.#buildSchematicCrossMarkup(cross, height)
274
+ SchematicSvgRenderer.#buildSchematicCrossMarkup(
275
+ cross,
276
+ contentHeight
277
+ )
240
278
  )
241
279
  .join('')
242
280
 
@@ -327,6 +365,80 @@ export class SchematicSvgRenderer {
327
365
  )
328
366
  }
329
367
 
368
+ /**
369
+ * Resolves the rendered dimensions and sheet metadata used by SVG output.
370
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean } | undefined} sheet
371
+ * @returns {{ width: number, height: number, contentHeight: number, sheet: object, contentSheet: object }}
372
+ */
373
+ static #resolveRenderedSheet(sheet) {
374
+ const width = Math.max(Number(sheet?.width || 1000), 100)
375
+ const height = Math.max(Number(sheet?.height || 700), 100)
376
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
377
+ const renderedHeight = SchematicSvgRenderer.#resolveRenderedSheetHeight(
378
+ sheet,
379
+ width,
380
+ height,
381
+ margin
382
+ )
383
+
384
+ if (renderedHeight === height) {
385
+ return {
386
+ width,
387
+ height,
388
+ contentHeight: height,
389
+ sheet: sheet || {},
390
+ contentSheet: sheet || {}
391
+ }
392
+ }
393
+ const contentHeight = renderedHeight - margin
394
+
395
+ return {
396
+ width,
397
+ height: renderedHeight,
398
+ contentHeight,
399
+ sheet: {
400
+ ...(sheet || {}),
401
+ width,
402
+ height: renderedHeight,
403
+ sourceWidth: width,
404
+ sourceHeight: renderedHeight
405
+ },
406
+ contentSheet: {
407
+ ...(sheet || {}),
408
+ width,
409
+ height: contentHeight,
410
+ sourceWidth: width,
411
+ sourceHeight: contentHeight
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Adds top and bottom zone bands for preserved custom border sheets whose
418
+ * stored Y extent describes the inner drawing frame.
419
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean } | undefined} sheet
420
+ * @param {number} width
421
+ * @param {number} height
422
+ * @param {number} margin
423
+ * @returns {number}
424
+ */
425
+ static #resolveRenderedSheetHeight(sheet, width, height, margin) {
426
+ const sourceWidth = Number(sheet?.sourceWidth || 0)
427
+ const sourceHeight = Number(sheet?.sourceHeight || 0)
428
+
429
+ if (
430
+ !sheet?.borderOn ||
431
+ sheet?.paperSize ||
432
+ width !== sourceWidth ||
433
+ height !== sourceHeight ||
434
+ height <= margin * 2
435
+ ) {
436
+ return height
437
+ }
438
+
439
+ return height + margin * 2
440
+ }
441
+
330
442
  /**
331
443
  * Builds interleaved owner geometry so symbol-internal primitives preserve
332
444
  * their recovered Altium paint order instead of batching fills ahead of all
@@ -550,7 +662,7 @@ export class SchematicSvgRenderer {
550
662
 
551
663
  /**
552
664
  * Builds one free text primitive with font metadata.
553
- * @param {{ x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
665
+ * @param {{ 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', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
554
666
  * @param {number} sheetHeight
555
667
  * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
556
668
  * @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
@@ -578,6 +690,7 @@ export class SchematicSvgRenderer {
578
690
  const placement = SchematicSvgRenderer.#resolveSchematicTextPlacement(
579
691
  text,
580
692
  sheetHeight,
693
+ lines,
581
694
  matchedOwnerPin
582
695
  )
583
696
 
@@ -601,28 +714,139 @@ export class SchematicSvgRenderer {
601
714
 
602
715
  /**
603
716
  * Resolves final text placement for schematic free-text annotations.
604
- * @param {{ x: number, y: number, text: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end' }} text
717
+ * @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end' }} text
605
718
  * @param {number} sheetHeight
719
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
606
720
  * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
607
721
  * @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
608
722
  */
609
- static #resolveSchematicTextPlacement(text, sheetHeight, matchedOwnerPin) {
723
+ static #resolveSchematicTextPlacement(
724
+ text,
725
+ sheetHeight,
726
+ lines,
727
+ matchedOwnerPin
728
+ ) {
610
729
  const mirroredOwnerPinPlacement =
611
730
  SchematicOwnerPinLabelLayout.resolveMirroredOwnerPinLabelPlacement(
612
731
  text,
613
732
  matchedOwnerPin
614
733
  )
734
+ const sourceY = mirroredOwnerPinPlacement?.y ?? text.y
735
+ const projectedY = projectSchematicY(sheetHeight, sourceY)
736
+ const fontSize =
737
+ SchematicTypography.resolveViewerFontSize(text.fontSize) || 0
738
+ const baselineLift =
739
+ SchematicSvgRenderer.#resolveSectionHeadingBaselineLift(
740
+ text,
741
+ lines,
742
+ sourceY,
743
+ fontSize,
744
+ matchedOwnerPin
745
+ )
615
746
 
616
747
  return {
617
748
  x: mirroredOwnerPinPlacement?.x ?? text.x,
618
- y: projectSchematicY(
619
- sheetHeight,
620
- mirroredOwnerPinPlacement?.y ?? text.y
621
- ),
749
+ y: projectedY - baselineLift,
622
750
  anchor: text.anchor || 'start'
623
751
  }
624
752
  }
625
753
 
754
+ /**
755
+ * Lifts large section headings clear of authored dash-dot frame baselines.
756
+ * @param {{ x: number, y: number, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number }} text
757
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
758
+ * @param {number} sourceY
759
+ * @param {number} fontSize
760
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
761
+ * @returns {number}
762
+ */
763
+ static #resolveSectionHeadingBaselineLift(
764
+ text,
765
+ lines,
766
+ sourceY,
767
+ fontSize,
768
+ matchedOwnerPin
769
+ ) {
770
+ if (
771
+ !SchematicSvgRenderer.#isLargeOwnerlessFreeText(
772
+ text,
773
+ fontSize,
774
+ matchedOwnerPin
775
+ )
776
+ ) {
777
+ return 0
778
+ }
779
+
780
+ return SchematicSvgRenderer.#hasSameCoordinateDashDotFrameLine(
781
+ text,
782
+ lines,
783
+ sourceY
784
+ )
785
+ ? fontSize * SECTION_HEADING_BASELINE_LIFT_RATIO
786
+ : 0
787
+ }
788
+
789
+ /**
790
+ * Returns true when one text primitive behaves like a section heading.
791
+ * @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text
792
+ * @param {number} fontSize
793
+ * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
794
+ * @returns {boolean}
795
+ */
796
+ static #isLargeOwnerlessFreeText(text, fontSize, matchedOwnerPin) {
797
+ if (matchedOwnerPin) return false
798
+ if (text.recordType !== '4') return false
799
+ if (String(text.ownerIndex || '').trim()) return false
800
+ if (fontSize < SECTION_HEADING_MIN_FONT_SIZE) return false
801
+
802
+ return SchematicSvgRenderer.#normalizeDegrees(text.rotation) === 0
803
+ }
804
+
805
+ /**
806
+ * Detects section frame lines that share the title baseline coordinate.
807
+ * @param {{ x: number }} text
808
+ * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
809
+ * @param {number} sourceY
810
+ * @returns {boolean}
811
+ */
812
+ static #hasSameCoordinateDashDotFrameLine(text, lines, sourceY) {
813
+ const textX = Number(text.x)
814
+ if (!Number.isFinite(textX) || !Number.isFinite(sourceY)) return false
815
+
816
+ return lines.some((line) => {
817
+ if (Number(line.lineStyle || 0) !== 3) return false
818
+
819
+ const y1 = Number(line.y1)
820
+ const y2 = Number(line.y2)
821
+ if (
822
+ !Number.isFinite(y1) ||
823
+ !Number.isFinite(y2) ||
824
+ Math.abs(y1 - y2) > SECTION_HEADING_LINE_Y_TOLERANCE ||
825
+ Math.abs(y1 - sourceY) > SECTION_HEADING_LINE_Y_TOLERANCE
826
+ ) {
827
+ return false
828
+ }
829
+
830
+ const minX =
831
+ Math.min(Number(line.x1), Number(line.x2)) -
832
+ SECTION_HEADING_LINE_X_PADDING
833
+ const maxX =
834
+ Math.max(Number(line.x1), Number(line.x2)) +
835
+ SECTION_HEADING_LINE_X_PADDING
836
+
837
+ return textX >= minX && textX <= maxX
838
+ })
839
+ }
840
+
841
+ /**
842
+ * Normalizes text rotation into a whole-degree clockwise range.
843
+ * @param {number | undefined} rotation
844
+ * @returns {number}
845
+ */
846
+ static #normalizeDegrees(rotation) {
847
+ return ((Math.round(Number(rotation || 0)) % 360) + 360) % 360
848
+ }
849
+
626
850
  /**
627
851
  * Builds one schematic cross marker.
628
852
  * @param {{ x: number, y: number, size: number, color: string }} cross
@@ -692,6 +916,84 @@ export class SchematicSvgRenderer {
692
916
  )
693
917
  }
694
918
 
919
+ /**
920
+ * Resolves authored junction colors from their connected wire routes.
921
+ * @param {{ x: number, y: number, color: string }[]} junctions
922
+ * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, ownerIndex?: string, isBus?: boolean, recordType?: string }[]} lines
923
+ * @returns {{ x: number, y: number, color: string }[]}
924
+ */
925
+ static #resolveAuthoredSchematicJunctions(junctions, lines) {
926
+ return junctions.map((junction) => ({
927
+ ...junction,
928
+ color: SchematicSvgRenderer.#resolveAuthoredSchematicJunctionColor(
929
+ junction,
930
+ lines
931
+ )
932
+ }))
933
+ }
934
+
935
+ /**
936
+ * Returns the connected wire color for one authored junction.
937
+ * @param {{ x: number, y: number, color: string }} junction
938
+ * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, ownerIndex?: string, isBus?: boolean, recordType?: string }[]} lines
939
+ * @returns {string}
940
+ */
941
+ static #resolveAuthoredSchematicJunctionColor(junction, lines) {
942
+ const connectedLine = lines.find(
943
+ (line) =>
944
+ SchematicSvgRenderer.#isElectricalSchematicLine(line) &&
945
+ SchematicSvgRenderer.#schematicLineContainsPoint(line, junction)
946
+ )
947
+
948
+ return connectedLine?.color || junction.color
949
+ }
950
+
951
+ /**
952
+ * Returns true when one normalized line can carry schematic net color.
953
+ * @param {{ ownerIndex?: string, isBus?: boolean, recordType?: string } | null | undefined} line
954
+ * @returns {boolean}
955
+ */
956
+ static #isElectricalSchematicLine(line) {
957
+ if (line?.ownerIndex || line?.isBus === true) {
958
+ return false
959
+ }
960
+
961
+ if (!Object.prototype.hasOwnProperty.call(line || {}, 'recordType')) {
962
+ return true
963
+ }
964
+
965
+ return !['6', '7', '26'].includes(String(line.recordType || ''))
966
+ }
967
+
968
+ /**
969
+ * Returns true when one schematic line segment contains the given point.
970
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} line
971
+ * @param {{ x: number, y: number }} point
972
+ * @returns {boolean}
973
+ */
974
+ static #schematicLineContainsPoint(line, point) {
975
+ const dx = Number(line.x2) - Number(line.x1)
976
+ const dy = Number(line.y2) - Number(line.y1)
977
+ const pointDx = Number(point.x) - Number(line.x1)
978
+ const pointDy = Number(point.y) - Number(line.y1)
979
+ const lengthSquared = dx * dx + dy * dy
980
+
981
+ if (!lengthSquared) {
982
+ return (
983
+ Math.abs(Number(line.x1) - Number(point.x)) <= 0.01 &&
984
+ Math.abs(Number(line.y1) - Number(point.y)) <= 0.01
985
+ )
986
+ }
987
+
988
+ const cross = Math.abs(pointDx * dy - pointDy * dx)
989
+ if (cross > 0.01) {
990
+ return false
991
+ }
992
+
993
+ const dot = pointDx * dx + pointDy * dy
994
+ return dot >= -0.01 && dot <= lengthSquared + 0.01
995
+ }
996
+
695
997
  /**
696
998
  * Builds one schematic bus-entry line marker.
697
999
  * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number }} busEntry
@@ -46,7 +46,7 @@ export class SchematicSvgUtils {
46
46
  * @param {string} text
47
47
  * @param {string} color
48
48
  * @param {'start' | 'end' | 'middle'} anchor
49
- * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, segments?: { text: string, overline?: boolean }[] }} [options]
49
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, segments?: { text: string, overline?: boolean }[] }} [options]
50
50
  * @returns {string}
51
51
  */
52
52
  static createSvgText(className, x, y, text, color, anchor, options = {}) {
@@ -102,7 +102,7 @@ export class SchematicSvgUtils {
102
102
  * Creates optional inline SVG text attributes for typography and rotation.
103
103
  * @param {number} x
104
104
  * @param {number} y
105
- * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }} options
105
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number }} options
106
106
  * @returns {string}
107
107
  */
108
108
  static #buildSvgTextStyleAttributes(x, y, options) {
@@ -131,6 +131,13 @@ export class SchematicSvgUtils {
131
131
  '"'
132
132
  }
133
133
 
134
+ if (options.fontStyle && options.fontStyle !== 'normal') {
135
+ attributes +=
136
+ ' font-style="' +
137
+ SchematicSvgUtils.escapeHtml(options.fontStyle) +
138
+ '"'
139
+ }
140
+
134
141
  if (options.rotation) {
135
142
  attributes +=
136
143
  ' transform="rotate(' +
@@ -26,28 +26,30 @@ export class SchematicTypography {
26
26
 
27
27
  /**
28
28
  * Builds the default font options used for synthetic schematic labels.
29
- * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
30
- * @returns {{ fontSize: number, fontFamily: string, fontWeight: number }}
29
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean, italic?: boolean }> }} sheet
30
+ * @returns {{ fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string }}
31
31
  */
32
32
  static buildDefaultSchematicFontOptions(sheet) {
33
33
  const font = sheet?.fonts?.['1'] || {
34
34
  size: 10,
35
35
  family: 'Times New Roman',
36
- bold: false
36
+ bold: false,
37
+ italic: false
37
38
  }
38
39
 
39
40
  return {
40
41
  fontSize: SchematicTypography.#toSvgFontSize(font.size),
41
42
  fontFamily: font.family || 'Times New Roman',
42
- fontWeight: font.bold ? 700 : 400
43
+ fontWeight: font.bold ? 700 : 400,
44
+ fontStyle: font.italic ? 'italic' : undefined
43
45
  }
44
46
  }
45
47
 
46
48
  /**
47
49
  * Builds default font options with the viewer-wide one-point reduction
48
50
  * already applied.
49
- * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
50
- * @returns {{ fontSize: number | undefined, fontFamily: string, fontWeight: number }}
51
+ * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean, italic?: boolean }> }} sheet
52
+ * @returns {{ fontSize: number | undefined, fontFamily: string, fontWeight: number, fontStyle?: string }}
51
53
  */
52
54
  static buildViewerSchematicFontOptions(sheet) {
53
55
  return SchematicTypography.withViewerFontSize(
@@ -59,14 +61,15 @@ export class SchematicTypography {
59
61
  * Builds render options for one schematic text label, including the signed
60
62
  * SVG rotation derived from the original Altium orientation and mirrored
61
63
  * source state.
62
- * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text
63
- * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }}
64
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text
65
+ * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number }}
64
66
  */
65
67
  static buildSchematicTextRenderOptions(text) {
66
68
  return {
67
69
  fontSize: SchematicTypography.resolveViewerFontSize(text.fontSize),
68
70
  fontFamily: text.fontFamily,
69
71
  fontWeight: text.fontWeight,
72
+ fontStyle: text.fontStyle,
70
73
  rotation: SchematicTypography.#resolveSignedTextRotation(
71
74
  text.rotation,
72
75
  text.sourceOrientation,
@@ -77,8 +80,8 @@ export class SchematicTypography {
77
80
 
78
81
  /**
79
82
  * Applies the viewer-wide one-point text reduction to one option bag.
80
- * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }} options
81
- * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, rotation?: number }}
83
+ * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number }} options
84
+ * @returns {{ fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number }}
82
85
  */
83
86
  static withViewerFontSize(options) {
84
87
  return {