altium-toolkit 1.1.2 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  8. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  9. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  10. package/src/core/altium/AltiumParser.mjs +245 -10
  11. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  12. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  13. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  14. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  15. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  16. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  17. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  18. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  19. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  20. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  21. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  22. package/src/core/altium/SchematicImageParser.mjs +291 -6
  23. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  24. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  25. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  26. package/src/core/altium/SchematicPinParser.mjs +262 -24
  27. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  28. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  29. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  30. package/src/core/altium/SchematicTextParser.mjs +125 -11
  31. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  32. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  33. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  34. package/src/core/altium/SourceComponentClient.mjs +239 -0
  35. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  36. package/src/parser.mjs +8 -0
  37. package/src/styles/altium-renderers.css +6 -6
  38. package/src/ui/PcbArcUtils.mjs +19 -2
  39. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  40. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  41. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  42. package/src/ui/SchematicColorResolver.mjs +263 -0
  43. package/src/ui/SchematicContentLayout.mjs +58 -1
  44. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  45. package/src/ui/SchematicImageRenderer.mjs +125 -10
  46. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  47. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  48. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  49. package/src/ui/SchematicNoteRenderer.mjs +87 -7
  50. package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
  51. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  52. package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
  53. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  54. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  55. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  56. package/src/ui/SchematicShapeRenderer.mjs +109 -24
  57. package/src/ui/SchematicSvgRenderer.mjs +1210 -71
