altium-toolkit 1.1.38 → 1.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.38",
3
+ "version": "1.1.39",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -12,7 +12,7 @@ export class SchematicDirectiveParser {
12
12
  /**
13
13
  * Normalizes schematic directive records into drawable directive metadata.
14
14
  * @param {{ fields: Record<string, string | string[]> }[]} records
15
- * @returns {{ x: number, y: number, color: string, name: string, orientation: number }[]}
15
+ * @returns {{ x: number, y: number, color: string, name: string, orientation: number, style?: number }[]}
16
16
  */
17
17
  static parseSchematicDirectives(records) {
18
18
  return records
@@ -31,7 +31,11 @@ export class SchematicDirectiveParser {
31
31
  return null
32
32
  }
33
33
 
34
- return {
34
+ const style = ParserUtils.parseNumericField(
35
+ record.fields,
36
+ 'Style'
37
+ )
38
+ const directive = {
35
39
  x,
36
40
  y,
37
41
  color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
@@ -42,6 +46,12 @@ export class SchematicDirectiveParser {
42
46
  'Orientation'
43
47
  ) || 0
44
48
  }
49
+
50
+ if (style !== null) {
51
+ directive.style = style
52
+ }
53
+
54
+ return directive
45
55
  })
46
56
  .filter(Boolean)
47
57
  }
@@ -210,6 +220,8 @@ export class SchematicDirectiveParser {
210
220
  orientation:
211
221
  ParserUtils.parseNumericField(record.fields, 'Orientation') ||
212
222
  0,
223
+ style:
224
+ ParserUtils.parseNumericField(record.fields, 'Style') ?? null,
213
225
  parameters,
214
226
  parameterMap,
215
227
  isDifferentialPair,
@@ -904,6 +904,15 @@ export class SchematicPinParser {
904
904
  )
905
905
  ) {
906
906
  labelMode = 'number-only'
907
+ } else if (
908
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
909
+ SchematicPinParser.#isCompactCommonTerminalDiodeGroup(
910
+ normalizedPins,
911
+ names,
912
+ orientationCount
913
+ )
914
+ ) {
915
+ labelMode = 'number-only'
907
916
  } else if (!semanticNames.length && orientationCount <= 2) {
908
917
  labelMode = 'number-only'
909
918
  } else if (
@@ -1059,6 +1068,48 @@ export class SchematicPinParser {
1059
1068
  )
1060
1069
  }
1061
1070
 
1071
+ /**
1072
+ * Returns true when a compact owner-drawn diode-like section exposes one
1073
+ * ordinary anode/cathode terminal and one common terminal name internally.
1074
+ * @param {{ designator: string, name: string, length: number, electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
1075
+ * @param {string[]} names
1076
+ * @param {number} orientationCount
1077
+ * @returns {boolean}
1078
+ */
1079
+ static #isCompactCommonTerminalDiodeGroup(pins, names, orientationCount) {
1080
+ if (
1081
+ pins.length !== 2 ||
1082
+ orientationCount !== 2 ||
1083
+ names.length !== pins.length ||
1084
+ !SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
1085
+ ) {
1086
+ return false
1087
+ }
1088
+
1089
+ if (
1090
+ pins.some((pin) => {
1091
+ const length = Math.abs(Number(pin.length || 0))
1092
+ const electrical = Number(pin.electrical)
1093
+ return (
1094
+ length <= 0 ||
1095
+ length > 15 ||
1096
+ (Number.isFinite(electrical) && electrical !== 4)
1097
+ )
1098
+ })
1099
+ ) {
1100
+ return false
1101
+ }
1102
+
1103
+ const hasDiodeTerminal = names.some((name) =>
1104
+ SchematicPinParser.#isDiodeTerminalName(name)
1105
+ )
1106
+ const hasCommonTerminal = names.some((name) =>
1107
+ SchematicPinParser.#isCommonDiodeTerminalName(name)
1108
+ )
1109
+
1110
+ return hasDiodeTerminal && hasCommonTerminal
1111
+ }
1112
+
1062
1113
  /**
1063
1114
  * Returns true when compact owner-drawn terminal glyph pins have either no
1064
1115
  * external designators or ordinary numeric pin numbers.
@@ -1104,6 +1155,24 @@ export class SchematicPinParser {
1104
1155
  )
1105
1156
  }
1106
1157
 
1158
+ /**
1159
+ * Returns true for one-letter terminals used inside diode-style symbols.
1160
+ * @param {string} name
1161
+ * @returns {boolean}
1162
+ */
1163
+ static #isDiodeTerminalName(name) {
1164
+ return /^[AKC]$/i.test(String(name || '').trim())
1165
+ }
1166
+
1167
+ /**
1168
+ * Returns true for common-terminal names used by multipart diode symbols.
1169
+ * @param {string} name
1170
+ * @returns {boolean}
1171
+ */
1172
+ static #isCommonDiodeTerminalName(name) {
1173
+ return /^COM[AC]$/i.test(String(name || '').trim())
1174
+ }
1175
+
1107
1176
  /**
1108
1177
  * Returns true when one passive two-pin symbol uses the ordinary 1/2 pin
1109
1178
  * numbering that should stay hidden for simple resistor-like parts.
@@ -29,12 +29,19 @@ export class SchematicTextOrientationResolver {
29
29
  }
30
30
 
31
31
  /**
32
- * Resolves component text orientation bit 1 to top source anchoring.
32
+ * Resolves text vertical anchoring from Altium's justification grid and
33
+ * component text orientation bits.
33
34
  * @param {Record<string, string | string[]>} fields Text record fields.
34
35
  * @param {string} recordType Native text record type.
35
- * @returns {'top' | null}
36
+ * @returns {'middle' | 'top' | null}
36
37
  */
