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.
- package/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1252 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- 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(
|
|
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(
|
|
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
|
}
|