@@ -31,6 +31,7 @@ export class SchematicColorResolver {
31
31
  * Resolves one SVG color value to a schematic theme variable.
32
32
  * @param {string | undefined} color
33
33
  * @param {string} fallbackVariable
34
+ * @param {boolean} [preserveUnknown]
34
35
  * @returns {string}
35
36
  */
36
37
  static resolveColor(color, fallbackVariable, preserveUnknown = false) {
@@ -59,10 +60,36 @@ export class SchematicColorResolver {
59
60
  : SchematicColorResolver.#toVariable(fallbackVariable)
60
61
  }
61
62
 
63
+ /**
64
+ * Resolves one non-text primitive color, using the semantic fallback when
65
+ * source artwork only supplies the default black text color.
66
+ * @param {string | undefined} color
67
+ * @param {string} fallbackVariable
68
+ * @param {boolean} [preserveUnknown]
69
+ * @returns {string}
70
+ */
71
+ static resolveNonTextColor(
72
+ color,
73
+ fallbackVariable,
74
+ preserveUnknown = false
75
+ ) {
76
+ const resolved = SchematicColorResolver.resolveColor(
77
+ color,
78
+ fallbackVariable,
79
+ preserveUnknown
80
+ )
81
+
82
+ return resolved ===
83
+ SchematicColorResolver.#toVariable('--schematic-text-color')
84
+ ? SchematicColorResolver.#toVariable(fallbackVariable)
85
+ : resolved
86
+ }
87
+
62
88
  /**
63
89
  * Resolves one SVG fill value to a schematic theme variable.
64
90
  * @param {string | undefined} fill
65
91
  * @param {string} fallbackVariable
92
+ * @param {boolean} [preserveUnknown]
66
93
  * @returns {string}
67
94
  */
68
95
  static resolveFill(fill, fallbackVariable, preserveUnknown = false) {
@@ -100,6 +127,121 @@ export class SchematicColorResolver {
100
127
  : SchematicColorResolver.#toVariable(fallbackVariable)
101
128
  }
102
129
 
130
+ /**
131
+ * Resolves an explicitly authored source stroke color without mapping it to
132
+ * a semantic theme token.
133
+ * @param {string | undefined} color
134
+ * @param {string} fallbackVariable
135
+ * @returns {string}
136
+ */
137
+ static resolveSourceColor(color, fallbackVariable) {
138
+ return SchematicColorResolver.#resolveSourcePaint(
139
+ color,
140
+ fallbackVariable
141
+ )
142
+ }
143
+
144
+ /**
145
+ * Resolves an explicitly authored source fill color without mapping it to a
146
+ * semantic theme token.
147
+ * @param {string | undefined} fill
148
+ * @param {string} fallbackVariable
149
+ * @returns {string}
150
+ */
151
+ static resolveSourceFill(fill, fallbackVariable) {
152
+ return SchematicColorResolver.#resolveSourcePaint(
153
+ fill,
154
+ fallbackVariable
155
+ )
156
+ }
157
+
158
+ /**
159
+ * Resolves an authored source stroke through a muted source palette.
160
+ * @param {string | undefined} color
161
+ * @param {string} fallbackVariable
162
+ * @returns {string}
163
+ */
164
+ static resolveMutedSourceColor(color, fallbackVariable) {
165
+ return SchematicColorResolver.#resolveMutedSourcePaint(
166
+ color,
167
+ fallbackVariable
168
+ )
169
+ }
170
+
171
+ /**
172
+ * Resolves an authored source fill through a muted source palette.
173
+ * @param {string | undefined} fill
174
+ * @param {string} fallbackVariable
175
+ * @returns {string}
176
+ */
177
+ static resolveMutedSourceFill(fill, fallbackVariable) {
178
+ return SchematicColorResolver.#resolveMutedSourcePaint(
179
+ fill,
180
+ fallbackVariable
181
+ )
182
+ }
183
+
184
+ /**
185
+ * Resolves one literal source paint value while preserving SVG control
186
+ * values and falling back only when no source paint exists.
187
+ * @param {string | undefined} paint
188
+ * @param {string} fallbackVariable
189
+ * @returns {string}
190
+ */
191
+ static #resolveSourcePaint(paint, fallbackVariable) {
192
+ const normalized = SchematicColorResolver.#normalizeColor(paint)
193
+
194
+ if (!normalized) {
195
+ return SchematicColorResolver.#toVariable(fallbackVariable)
196
+ }
197
+
198
+ if (
199
+ normalized === 'none' ||
200
+ normalized === 'transparent' ||
201
+ normalized.startsWith('var(')
202
+ ) {
203
+ return normalized
204
+ }
205
+
206
+ return normalized
207
+ }
208
+
209
+ /**
210
+ * Resolves one literal source paint while reducing vivid palette colors.
211
+ * @param {string | undefined} paint
212
+ * @param {string} fallbackVariable
213
+ * @returns {string}
214
+ */
215
+ static #resolveMutedSourcePaint(paint, fallbackVariable) {
216
+ const normalized = SchematicColorResolver.#normalizeColor(paint)
217
+
218
+ if (!normalized) {
219
+ return SchematicColorResolver.#toVariable(fallbackVariable)
220
+ }
221
+
222
+ if (
223
+ normalized === 'none' ||
224
+ normalized === 'transparent' ||
225
+ normalized.startsWith('var(')
226
+ ) {
227
+ return normalized
228
+ }
229
+
230
+ const rgb = SchematicColorResolver.#parseHexColor(normalized)
231
+ if (!rgb) {
232
+ return normalized
233
+ }
234
+
235
+ const hsl = SchematicColorResolver.#rgbToHsl(rgb)
236
+ const mutedRgb = SchematicColorResolver.#hslToRgb({
237
+ h: hsl.h,
238
+ s: Math.min(hsl.s, 45),
239
+ l: Math.min(Math.max(hsl.l, 30), 68)
240
+ })
241
+
242
+ return SchematicColorResolver.#formatHexColor(mutedRgb)
243
+ }
244
+
103
245
  /**
104
246
  * Normalizes one raw color string for token lookup.
105
247
  * @param {string | undefined} color
@@ -111,6 +253,127 @@ export class SchematicColorResolver {
111
253
  .toLowerCase()
112
254
  }
113
255
 
256
+ /**
257
+ * Parses a six-digit hex color.
258
+ * @param {string} color
259
+ * @returns {{ r: number, g: number, b: number } | null}
260
+ */
261
+ static #parseHexColor(color) {
262
+ const match = /^#([0-9a-f]{6})$/u.exec(color)
263
+ if (!match) {
264
+ return null
265
+ }
266
+
267
+ const value = match[1]
268
+ return {
269
+ r: Number.parseInt(value.slice(0, 2), 16),
270
+ g: Number.parseInt(value.slice(2, 4), 16),
271
+ b: Number.parseInt(value.slice(4, 6), 16)
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Converts RGB channels to HSL.
277
+ * @param {{ r: number, g: number, b: number }} color
278
+ * @returns {{ h: number, s: number, l: number }}
279
+ */
280
+ static #rgbToHsl(color) {
281
+ const r = color.r / 255
282
+ const g = color.g / 255
283
+ const b = color.b / 255
284
+ const max = Math.max(r, g, b)
285
+ const min = Math.min(r, g, b)
286
+ let h = 0
287
+ let s = 0
288
+ const l = ((max + min) / 2) * 100
289
+
290
+ if (max !== min) {
291
+ const delta = max - min
292
+ s =
293
+ ((max + min) / 2 > 0.5
294
+ ? delta / (2 - max - min)
295
+ : delta / (max + min)) * 100
296
+
297
+ switch (max) {
298
+ case r:
299
+ h = (g - b) / delta + (g < b ? 6 : 0)
300
+ break
301
+ case g:
302
+ h = (b - r) / delta + 2
303
+ break
304
+ default:
305
+ h = (r - g) / delta + 4
306
+ break
307
+ }
308
+
309
+ h /= 6
310
+ }
311
+
312
+ return { h, s, l }
313
+ }
314
+
315
+ /**
316
+ * Converts HSL channels to RGB.
317
+ * @param {{ h: number, s: number, l: number }} color
318
+ * @returns {{ r: number, g: number, b: number }}
319
+ */
320
+ static #hslToRgb(color) {
321
+ const s = color.s / 100
322
+ const l = color.l / 100
323
+
324
+ if (s === 0) {
325
+ const value = Math.round(l * 255)
326
+ return { r: value, g: value, b: value }
327
+ }
328
+
329
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s
330
+ const p = 2 * l - q
331
+ const r = SchematicColorResolver.#hueToRgb(p, q, color.h + 1 / 3)
332
+ const g = SchematicColorResolver.#hueToRgb(p, q, color.h)
333
+ const b = SchematicColorResolver.#hueToRgb(p, q, color.h - 1 / 3)
334
+
335
+ return {
336
+ r: Math.round(r * 255),
337
+ g: Math.round(g * 255),
338
+ b: Math.round(b * 255)
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Converts one hue channel to RGB.
344
+ * @param {number} p
345
+ * @param {number} q
346
+ * @param {number} t
347
+ * @returns {number}
348
+ */
349
+ static #hueToRgb(p, q, t) {
350
+ let value = t
351
+ if (value < 0) value += 1
352
+ if (value > 1) value -= 1
353
+ if (value < 1 / 6) return p + (q - p) * 6 * value
354
+ if (value < 1 / 2) return q
355
+ if (value < 2 / 3) return p + (q - p) * (2 / 3 - value) * 6
356
+ return p
357
+ }
358
+
359
+ /**
360
+ * Formats RGB channels as a hex color.
361
+ * @param {{ r: number, g: number, b: number }} color
362
+ * @returns {string}
363
+ */
364
+ static #formatHexColor(color) {
365
+ return (
366
+ '#' +
367
+ [color.r, color.g, color.b]
368
+ .map((channel) =>
369
+ Math.max(0, Math.min(255, channel))
370
+ .toString(16)
371
+ .padStart(2, '0')
372
+ )
373
+ .join('')
374
+ )
375
+ }
376
+
114
377
  /**
115
378
  * Wraps one CSS custom property name in `var(...)` markup.
116
379
  * @param {string} variableName
@@ -721,7 +721,12 @@ export class SchematicContentLayout {
721
721
  }
722
722
 
723
723
  for (const text of schematic?.texts || []) {
724
- coordinates.push([text.x, projectSchematicY(sheetHeight, text.y)])
724
+ coordinates.push(
725
+ ...SchematicContentLayout.#collectRenderedTextPoints(
726
+ text,
727
+ sheetHeight
728
+ )
729
+ )
725
730
  }
726
731
 
727
732
  for (const component of schematic?.components || []) {
@@ -829,6 +834,58 @@ export class SchematicContentLayout {
829
834
  }))
830
835
  }
831
836
 
837
+ /**
838
+ * Collects rendered bounds points for a text primitive.
839
+ * @param {{ x?: number, y?: number, recordType?: string, cornerX?: number, cornerY?: number, noteLines?: string[] }} text Text primitive.
840
+ * @param {number} sheetHeight Sheet height.
841
+ * @returns {number[][]}
842
+ */
843
+ static #collectRenderedTextPoints(text, sheetHeight) {
844
+ const x = Number(text?.x)
845
+ const y = Number(text?.y)
846
+
847
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
848
+ return []
849
+ }
850
+
851
+ if (SchematicContentLayout.#hasRenderedTextFrame(text)) {
852
+ const cornerX = Number(text.cornerX)
853
+ const cornerY = Number(text.cornerY)
854
+ const left = Math.min(x, cornerX)
855
+ const right = Math.max(x, cornerX)
856
+ const top = Math.min(
857
+ projectSchematicY(sheetHeight, y),
858
+ projectSchematicY(sheetHeight, cornerY)
859
+ )
860
+ const bottom = Math.max(
861
+ projectSchematicY(sheetHeight, y),
862
+ projectSchematicY(sheetHeight, cornerY)
863
+ )
864
+
865
+ return [
866
+ [left, top],
867
+ [right, bottom]
868
+ ]
869
+ }
870
+
871
+ return [[x, projectSchematicY(sheetHeight, y)]]
872
+ }
873
+
874
+ /**
875
+ * Returns true when a text primitive renders as a framed note/callout box.
876
+ * @param {{ recordType?: string, cornerX?: number, cornerY?: number, noteLines?: string[] }} text Text primitive.
877
+ * @returns {boolean}
878
+ */
879
+ static #hasRenderedTextFrame(text) {
880
+ const recordType = String(text?.recordType || '')
881
+
882
+ return Boolean(
883
+ (recordType === '28' || recordType === '209') &&
884
+ Number.isFinite(Number(text?.cornerX)) &&
885
+ Number.isFinite(Number(text?.cornerY))
886
+ )
887
+ }
888
+
832
889
  /**
833
890
  * Computes the inner endpoint for one schematic pin stub.
834
891
  * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
@@ -50,32 +50,45 @@ export class SchematicDirectiveRenderer {
50
50
  return SchematicDirectiveRenderer.#buildRouteMarkup(
51
51
  directive,
52
52
  sheetHeight,
53
- sheet
53
+ sheet,
54
+ 'route'
54
55
  )
55
56
  default:
56
- return ''
57
+ return directive?.name
58
+ ? SchematicDirectiveRenderer.#buildRouteMarkup(
59
+ directive,
60
+ sheetHeight,
61
+ sheet,
62
+ 'parameter-set'
63
+ )
64
+ : ''
57
65
  }
58
66
  }
59
67
 
60
68
  /**
61
- * Builds the labeled route-callout marker for one differential-pair
62
- * routing directive.
63
- * @param {{ x: number, y: number, color: string, orientation?: number }} directive
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
64
71
  * @param {number} sheetHeight
65
72
  * @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
73
+ * @param {string} classModifier
66
74
  * @returns {string}
67
75
  */