37
38
  static resolveVerticalAnchor(fields, recordType) {
39
+ const justificationAnchor =
40
+ SchematicTextOrientationResolver.#resolveJustificationVerticalAnchor(
41
+ fields
42
+ )
43
+ if (justificationAnchor) return justificationAnchor
44
+
38
45
  return SchematicTextOrientationResolver.resolveHorizontalAnchor(
39
46
  fields,
40
47
  recordType
@@ -65,6 +72,28 @@ export class SchematicTextOrientationResolver {
65
72
  return orientation === 1
66
73
  }
67
74
 
75
+ /**
76
+ * Resolves the vertical row from Altium's 3x3 justification grid.
77
+ * @param {Record<string, string | string[]>} fields Text record fields.
78
+ * @returns {'middle' | 'top' | null}
79
+ */
80
+ static #resolveJustificationVerticalAnchor(fields) {
81
+ const justification =
82
+ ParserUtils.parseNumericField(fields, 'Justification') ??
83
+ ParserUtils.parseNumericField(fields, 'Alignment')
84
+
85
+ if (!Number.isInteger(justification)) {
86
+ return null
87
+ }
88
+
89
+ const normalizedJustification = ((justification % 9) + 9) % 9
90
+ const verticalRow = Math.floor(normalizedJustification / 3)
91
+
92
+ if (verticalRow === 1) return 'middle'
93
+ if (verticalRow === 2) return 'top'
94
+ return null
95
+ }
96
+
68
97
  /**
69
98
  * Returns true for component designator and parameter text records.
70
99
  * @param {string} recordType Native text record type.
@@ -137,7 +137,7 @@ export class SchematicTextParser {
137
137
  * @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
138
138
  * @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
139
139
  * @param {Map<string, Record<string, string>>} [ownerMetadata]
140
- * @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', verticalAnchor?: 'top', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
140
+ * @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', verticalAnchor?: 'middle' | 'top', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
141
141
  */
