altium-toolkit 1.0.8 → 1.0.10

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 (102) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -2,6 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ import { PcbBarcodeTextRenderer } from './PcbBarcodeTextRenderer.mjs'
5
6
  import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
6
7
 
7
8
  /**
@@ -51,12 +52,17 @@ export class PcbTextPrimitiveRenderer {
51
52
  /**
52
53
  * Renders selected PCB texts into SVG markup.
53
54
  * @param {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string }[]} texts
55
+ * @param {{ semanticContext?: object }} [options] Render options.
54
56
  * @returns {string}
55
57
  */
56
- static render(texts) {
58
+ static render(texts, options = {}) {
57
59
  return (texts || [])
58
60
  .map((text, index) =>
59
- PcbTextPrimitiveRenderer.#renderText(text, index)
61
+ PcbTextPrimitiveRenderer.#renderText(
62
+ text,
63
+ index,
64
+ options.semanticContext || {}
65
+ )
60
66
  )
61
67
  .join('')
62
68
  }
@@ -65,12 +71,29 @@ export class PcbTextPrimitiveRenderer {
65
71
  * Renders one PCB text primitive.
66
72
  * @param {{ text: string, x: number, y: number, height?: number, rotation?: number, layerId?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string }} text
67
73
  * @param {number} index Text index for stable SVG resource ids.
74
+ * @param {object} semanticContext Semantic lookup context.
68
75
  * @returns {string}
69
76
  */
70
- static #renderText(text, index) {
77
+ static #renderText(text, index, semanticContext) {
71
78
  const fontSize = PcbTextPrimitiveRenderer.#resolveFontSize(text)
72
79
  const rotation = Number(text.rotation || 0)
73
80
  const lines = PcbTextPrimitiveRenderer.#textLines(text)
81
+ const semanticAttributes = PcbTextPrimitiveRenderer.#semanticAttributes(
82
+ text,
83
+ index,
84
+ semanticContext
85
+ )
86
+
87
+ if (PcbTextPrimitiveRenderer.#isBarcodeText(text)) {
88
+ return PcbBarcodeTextRenderer.render(text, {
89
+ transform: PcbTextPrimitiveRenderer.#renderTextTransform(
90
+ text,
91
+ rotation
92
+ ),
93
+ fontSize,
94
+ semanticAttributes
95
+ })
96
+ }
74
97
 
75
98
  if (PcbTextPrimitiveRenderer.#isInvertedText(text)) {
76
99
  return PcbTextPrimitiveRenderer.#renderInvertedText(
@@ -78,7 +101,8 @@ export class PcbTextPrimitiveRenderer {
78
101
  index,
79
102
  fontSize,
80
103
  rotation,
81
- lines
104
+ lines,
105
+ semanticAttributes
82
106
  )
83
107
  }
84
108
 
@@ -95,7 +119,9 @@ export class PcbTextPrimitiveRenderer {
95
119
  PcbTextPrimitiveRenderer.#formatTextNumber(fontSize) +
96
120
  '"' +
97
121
  PcbTextPrimitiveRenderer.#renderFontAttributes(text) +
98
- ' text-anchor="start" dominant-baseline="alphabetic">' +
122
+ ' text-anchor="start" dominant-baseline="alphabetic"' +
123
+ semanticAttributes +
124
+ '>' +
99
125
  content +
100
126
  '</text>'
101
127
  )