68
- static #buildRouteMarkup(directive, sheetHeight, sheet) {
76
+ static #buildRouteMarkup(directive, sheetHeight, sheet, classModifier) {
69
77
  const color = SchematicColorResolver.resolveColor(
70
78
  directive.color,
71
79
  '--schematic-alert-color'
72
80
  )
73
81
  const projectedY = projectSchematicY(sheetHeight, directive.y)
74
- const verticalDirection =
75
- Number(directive.orientation || 0) === 3 ? 1 : -1
82
+ const direction = SchematicDirectiveRenderer.#resolveCalloutDirection(
83
+ directive.orientation
84
+ )
76
85
  const circleRadius = 7
77
- const circleCenterY = projectedY + verticalDirection * 18
78
- const leaderEndY = circleCenterY - verticalDirection * circleRadius
86
+ const calloutDistance =
87
+ SchematicDirectiveRenderer.#resolveCalloutDistance(direction)
88
+ const circleCenterX = directive.x + direction.x * calloutDistance
89
+ const circleCenterY = projectedY + direction.y * calloutDistance
90
+ const leaderEndX = circleCenterX - direction.x * circleRadius
91
+ const leaderEndY = circleCenterY - direction.y * circleRadius
79
92
  const labelOptions =
80
93
  SchematicTypography.buildViewerSchematicFontOptions(sheet)