142
142
  static normalizeSchematicTextRecord(
143
143
  fields,
@@ -15,7 +15,7 @@ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
15
15
  export class SchematicDirectiveRenderer {
16
16
  /**
17
17
  * Builds directive markup for supported schematic directive primitives.
18
- * @param {{ x: number, y: number, color: string, name: string, orientation?: number }[]} directives
18
+ * @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }[]} directives
19
19
  * @param {number} sheetHeight
20
20
  * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
21
21
  * @returns {string}
@@ -34,7 +34,7 @@ export class SchematicDirectiveRenderer {
34
34
 
35
35
  /**
36
36
  * Builds one supported directive glyph.
37
- * @param {{ x: number, y: number, color: string, name: string, orientation?: number }} directive
37
+ * @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }} directive
38
38
  * @param {number} sheetHeight
39
39
  * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
40
40
  * @returns {string}
@@ -67,13 +67,24 @@ export class SchematicDirectiveRenderer {
67
67
 
68
68
  /**
69
69
  * Builds the labeled info-callout marker for one parameter-set directive.
70
- * @param {{ x: number, y: number, color: string, name: string, orientation?: number }} directive
70
+ * @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }} directive
71
71
  * @param {number} sheetHeight
72
72
  * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
73
73
  * @param {string} classModifier
74
74
  * @returns {string}
75
75
  */
76
76
  static #buildRouteMarkup(directive, sheetHeight, sheet, classModifier) {
77
+ if (
78
+ classModifier === 'parameter-set' &&
79
+ Number(directive?.style) === 1
80
+ ) {
81
+ return SchematicDirectiveRenderer.#buildCompactParameterSetMarkup(
82
+ directive,
83
+ sheetHeight,
84
+ classModifier
85
+ )
86
+ }
87
+
77
88
  const color = SchematicColorResolver.resolveColor(
78
89
  directive.color,
79
90
  '--schematic-alert-color'
@@ -154,6 +165,61 @@ export class SchematicDirectiveRenderer {
154
165
  )
155
166
  }
156
167
 
168
+ /**
169
+ * Builds the compact style-1 parameter-set marker used for connection
170
+ * adornments whose class name is carried as metadata rather than text.
171
+ * @param {{ x: number, y: number, color: string, orientation?: number }} directive
172
+ * @param {number} sheetHeight
173
+ * @param {string} classModifier
174
+ * @returns {string}
175
+ */
176
+ static #buildCompactParameterSetMarkup(
177
+ directive,
178
+ sheetHeight,
179
+ classModifier
180
+ ) {
181
+ const color = SchematicColorResolver.resolveColor(
182
+ directive.color,
183
+ '--schematic-alert-color'
184
+ )
185
+ const projectedY = projectSchematicY(sheetHeight, directive.y)
186
+ const direction = SchematicDirectiveRenderer.#resolveCalloutDirection(
187
+ directive.orientation
188
+ )
189
+ const circleRadius = 3
190
+ const circleCenterX = directive.x + direction.x * 6
191
+ const circleCenterY = projectedY + direction.y * 6
192
+ const leaderEndX = circleCenterX - direction.x * circleRadius
193
+ const leaderEndY = circleCenterY - direction.y * circleRadius
194
+
195
+ return (
196
+ '<g class="schematic-directive schematic-directive--' +
197
+ escapeHtml(classModifier) +
198
+ '">' +
199
+ '<line x1="' +
200
+ formatNumber(directive.x) +
201
+ '" y1="' +
202
+ formatNumber(projectedY) +
203
+ '" x2="' +
204
+ formatNumber(leaderEndX) +
205
+ '" y2="' +
206
+ formatNumber(leaderEndY) +
207
+ '" stroke="' +
208
+ escapeHtml(color) +
209
+ '" stroke-width="1" />' +
210
+ '<circle cx="' +
211
+ formatNumber(circleCenterX) +
212
+ '" cy="' +
213
+ formatNumber(circleCenterY) +
214
+ '" r="' +
215
+ formatNumber(circleRadius) +
216
+ '" fill="none" stroke="' +
217
+ escapeHtml(color) +
218
+ '" stroke-width="1" />' +
219
+ '</g>'
220
+ )
221
+ }
222
+
157
223
  /**
158
224
  * Resolves Altium's four-way callout orientation into an outward vector.
159
225
  * @param {number | undefined} orientation
@@ -17,7 +17,8 @@ export class SchematicLineColorResolver {
17
17
  if (SchematicLineColorResolver.isElectricalLine(line)) {
18
18
  return SchematicColorResolver.resolveNonTextColor(
19
19
  line?.color,
20
- '--schematic-default-ink-color'
20
+ '--schematic-default-ink-color',
21
+ true
21
22
  )
22
23
  }
23
24
 
@@ -37,7 +38,8 @@ export class SchematicLineColorResolver {
37
38
 
38
39
  return SchematicColorResolver.resolveNonTextColor(
39
40
  line?.color,
40
- '--schematic-default-ink-color'
41
+ '--schematic-default-ink-color',
42
+ true
41
43
  )
42
44
  }
43
45
 
@@ -143,6 +143,56 @@ export class SchematicOwnerPinLabelLayout {
143
143
  return offsets
144
144
  }
145
145
 
146
+ /**
147
+ * Collects pins whose owner already draws a visible text label for the pin
148
+ * designator, avoiding a second synthetic pin-number label at the contact.
149
+ * @param {{ ownerIndex?: string, text?: string, x?: number, y?: number, recordType?: string, hidden?: boolean }[]} texts
150
+ * @param {{ ownerIndex?: string, designator?: string, x: number, y: number, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
151
+ * @returns {Set<string>}
152
+ */
153
+ static collectExplicitOwnerPinNumberLabelKeys(texts, pins) {
154
+ const ownerTexts = SchematicOwnerPinLabelLayout.#groupByOwnerIndex(
155
+ (texts || []).filter(
156
+ (text) => text.recordType === '4' && !text.hidden
157
+ )
158
+ )
159
+ const keys = new Set()
160
+
161
+ for (const pin of pins || []) {
162
+ const ownerIndex = String(pin.ownerIndex || '').trim()
163
+ const designator = String(pin.designator || '').trim()
164
+
165
+ if (
166
+ !ownerIndex ||
167
+ !designator ||
168
+ (pin.labelMode || 'name-and-number') === 'hidden' ||
169
+ (pin.labelMode || 'name-and-number') === 'name-only'
170
+ ) {
171
+ continue
172
+ }
173
+
174
+ const nativeNumberLabels = ownerTexts.get(ownerIndex) || []
175
+ const hasNativeNumberLabel = nativeNumberLabels.some((text) =>
176
+ SchematicOwnerPinLabelLayout.#isExplicitOwnerPinNumberLabel(
177
+ text,
178
+ pin,
179
+ designator
180
+ )
181
+ )
182
+
183
+ if (hasNativeNumberLabel) {
184
+ keys.add(
185
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
186
+ ownerIndex,
187
+ designator
188
+ )
189
+ )
190
+ }
191
+ }
192
+
193
+ return keys
194
+ }
195
+
146
196
  /**
147
197
  * Collects compact FET-like owner groups whose numeric contact labels need
148
198
  * to stay outside the owner-drawn device body.
@@ -481,6 +531,53 @@ export class SchematicOwnerPinLabelLayout {
481
531
  )
482
532
  }
483
533
 
534
+ /**
535
+ * Returns true when an owner text is a native pin-number label for a pin.
536
+ * @param {{ text?: string, x?: number, y?: number }} text
537
+ * @param {{ x: number, y: number, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
538
+ * @param {string} designator
539
+ * @returns {boolean}
540
+ */
541
+ static #isExplicitOwnerPinNumberLabel(text, pin, designator) {
542
+ if (String(text?.text || '').trim() !== designator) {
543
+ return false
544
+ }
545
+
546
+ const textX = Number(text?.x)
547
+ const textY = Number(text?.y)
548
+ const pinX = Number(pin?.x)
549
+ const pinY = Number(pin?.y)
550
+
551
+ if (
552
+ !Number.isFinite(textX) ||
553
+ !Number.isFinite(textY) ||
554
+ !Number.isFinite(pinX) ||
555
+ !Number.isFinite(pinY)
556
+ ) {
557
+ return false
558
+ }
559
+
560
+ const laneTolerance = 6
561
+ const axisTolerance =
562
+ Math.max(Math.abs(Number(pin.length || 0)), 10) + 6
563
+
564
+ if (pin.orientation === 'left' || pin.orientation === 'right') {
565
+ return (
566
+ Math.abs(textY - pinY) <= laneTolerance &&
567
+ Math.abs(textX - pinX) <= axisTolerance
568
+ )
569
+ }
570
+
571
+ if (pin.orientation === 'top' || pin.orientation === 'bottom') {
572
+ return (
573
+ Math.abs(textX - pinX) <= laneTolerance &&
574
+ Math.abs(textY - pinY) <= axisTolerance
575
+ )
576
+ }
577
+
578
+ return false
579
+ }
580
+
484
581
  /**
485
582
  * Groups schematic owner-local primitives by owner index.
486
583
  * @template T
@@ -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>} explicitOwnerPinNumberLabelKeys
27
28
  * @param {Set<string>} overlappingExternalNumberLabelKeys
28
29
  * @returns {string}
29
30
  */