@@ -109,9 +135,17 @@ export class PcbTextPrimitiveRenderer {
109
135
  * @param {number} fontSize Text font size in board units.
110
136
  * @param {number} rotation Text rotation in degrees.
111
137
  * @param {string[]} lines Text lines to render.
138
+ * @param {string} semanticAttributes SVG semantic attributes.
112
139
  * @returns {string}
113
140
  */
114
- static #renderInvertedText(text, index, fontSize, rotation, lines) {
141
+ static #renderInvertedText(
142
+ text,
143
+ index,
144
+ fontSize,
145
+ rotation,
146
+ lines,
147
+ semanticAttributes
148
+ ) {
115
149
  const metrics = PcbTextPrimitiveRenderer.#measureLines(
116
150
  text,
117
151
  lines,
@@ -144,7 +178,9 @@ export class PcbTextPrimitiveRenderer {
144
178
  SchematicSvgUtils.escapeHtml(String(Number(text.layerId || 0))) +
145
179
  ' pcb-text--inverted" transform="' +
146
180
  PcbTextPrimitiveRenderer.#renderTextTransform(text, rotation) +
147
- '">' +
181
+ '"' +
182
+ semanticAttributes +
183
+ '>' +
148
184
  '<mask id="' +
149
185
  SchematicSvgUtils.escapeHtml(maskId) +
150
186
  '" maskUnits="userSpaceOnUse" mask-type="luminance" x="' +
@@ -192,6 +228,142 @@ export class PcbTextPrimitiveRenderer {
192
228
  )
193
229
  }
194
230
 
231
+ /**
232
+ * Renders semantic data attributes for one text primitive.
233
+ * @param {object} text Text primitive.
234
+ * @param {number} fallbackIndex Rendered text index.
235
+ * @param {object} semanticContext Semantic lookup context.
236
+ * @returns {string}
237
+ */
238
+ static #semanticAttributes(text, fallbackIndex, semanticContext) {
239
+ const index =
240
+ semanticContext?.primitiveIndexes?.texts?.get(text) ?? fallbackIndex
241
+ const layer = PcbTextPrimitiveRenderer.#layerForText(
242
+ text,
243
+ semanticContext
244
+ )
245
+ const netName = PcbTextPrimitiveRenderer.#netNameForText(
246
+ text,
247
+ semanticContext
248
+ )
249
+ const component = PcbTextPrimitiveRenderer.#componentForText(
250
+ text,
251
+ semanticContext
252
+ )
253
+ const netClasses = netName
254
+ ? semanticContext?.netClassNamesByNetName?.get(netName) || []
255
+ : []
256
+
257
+ return PcbTextPrimitiveRenderer.#renderDataAttributes({
258
+ 'data-primitive': 'text',
259
+ 'data-element-key': 'pcb-text-' + index,
260
+ 'data-layer-key': layer?.layerKey,
261
+ 'data-layer-display-name': layer?.displayName,
262
+ 'data-layer-id': layer?.layerId,
263
+ 'data-net': netName,
264
+ 'data-net-index': text?.netIndex,
265
+ 'data-net-class': netClasses[0],
266
+ 'data-net-classes': netClasses.length > 1 ? netClasses : undefined,
267
+ 'data-component': component?.designator,
268
+ 'data-component-index': component?.componentIndex,
269
+ 'data-text-role':
270
+ text?.role ||
271
+ text?.textRole ||
272
+ (PcbTextPrimitiveRenderer.#isBarcodeText(text)
273
+ ? 'barcode'
274
+ : ''),
275
+ 'data-barcode-kind': text?.barcode?.kindName,
276
+ 'data-barcode-render-mode': text?.barcode?.renderModeName,
277
+ 'data-barcode-inverted':
278
+ text?.barcode?.inverted === true ? 'true' : undefined
279
+ })
280
+ }
281
+
282
+ /**
283
+ * Resolves layer metadata for one text primitive.
284
+ * @param {object} text Text primitive.
285
+ * @param {object} semanticContext Semantic lookup context.
286
+ * @returns {object | null}
287
+ */
288
+ static #layerForText(text, semanticContext) {
289
+ const layerId = Number(text?.layerId)
290
+ if (Number.isInteger(layerId)) {
291
+ return (
292
+ semanticContext?.layersById?.get(layerId) || {
293
+ layerId,
294
+ layerKey: 'L' + layerId,
295
+ displayName: text?.layerName || 'Layer ' + layerId
296
+ }
297
+ )
298
+ }
299
+
300
+ return null
301
+ }
302
+
303
+ /**
304
+ * Resolves net metadata for one text primitive.
305
+ * @param {object} text Text primitive.
306
+ * @param {object} semanticContext Semantic lookup context.
307
+ * @returns {string}
308
+ */
309
+ static #netNameForText(text, semanticContext) {
310
+ if (text?.netName) {
311
+ return String(text.netName)
312
+ }
313
+
314
+ const netIndex = Number(text?.netIndex)
315
+ if (Number.isInteger(netIndex)) {
316
+ return semanticContext?.netByIndex?.get(netIndex)?.name || ''
317
+ }
318
+
319
+ return ''
320
+ }
321
+
322
+ /**
323
+ * Resolves component metadata for one text primitive.
324
+ * @param {object} text Text primitive.
325
+ * @param {object} semanticContext Semantic lookup context.
326
+ * @returns {object | null}
327
+ */
328
+ static #componentForText(text, semanticContext) {
329
+ const componentIndex = Number(text?.componentIndex)
330
+ if (Number.isInteger(componentIndex)) {
331
+ return (
332
+ semanticContext?.componentsByIndex?.get(componentIndex) || null
333
+ )
334
+ }
335
+
336
+ return null
337
+ }
338
+
339
+ /**
340
+ * Renders a dictionary as SVG data attributes.
341
+ * @param {Record<string, unknown>} attributes Attribute dictionary.
342
+ * @returns {string}
343
+ */
344
+ static #renderDataAttributes(attributes) {
345
+ return Object.entries(attributes || {})
346
+ .filter(([, value]) => {
347
+ if (Array.isArray(value)) {
348
+ return value.length > 0
349
+ }
350
+ return value !== null && value !== undefined && value !== ''
351
+ })
352
+ .map(([name, value]) => {
353
+ const renderedValue = Array.isArray(value)
354
+ ? value.join(',')
355
+ : String(value)
356
+ return (
357
+ ' ' +
358
+ name +
359
+ '="' +
360
+ SchematicSvgUtils.escapeHtml(renderedValue) +
361
+ '"'
362
+ )
363
+ })
364
+ .join('')
365
+ }
366
+
195
367
  /**
196
368
  * Renders the local text transform, including authored PCB text mirroring.
197
369
  * @param {{ x?: number, y?: number, mirrored?: boolean }} text Text record.
@@ -246,6 +418,20 @@ export class PcbTextPrimitiveRenderer {
246
418
  )
247
419
  }
248
420
 
421
+ /**
422
+ * Checks whether a text primitive should render as barcode artwork.
423
+ * @param {{ fontType?: number | string, fontTypeName?: string, barcode?: object }} text Text record.
424
+ * @returns {boolean}
425
+ */
426
+ static #isBarcodeText(text) {
427
+ const fontTypeName = String(text?.fontTypeName || '').toUpperCase()
428
+ return (
429
+ Boolean(text?.barcode) ||
430
+ Number(text?.fontType) === 2 ||
431
+ fontTypeName.includes('BARCODE')
432
+ )
433
+ }
434
+
249
435
  /**
250
436
  * Resolves the imported TrueType scale used by browser outline fonts.
251
437
  * @param {{ trueTypeFontScale?: number, fontMetrics?: { emScaleFromPcbHeight?: number } }} text Text record.
@@ -15,7 +15,7 @@ const MINIMUM_NOTE_TEXT_SIZE = 4
15
15
  export class SchematicNoteRenderer {
16
16
  /**
17
17
  * Builds one boxed schematic note/callout with wrapped text rows.
18
- * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
18
+ * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, lineWidth?: number, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
19
19
  * @param {number} sheetHeight
20
20
  * @returns {string}
21
21
  */