81
94
  const infoOptions = {
@@ -83,26 +96,32 @@ export class SchematicDirectiveRenderer {
83
96
  fontSize: Math.max(Number(labelOptions.fontSize || 9) - 1, 6),
84
97
  fontWeight: 700
85
98
  }
86
- const labelY =
87
- circleCenterY +
88
- verticalDirection *
89
- (circleRadius + Number(labelOptions.fontSize || 9))
99
+ const labelPlacement =
100
+ SchematicDirectiveRenderer.#resolveLabelPlacement(
101
+ circleCenterX,
102
+ circleCenterY,
103
+ direction,
104
+ circleRadius,
105
+ Number(labelOptions.fontSize || 9)
106
+ )
90
107
 
91
108
  return (
92
- '<g class="schematic-directive schematic-directive--route">' +
109
+ '<g class="schematic-directive schematic-directive--' +
110
+ escapeHtml(classModifier) +
111
+ '">' +
93
112
  '<line x1="' +
94
113
  formatNumber(directive.x) +
95
114
  '" y1="' +
96
115
  formatNumber(projectedY) +
97
116
  '" x2="' +
98
- formatNumber(directive.x) +
117
+ formatNumber(leaderEndX) +
99
118
  '" y2="' +
100
119
  formatNumber(leaderEndY) +
101
120
  '" stroke="' +
102
121
  escapeHtml(color) +
103
122
  '" stroke-width="1" />' +
104
123
  '<circle cx="' +
105
- formatNumber(directive.x) +
124
+ formatNumber(circleCenterX) +
106
125
  '" cy="' +
107
126
  formatNumber(circleCenterY) +
108
127
  '" r="' +
@@ -112,17 +131,20 @@ export class SchematicDirectiveRenderer {
112
131
  '" stroke-width="1" />' +
113
132
  createSvgText(
114
133
  'schematic-directive-label',
115
- directive.x,
116
- labelY,
134
+ labelPlacement.x,
135
+ labelPlacement.y,
117
136
  String(directive.name || ''),
118
137
  color,
119
- 'middle',
138
+ labelPlacement.anchor,
120
139
  labelOptions
121
140
  ) +
122
141
  createSvgText(
123
142
  'schematic-directive-info',
124
- directive.x,
125
- circleCenterY + Number(infoOptions.fontSize || 8) * 0.34,
143
+ circleCenterX,
144
+ circleCenterY +
145
+ SchematicDirectiveRenderer.#baselineOffset(
146
+ Number(infoOptions.fontSize || 8)
147
+ ),
126
148
  'i',
127
149
  color,
128
150
  'middle',
@@ -132,6 +154,95 @@ export class SchematicDirectiveRenderer {
132
154
  )
133
155
  }
134
156
 
157
+ /**
158
+ * Resolves Altium's four-way callout orientation into an outward vector.
159
+ * @param {number | undefined} orientation
160
+ * @returns {{ x: number, y: number }}
161
+ */
162
+ static #resolveCalloutDirection(orientation) {
163
+ switch (Number(orientation || 0)) {
164
+ case 1:
165
+ return { x: 0, y: -1 }
166
+ case 2:
167
+ return { x: -1, y: 0 }
168
+ case 3:
169
+ return { x: 0, y: 1 }
170
+ default:
171
+ return { x: 1, y: 0 }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Resolves the distance from a directive anchor to its info marker center.
177
+ * @param {{ x: number, y: number }} direction
178
+ * @returns {number}
179
+ */
180
+ static #resolveCalloutDistance(direction) {
181
+ return direction.y < 0 ? 12 : 18
182
+ }
183
+
184
+ /**
185
+ * Resolves label placement outside one directive info marker.
186
+ * @param {number} circleCenterX
187
+ * @param {number} circleCenterY
188
+ * @param {{ x: number, y: number }} direction
189
+ * @param {number} circleRadius
190
+ * @param {number} fontSize
191
+ * @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
192
+ */
193
+ static #resolveLabelPlacement(
194
+ circleCenterX,
195
+ circleCenterY,
196
+ direction,
197
+ circleRadius,
198
+ fontSize
199
+ ) {
200
+ const labelDistance = circleRadius + fontSize
201
+
202
+ if (direction.x < 0) {
203
+ return {
204
+ x: circleCenterX - labelDistance,
205
+ y:
206
+ circleCenterY +
207
+ SchematicDirectiveRenderer.#baselineOffset(fontSize),
208
+ anchor: 'end'
209
+ }
210
+ }
211
+
212
+ if (direction.x > 0) {
213
+ return {
214
+ x: circleCenterX + labelDistance,
215
+ y:
216
+ circleCenterY +
217
+ SchematicDirectiveRenderer.#baselineOffset(fontSize),
218
+ anchor: 'start'
219
+ }
220
+ }
221
+
222
+ if (direction.y < 0) {
223
+ return {
224
+ x: circleCenterX,
225
+ y: circleCenterY - circleRadius - 2,
226
+ anchor: 'middle'
227
+ }
228
+ }
229
+
230
+ return {
231
+ x: circleCenterX,
232
+ y: circleCenterY + direction.y * labelDistance,
233
+ anchor: 'middle'
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Returns a baseline offset that visually centers text around a marker.
239
+ * @param {number} fontSize
240
+ * @returns {number}
241
+ */
242
+ static #baselineOffset(fontSize) {
243
+ return fontSize * 0.34
244
+ }
245
+
135
246
  /**
136
247
  * Builds the paired-trace differential-pair marker glyph.
137
248
  * @param {{ x: number, y: number, color: string }} directive