@@ -36,6 +37,7 @@ export class SchematicPinSvgRenderer {
36
37
  explicitOwnerPinLabelOffsets,
37
38
  compactExternalNumberLabelSides = new Map(),
38
39
  internalNumberLabelBoxes = new Map(),
40
+ explicitOwnerPinNumberLabelKeys = new Set(),
39
41
  overlappingExternalNumberLabelKeys = new Set()
40
42
  ) {
41
43
  const geometry =
@@ -76,13 +78,14 @@ export class SchematicPinSvgRenderer {
76
78
  null
77
79
  const internalNumberLabelBox =
78
80
  internalNumberLabelBoxes.get(String(pin.ownerIndex || '')) || null
79
- const shouldRenderExternalNumber =
80
- !overlappingExternalNumberLabelKeys.has(
81
- SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
82
- pin.ownerIndex,
83
- pin.designator
84
- )
81
+ const ownerPinNumberLabelKey =
82
+ SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
83
+ pin.ownerIndex,
84
+ pin.designator
85
85
  )
86
+ const shouldRenderExternalNumber =
87
+ !explicitOwnerPinNumberLabelKeys.has(ownerPinNumberLabelKey) &&
88
+ !overlappingExternalNumberLabelKeys.has(ownerPinNumberLabelKey)
86
89
 
87
90
  if (pin.orientation === 'left') {
88
91
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
@@ -247,6 +250,7 @@ export class SchematicPinSvgRenderer {
247
250
  if (
248
251
  labelMode !== 'hidden' &&
249
252
  labelMode !== 'name-only' &&
253
+ shouldRenderExternalNumber &&
250
254
  (pin.orientation === 'top' || pin.orientation === 'bottom')
251
255
  ) {
252
256
  texts.push(
@@ -34,6 +34,7 @@ const SECTION_HEADING_LINE_X_PADDING = 15
34
34
  const BODY_TEXT_ASCENT_RATIO = 0.72
35
35
  const BODY_TEXT_DESCENT_RATIO = 0.14
36
36
  const BODY_TEXT_FLAT_DESCENT_RATIO = 0
37
+ const TEXT_VERTICAL_MIDDLE_BASELINE_RATIO = 0.36
37
38
  const BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO = 0.3
38
39
  const BODY_TEXT_DESCENDER_PATTERN = /[gjpqyQ_,;]/
39
40
 
@@ -45,7 +46,7 @@ export class SchematicSvgRenderer {
45
46
 
46
47
  /**
47
48
  * Renders a normalized schematic model into SVG markup.
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, textSegments?: { text: string, overline: boolean }[], 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
49
+ * @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, style?: number }[], texts: { x: number, y: number, text: string, textSegments?: { text: string, overline: boolean }[], 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
49
50
  * @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
50
51
  * @returns {string}
51
52
  */
@@ -634,6 +635,11 @@ export class SchematicSvgRenderer {
634
635
  pins,
635
636
  contentRectangles
636
637
  )
638
+ const explicitOwnerPinNumberLabelKeys =
639
+ SchematicOwnerPinLabelLayout.collectExplicitOwnerPinNumberLabelKeys(
640
+ texts,
641
+ pins
642
+ )
637
643
  const overlappingExternalNumberLabelKeys =
638
644
  SchematicOwnerPinLabelLayout.collectOverlappingExternalNumberLabelKeys(
639
645
  pins,
@@ -652,6 +658,7 @@ export class SchematicSvgRenderer {
652
658
  explicitOwnerPinLabelOffsets,
653
659
  compactExternalNumberLabelSides,
654
660
  internalNumberLabelBoxes,
661
+ explicitOwnerPinNumberLabelKeys,
655
662
  overlappingExternalNumberLabelKeys
656
663
  ),
657
664
  SchematicSvgRenderer.#semanticAttributes(
@@ -2702,7 +2709,7 @@ export class SchematicSvgRenderer {
2702
2709
 
2703
2710
  /**
2704
2711
  * Resolves final text placement for schematic free-text annotations.
2705
- * @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end', verticalAnchor?: 'top' }} text
2712
+ * @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end', verticalAnchor?: 'middle' | 'top' }} text
2706
2713
  * @param {number} sheetHeight
2707
2714
  * @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
2708
2715
  * @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
@@ -2822,17 +2829,26 @@ export class SchematicSvgRenderer {
2822
2829
  }
2823
2830
 
2824
2831
  /**
2825
- * Converts top-anchored text coordinates into SVG baseline coordinates.
2826
- * @param {{ verticalAnchor?: 'top' }} text Text primitive.
2832
+ * Converts vertically anchored text coordinates into SVG baseline
2833
+ * coordinates.
2834
+ * @param {{ verticalAnchor?: 'middle' | 'top' }} text Text primitive.
2827
2835
  * @param {number} fontSize Viewer font size.
2828
2836
  * @returns {number}
2829
2837
  */
2830
2838
  static #resolveVerticalAnchorBaselineOffset(text, fontSize) {
2831
- if (text?.verticalAnchor !== 'top') {
2839
+ if (!Number.isFinite(fontSize) || fontSize <= 0) {
2832
2840
  return 0
2833
2841
  }
2834
2842
 
2835
- return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 0
2843
+ if (text?.verticalAnchor === 'middle') {
2844
+ return fontSize * TEXT_VERTICAL_MIDDLE_BASELINE_RATIO
2845
+ }
2846
+
2847
+ if (text?.verticalAnchor === 'top') {
2848
+ return fontSize
2849
+ }
2850
+
2851
+ return 0
2836
2852
  }
2837
2853
 
2838
2854
  /**