@@ -51,6 +51,7 @@ export class SchematicNoteRenderer {
51
51
  '--schematic-note-border-color'
52
52
  )
53
53
  const noteStroke = text.showBorder ? borderColor : 'none'
54
+ const noteStrokeWidth = Number(text.lineWidth)
54
55
  const noteSourceLines = text.noteLines || []
55
56
  const compactSingleLineNote =
56
57
  SchematicNoteRenderer.#isCompactSingleLineNote(
@@ -112,7 +113,13 @@ export class SchematicNoteRenderer {
112
113
  escapeHtml(noteFill) +
113
114
  '" stroke="' +
114
115
  escapeHtml(noteStroke) +
115
- '" />' +
116
+ '"' +
117
+ (Number.isFinite(noteStrokeWidth)
118
+ ? ' stroke-width="' +
119
+ formatNumber(Math.max(noteStrokeWidth, 0.8)) +
120
+ '"'
121
+ : '') +
122
+ ' />' +
116
123
  textMarkup +
117
124
  '</g>'
118
125
  )
@@ -7,6 +7,38 @@
7
7
  * synthetic pin-number clearance.
8
8
  */
9
9
  export class SchematicOwnerPinLabelLayout {
10
+ /**
11
+ * Resolves one native-facing pin text placement in renderer coordinates
12
+ * before sheet Y projection. The returned `yOffset` is applied after
13
+ * projection, matching SVG text baseline behavior.
14
+ * @param {{ x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', symbolOuter?: number }} pin
15
+ * @param {'name' | 'number'} labelKind
16
+ * @param {{ labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', rotateTopNumber?: boolean }} [options]
17
+ * @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
18
+ */
19
+ static resolveNativePinTextPlacement(pin, labelKind, options = {}) {
20
+ const labelMode = options.labelMode || 'name-and-number'
21
+ const markerStyle =
22
+ SchematicOwnerPinLabelLayout.#resolveOuterPinMarkerStyle(pin)
23
+
24
+ if (labelKind === 'number') {
25
+ return SchematicOwnerPinLabelLayout.#resolveNumberPlacement(
26
+ pin,
27
+ markerStyle,
28
+ options
29
+ )
30
+ }
31
+
32
+ if (labelKind === 'name') {
33
+ return SchematicOwnerPinLabelLayout.#resolveNamePlacement(
34
+ pin,
35
+ labelMode
36
+ )
37
+ }
38
+
39
+ return null
40
+ }
41
+
10
42
  /**
11
43
  * Builds one owner/pin label key.
12
44
  * @param {string | undefined} ownerIndex
@@ -170,4 +202,178 @@ export class SchematicOwnerPinLabelLayout {
170
202
  return baseX
171
203
  }
172
204
  }
205
+
206
+ /**
207
+ * Resolves schematic pin-number placement.
208
+ * @param {{ x: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
209
+ * @param {'single-in' | 'single-out' | 'double' | null} markerStyle
210
+ * @param {{ rotateTopNumber?: boolean }} options
211
+ * @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
212
+ */
213
+ static #resolveNumberPlacement(pin, markerStyle, options) {
214
+ switch (pin.orientation) {
215
+ case 'left':
216
+ return {
217
+ x:
218
+ Number(pin.x) -
219
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberClearance(
220
+ markerStyle,
221
+ pin
222
+ ),
223
+ yOffset: -1,
224
+ anchor: 'end',
225
+ rotation: 0
226
+ }
227
+ case 'right':
228
+ return {
229
+ x:
230
+ Number(pin.x) +
231
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNumberClearance(
232
+ markerStyle,
233
+ pin
234
+ ),
235
+ yOffset: -1,
236
+ anchor: 'start',
237
+ rotation: 0
238
+ }
239
+ case 'top':
240
+ return {
241
+ x: Number(pin.x) - 2,
242
+ yOffset: -6,
243
+ anchor: 'middle',
244
+ rotation: options.rotateTopNumber === false ? 0 : -90
245
+ }
246
+ case 'bottom':
247
+ return {
248
+ x: Number(pin.x) - 2,
249
+ yOffset: 7,
250
+ anchor: 'middle',
251
+ rotation: -90
252
+ }
253
+ default:
254
+ return null
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Resolves schematic pin-name placement.
260
+ * @param {{ x: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
261
+ * @param {'hidden' | 'number-only' | 'name-only' | 'name-and-number'} labelMode
262
+ * @returns {{ x: number, yOffset: number, anchor: 'start' | 'middle' | 'end', rotation: number } | null}
263
+ */
264
+ static #resolveNamePlacement(pin, labelMode) {
265
+ switch (pin.orientation) {
266
+ case 'left':
267
+ return {
268
+ x:
269
+ Number(pin.x) +
270
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNameInset(
271
+ pin,
272
+ labelMode
273
+ ),
274
+ yOffset: 3,
275
+ anchor: 'start',
276
+ rotation: 0
277
+ }
278
+ case 'right':
279
+ return {
280
+ x:
281
+ Number(pin.x) -
282
+ SchematicOwnerPinLabelLayout.#resolveHorizontalPinNameInset(
283
+ pin,
284
+ labelMode
285
+ ),
286
+ yOffset: 3,
287
+ anchor: 'end',
288
+ rotation: 0
289
+ }
290
+ case 'top':
291
+ return {
292
+ x: Number(pin.x),
293
+ yOffset: 4,
294
+ anchor: 'end',
295
+ rotation: -90
296
+ }
297
+ case 'bottom':
298
+ return {
299
+ x: Number(pin.x) + 4,
300
+ yOffset: -4,
301
+ anchor: 'start',
302
+ rotation: -90
303
+ }
304
+ default:
305
+ return null
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Returns the horizontal pin-number clearance needed by the pin geometry.
311
+ * @param {'single-in' | 'single-out' | 'double' | null} markerStyle
312
+ * @param {{ length?: number }} pin
313
+ * @returns {number}
314
+ */
315
+ static #resolveHorizontalPinNumberClearance(markerStyle, pin) {
316
+ switch (markerStyle) {
317
+ case 'double':
318
+ return 17
319
+ case 'single-in':
320
+ case 'single-out':
321
+ return 8
322
+ default:
323
+ return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 2)
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Returns the horizontal pin-name inset used inside the symbol body.
329
+ * @param {{ length?: number }} pin
330
+ * @param {'hidden' | 'number-only' | 'name-only' | 'name-and-number'} labelMode
331
+ * @returns {number}
332
+ */
333
+ static #resolveHorizontalPinNameInset(pin, labelMode) {
334
+ if (labelMode === 'name-only') {
335
+ return 10
336
+ }
337
+
338
+ return SchematicOwnerPinLabelLayout.#resolveLongPinInset(pin, 4)
339
+ }
340
+
341
+ /**
342
+ * Adds extra text clearance for long connector-style pin stubs.
343
+ * @param {{ length?: number }} pin
344
+ * @param {number} fallback
345
+ * @returns {number}
346
+ */
347
+ static #resolveLongPinInset(pin, fallback) {
348
+ const length = Math.abs(Number(pin?.length || 0))
349
+
350
+ if (length < 30) {
351
+ return fallback
352
+ }
353
+
354
+ return fallback === 2 ? 10 : 8
355
+ }
356
+
357
+ /**
358
+ * Resolves one authored outer pin marker style from the stored symbol flag.
359
+ * @param {{ symbolOuter?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
360
+ * @returns {'single-in' | 'single-out' | 'double' | null}
361
+ */
362
+ static #resolveOuterPinMarkerStyle(pin) {
363
+ if (pin.orientation !== 'left' && pin.orientation !== 'right') {
364
+ return null
365
+ }
366
+
367
+ switch (Number(pin.symbolOuter || 0)) {
368
+ case 1:
369
+ case 33:
370
+ return 'single-out'
371
+ case 2:
372
+ return 'single-in'
373
+ case 34:
374
+ return 'double'
375
+ default:
376
+ return null
377
+ }
378
+ }
173
379
  }