altium-toolkit 1.0.8 → 1.0.9
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/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -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 +191 -45
- 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/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 +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- 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 +532 -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 +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -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/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -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 +1187 -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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders PCB barcode text primitives as deterministic SVG bar groups.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbBarcodeTextRenderer {
|
|
11
|
+
static #DEFAULT_MODULE_WIDTH = 2
|
|
12
|
+
static #DEFAULT_HEIGHT_RATIO = 1.8
|
|
13
|
+
static #CAPTION_RATIO = 0.34
|
|
14
|
+
|
|
15
|
+
static #CODE39_PATTERNS = new Map([
|
|
16
|
+
['0', '101001101101'],
|
|
17
|
+
['1', '110100101011'],
|
|
18
|
+
['2', '101100101011'],
|
|
19
|
+
['3', '110110010101'],
|
|
20
|
+
['4', '101001101011'],
|
|
21
|
+
['5', '110100110101'],
|
|
22
|
+
['6', '101100110101'],
|
|
23
|
+
['7', '101001011011'],
|
|
24
|
+
['8', '110100101101'],
|
|
25
|
+
['9', '101100101101'],
|
|
26
|
+
['A', '110101001011'],
|
|
27
|
+
['B', '101101001011'],
|
|
28
|
+
['C', '110110100101'],
|
|
29
|
+
['D', '101011001011'],
|
|
30
|
+
['E', '110101100101'],
|
|
31
|
+
['F', '101101100101'],
|
|
32
|
+
['G', '101010011011'],
|
|
33
|
+
['H', '110101001101'],
|
|
34
|
+
['I', '101101001101'],
|
|
35
|
+
['J', '101011001101'],
|
|
36
|
+
['K', '110101010011'],
|
|
37
|
+
['L', '101101010011'],
|
|
38
|
+
['M', '110110101001'],
|
|
39
|
+
['N', '101011010011'],
|
|
40
|
+
['O', '110101101001'],
|
|
41
|
+
['P', '101101101001'],
|
|
42
|
+
['Q', '101010110011'],
|
|
43
|
+
['R', '110101011001'],
|
|
44
|
+
['S', '101101011001'],
|
|
45
|
+
['T', '101011011001'],
|
|
46
|
+
['U', '110010101011'],
|
|
47
|
+
['V', '100110101011'],
|
|
48
|
+
['W', '110011010101'],
|
|
49
|
+
['X', '100101101011'],
|
|
50
|
+
['Y', '110010110101'],
|
|
51
|
+
['Z', '100110110101'],
|
|
52
|
+
['-', '100101011011'],
|
|
53
|
+
['.', '110010101101'],
|
|
54
|
+
[' ', '100110101101'],
|
|
55
|
+
['$', '100100100101'],
|
|
56
|
+
['/', '100100101001'],
|
|
57
|
+
['+', '100101001001'],
|
|
58
|
+
['%', '101001001001'],
|
|
59
|
+
['*', '100101101101']
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
static #CODE128_PATTERNS = [
|
|
63
|
+
'11011001100',
|
|
64
|
+
'11001101100',
|
|
65
|
+
'11001100110',
|
|
66
|
+
'10010011000',
|
|
67
|
+
'10010001100',
|
|
68
|
+
'10001001100',
|
|
69
|
+
'10011001000',
|
|
70
|
+
'10011000100',
|
|
71
|
+
'10001100100',
|
|
72
|
+
'11001001000',
|
|
73
|
+
'11001000100',
|
|
74
|
+
'11000100100',
|
|
75
|
+
'10110011100',
|
|
76
|
+
'10011011100',
|
|
77
|
+
'10011001110',
|
|
78
|
+
'10111001100',
|
|
79
|
+
'10011101100',
|
|
80
|
+
'10011100110',
|
|
81
|
+
'11001110010',
|
|
82
|
+
'11001011100',
|
|
83
|
+
'11001001110',
|
|
84
|
+
'11011100100',
|
|
85
|
+
'11001110100',
|
|
86
|
+
'11101101110',
|
|
87
|
+
'11101001100',
|
|
88
|
+
'11100101100',
|
|
89
|
+
'11100100110',
|
|
90
|
+
'11101100100',
|
|
91
|
+
'11100110100',
|
|
92
|
+
'11100110010',
|
|
93
|
+
'11011011000',
|
|
94
|
+
'11011000110',
|
|
95
|
+
'11000110110',
|
|
96
|
+
'10100011000',
|
|
97
|
+
'10001011000',
|
|
98
|
+
'10001000110',
|
|
99
|
+
'10110001000',
|
|
100
|
+
'10001101000',
|
|
101
|
+
'10001100010',
|
|
102
|
+
'11010001000',
|
|
103
|
+
'11000101000',
|
|
104
|
+
'11000100010',
|
|
105
|
+
'10110111000',
|
|
106
|
+
'10110001110',
|
|
107
|
+
'10001101110',
|
|
108
|
+
'10111011000',
|
|
109
|
+
'10111000110',
|
|
110
|
+
'10001110110',
|
|
111
|
+
'11101110110',
|
|
112
|
+
'11010001110',
|
|
113
|
+
'11000101110',
|
|
114
|
+
'11011101000',
|
|
115
|
+
'11011100010',
|
|
116
|
+
'11011101110',
|
|
117
|
+
'11101011000',
|
|
118
|
+
'11101000110',
|
|
119
|
+
'11100010110',
|
|
120
|
+
'11101101000',
|
|
121
|
+
'11101100010',
|
|
122
|
+
'11100011010',
|
|
123
|
+
'11101111010',
|
|
124
|
+
'11001000010',
|
|
125
|
+
'11110001010',
|
|
126
|
+
'10100110000',
|
|
127
|
+
'10100001100',
|
|
128
|
+
'10010110000',
|
|
129
|
+
'10010000110',
|
|
130
|
+
'10000101100',
|
|
131
|
+
'10000100110',
|
|
132
|
+
'10110010000',
|
|
133
|
+
'10110000100',
|
|
134
|
+
'10011010000',
|
|
135
|
+
'10011000010',
|
|
136
|
+
'10000110100',
|
|
137
|
+
'10000110010',
|
|
138
|
+
'11000010010',
|
|
139
|
+
'11001010000',
|
|
140
|
+
'11110111010',
|
|
141
|
+
'11000010100',
|
|
142
|
+
'10001111010',
|
|
143
|
+
'10100111100',
|
|
144
|
+
'10010111100',
|
|
145
|
+
'10010011110',
|
|
146
|
+
'10111100100',
|
|
147
|
+
'10011110100',
|
|
148
|
+
'10011110010',
|
|
149
|
+
'11110100100',
|
|
150
|
+
'11110010100',
|
|
151
|
+
'11110010010',
|
|
152
|
+
'11011011110',
|
|
153
|
+
'11011110110',
|
|
154
|
+
'11110110110',
|
|
155
|
+
'10101111000',
|
|
156
|
+
'10100011110',
|
|
157
|
+
'10001011110',
|
|
158
|
+
'10111101000',
|
|
159
|
+
'10111100010',
|
|
160
|
+
'11110101000',
|
|
161
|
+
'11110100010',
|
|
162
|
+
'10111011110',
|
|
163
|
+
'10111101110',
|
|
164
|
+
'11101011110',
|
|
165
|
+
'11110101110',
|
|
166
|
+
'11010000100',
|
|
167
|
+
'11010010000',
|
|
168
|
+
'11010011100',
|
|
169
|
+
'1100011101011'
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Renders one barcode text primitive.
|
|
174
|
+
* @param {{ text: string, layerId?: number, barcode?: object }} text Text primitive.
|
|
175
|
+
* @param {{ transform: string, fontSize: number, semanticAttributes: string }} options Render options.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static render(text, options) {
|
|
179
|
+
const encoding = PcbBarcodeTextRenderer.#encoding(text)
|
|
180
|
+
const layout = PcbBarcodeTextRenderer.#layout(
|
|
181
|
+
text,
|
|
182
|
+
options.fontSize,
|
|
183
|
+
encoding.pattern.length
|
|
184
|
+
)
|
|
185
|
+
const bars = PcbBarcodeTextRenderer.#bars(encoding.pattern, layout)
|
|
186
|
+
const className =
|
|
187
|
+
'pcb-text pcb-text--layer-' +
|
|
188
|
+
SchematicSvgUtils.escapeHtml(String(Number(text.layerId || 0))) +
|
|
189
|
+
' pcb-text--barcode' +
|
|
190
|
+
(text?.barcode?.inverted ? ' pcb-text--barcode-inverted' : '')
|
|
191
|
+
const background = text?.barcode?.inverted
|
|
192
|
+
? '<rect class="pcb-barcode__background" x="0" y="0" width="' +
|
|
193
|
+
SchematicSvgUtils.formatNumber(layout.width) +
|
|
194
|
+
'" height="' +
|
|
195
|
+
SchematicSvgUtils.formatNumber(layout.height) +
|
|
196
|
+
'" />'
|
|
197
|
+
: ''
|
|
198
|
+
const caption = text?.barcode?.showText
|
|
199
|
+
? PcbBarcodeTextRenderer.#caption(text, layout)
|
|
200
|
+
: ''
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
'<g class="' +
|
|
204
|
+
className +
|
|
205
|
+
'" transform="' +
|
|
206
|
+
options.transform +
|
|
207
|
+
'"' +
|
|
208
|
+
options.semanticAttributes +
|
|
209
|
+
' data-barcode-symbology="' +
|
|
210
|
+
SchematicSvgUtils.escapeHtml(encoding.symbology) +
|
|
211
|
+
'" data-barcode-module-count="' +
|
|
212
|
+
SchematicSvgUtils.escapeHtml(String(encoding.pattern.length)) +
|
|
213
|
+
'"' +
|
|
214
|
+
'>' +
|
|
215
|
+
background +
|
|
216
|
+
'<g class="pcb-barcode__bars" transform="translate(' +
|
|
217
|
+
SchematicSvgUtils.formatNumber(layout.marginX) +
|
|
218
|
+
' ' +
|
|
219
|
+
SchematicSvgUtils.formatNumber(layout.marginY) +
|
|
220
|
+
')">' +
|
|
221
|
+
bars +
|
|
222
|
+
'</g>' +
|
|
223
|
+
caption +
|
|
224
|
+
'</g>'
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves barcode layout dimensions.
|
|
230
|
+
* @param {{ text: string, height?: number, barcode?: object }} text Text primitive.
|
|
231
|
+
* @param {number} fontSize Resolved text font size.
|
|
232
|
+
* @param {number} patternLength Encoded module count.
|
|
233
|
+
* @returns {{ width: number, height: number, barHeight: number, marginX: number, marginY: number, moduleWidth: number }}
|
|
234
|
+
*/
|
|
235
|
+
static #layout(text, fontSize, patternLength) {
|
|
236
|
+
const barcode = text?.barcode || {}
|
|
237
|
+
const moduleWidth = Math.max(
|
|
238
|
+
Number(barcode.minBarWidth) ||
|
|
239
|
+
PcbBarcodeTextRenderer.#DEFAULT_MODULE_WIDTH,
|
|
240
|
+
0.1
|
|
241
|
+
)
|
|
242
|
+
const contentWidth = patternLength * moduleWidth
|
|
243
|
+
const marginX = Math.max(Number(barcode.marginX) || moduleWidth, 0)
|
|
244
|
+
const marginY = Math.max(Number(barcode.marginY) || moduleWidth, 0)
|
|
245
|
+
const width = Math.max(
|
|
246
|
+
Number(barcode.fullWidth) || 0,
|
|
247
|
+
contentWidth + marginX * 2
|
|
248
|
+
)
|
|
249
|
+
const height = Math.max(
|
|
250
|
+
Number(barcode.fullHeight) || 0,
|
|
251
|
+
fontSize * PcbBarcodeTextRenderer.#DEFAULT_HEIGHT_RATIO
|
|
252
|
+
)
|
|
253
|
+
const captionHeight = barcode.showText
|
|
254
|
+
? fontSize * PcbBarcodeTextRenderer.#CAPTION_RATIO
|
|
255
|
+
: 0
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
width,
|
|
259
|
+
height,
|
|
260
|
+
barHeight: Math.max(height - marginY * 2 - captionHeight, 1),
|
|
261
|
+
marginX,
|
|
262
|
+
marginY,
|
|
263
|
+
moduleWidth:
|
|
264
|
+
contentWidth > 0
|
|
265
|
+
? Math.max((width - marginX * 2) / patternLength, 0.1)
|
|
266
|
+
: moduleWidth
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Renders barcode bars.
|
|
272
|
+
* @param {string} pattern Encoded barcode module pattern.
|
|
273
|
+
* @param {{ barHeight: number, moduleWidth: number }} layout Barcode layout.
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
static #bars(pattern, layout) {
|
|
277
|
+
const runs = []
|
|
278
|
+
let cursor = 0
|
|
279
|
+
|
|
280
|
+
while (cursor < pattern.length) {
|
|
281
|
+
const value = pattern[cursor]
|
|
282
|
+
let length = 1
|
|
283
|
+
while (pattern[cursor + length] === value) {
|
|
284
|
+
length += 1
|
|
285
|
+
}
|
|
286
|
+
if (value === '1') {
|
|
287
|
+
runs.push({ offset: cursor, length })
|
|
288
|
+
}
|
|
289
|
+
cursor += length
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return runs
|
|
293
|
+
.map(
|
|
294
|
+
(run) =>
|
|
295
|
+
'<rect class="pcb-barcode__bar" x="' +
|
|
296
|
+
SchematicSvgUtils.formatNumber(
|
|
297
|
+
run.offset * layout.moduleWidth
|
|
298
|
+
) +
|
|
299
|
+
'" y="0" width="' +
|
|
300
|
+
SchematicSvgUtils.formatNumber(
|
|
301
|
+
run.length * layout.moduleWidth
|
|
302
|
+
) +
|
|
303
|
+
'" height="' +
|
|
304
|
+
SchematicSvgUtils.formatNumber(layout.barHeight) +
|
|
305
|
+
'" />'
|
|
306
|
+
)
|
|
307
|
+
.join('')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Renders optional human-readable barcode text.
|
|
312
|
+
* @param {{ text: string }} text Text primitive.
|
|
313
|
+
* @param {{ width: number, height: number, marginY: number }} layout Barcode layout.
|
|
314
|
+
* @returns {string}
|
|
315
|
+
*/
|
|
316
|
+
static #caption(text, layout) {
|
|
317
|
+
const fontSize = Math.max(layout.height * 0.16, 4)
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
'<text class="pcb-barcode__caption" x="' +
|
|
321
|
+
SchematicSvgUtils.formatNumber(layout.width / 2) +
|
|
322
|
+
'" y="' +
|
|
323
|
+
SchematicSvgUtils.formatNumber(layout.height - layout.marginY) +
|
|
324
|
+
'" font-size="' +
|
|
325
|
+
SchematicSvgUtils.formatNumber(fontSize) +
|
|
326
|
+
'" text-anchor="middle" dominant-baseline="alphabetic">' +
|
|
327
|
+
SchematicSvgUtils.escapeHtml(String(text.text || '')) +
|
|
328
|
+
'</text>'
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Encodes one barcode payload.
|
|
334
|
+
* @param {{ text: string, barcode?: object }} text Text primitive.
|
|
335
|
+
* @returns {{ pattern: string, symbology: string }}
|
|
336
|
+
*/
|
|
337
|
+
static #encoding(text) {
|
|
338
|
+
const kind = String(text?.barcode?.kindName || '').toLowerCase()
|
|
339
|
+
if (kind === 'code39') {
|
|
340
|
+
return PcbBarcodeTextRenderer.#encodeCode39(text)
|
|
341
|
+
}
|
|
342
|
+
if (kind === 'code128') {
|
|
343
|
+
return PcbBarcodeTextRenderer.#encodeCode128B(text)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
pattern: PcbBarcodeTextRenderer.#fallbackPattern(text),
|
|
348
|
+
symbology: 'deterministic'
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Encodes valid Code 39 content.
|
|
354
|
+
* @param {{ text: string }} text Text primitive.
|
|
355
|
+
* @returns {{ pattern: string, symbology: string }}
|
|
356
|
+
*/
|
|
357
|
+
static #encodeCode39(text) {
|
|
358
|
+
const content = String(text?.text || '').toUpperCase()
|
|
359
|
+
const encoded = '*' + content + '*'
|
|
360
|
+
const parts = []
|
|
361
|
+
|
|
362
|
+
for (const character of encoded) {
|
|
363
|
+
const pattern =
|
|
364
|
+
PcbBarcodeTextRenderer.#CODE39_PATTERNS.get(character)
|
|
365
|
+
if (!pattern) {
|
|
366
|
+
return {
|
|
367
|
+
pattern: PcbBarcodeTextRenderer.#fallbackPattern(text),
|
|
368
|
+
symbology: 'deterministic'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
parts.push(pattern)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
pattern: parts.join('0'),
|
|
376
|
+
symbology: 'Code 39'
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Encodes printable ASCII content as Code 128 set B.
|
|
382
|
+
* @param {{ text: string }} text Text primitive.
|
|
383
|
+
* @returns {{ pattern: string, symbology: string }}
|
|
384
|
+
*/
|
|
385
|
+
static #encodeCode128B(text) {
|
|
386
|
+
const values = [104]
|
|
387
|
+
for (const character of String(text?.text || '')) {
|
|
388
|
+
const codePoint = character.codePointAt(0) || 63
|
|
389
|
+
const value =
|
|
390
|
+
codePoint >= 32 && codePoint <= 127 ? codePoint - 32 : 31
|
|
391
|
+
values.push(value)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let checksum = values[0]
|
|
395
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
396
|
+
checksum += values[index] * index
|
|
397
|
+
}
|
|
398
|
+
values.push(checksum % 103)
|
|
399
|
+
values.push(106)
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
pattern: values
|
|
403
|
+
.map((value) => PcbBarcodeTextRenderer.#CODE128_PATTERNS[value])
|
|
404
|
+
.join(''),
|
|
405
|
+
symbology: 'Code 128B'
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Builds a deterministic fallback module pattern for unsupported content.
|
|
411
|
+
* @param {{ text: string }} text Text primitive.
|
|
412
|
+
* @returns {string}
|
|
413
|
+
*/
|
|
414
|
+
static #fallbackPattern(text) {
|
|
415
|
+
const value = String(text?.text || '')
|
|
416
|
+
const parts = ['11010010000']
|
|
417
|
+
for (const character of value) {
|
|
418
|
+
parts.push(
|
|
419
|
+
PcbBarcodeTextRenderer.#fallbackCharacterPattern(character)
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
parts.push('1100011101011')
|
|
423
|
+
return parts.join('0')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Builds a stable 11-module fallback pattern for one character.
|
|
428
|
+
* @param {string} character Barcode character.
|
|
429
|
+
* @returns {string}
|
|
430
|
+
*/
|
|
431
|
+
static #fallbackCharacterPattern(character) {
|
|
432
|
+
const code = character.codePointAt(0) || 0
|
|
433
|
+
const mixed = (code * 1103515245 + 12345) >>> 0
|
|
434
|
+
return mixed.toString(2).padStart(32, '0').slice(0, 11)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -469,12 +469,17 @@ export class PcbInteractionIndex {
|
|
|
469
469
|
const componentIndex = Number(primitive?.componentIndex)
|
|
470
470
|
if (!Number.isInteger(componentIndex)) return null
|
|
471
471
|
|
|
472
|
-
|
|
472
|
+
const explicitMatch =
|
|
473
473
|
context.components.find(
|
|
474
|
-
(component
|
|
475
|
-
Number(component?.componentIndex) === componentIndex
|
|
476
|
-
index === componentIndex
|
|
474
|
+
(component) =>
|
|
475
|
+
Number(component?.componentIndex) === componentIndex
|
|
477
476
|
) || null
|
|
477
|
+
if (explicitMatch) return explicitMatch
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
context.components.find((component, index) => {
|
|
481
|
+
return index === componentIndex
|
|
482
|
+
}) || null
|
|
478
483
|
)
|
|
479
484
|
}
|
|
480
485
|
|
|
@@ -18,7 +18,7 @@ export class PcbScene3dBuilder {
|
|
|
18
18
|
static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
|
|
19
19
|
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
20
20
|
static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
|
|
21
|
-
static #PRECISE_BODY_MATCH_TOLERANCE_MIL =
|
|
21
|
+
static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 20
|
|
22
22
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
23
23
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
24
24
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
@@ -27,8 +27,8 @@ export class PcbScene3dBuilder {
|
|
|
27
27
|
/**
|
|
28
28
|
* Builds a scene description for host 3D renderers.
|
|
29
29
|
* @param {{ pcb?: { boardOutline?: { widthMil?: number, heightMil?: number, minX?: number, minY?: number, segments?: Array<Record<string, number | string>> }, primitiveLayers?: { layerId: number, name: string }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[], tracks?: any[], arcs?: any[], fills?: any[], vias?: any[], polygons?: any[], embeddedModels?: any[], componentBodies?: { modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }[], components?: { designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }[] } }} documentModel
|
|
30
|
-
* @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
|
|
31
|
-
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
|
|
30
|
+
* @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null, resolveBoardAssemblyModel?: (documentModel: any) => { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
|
|
31
|
+
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, boardAssemblyModel: { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
|
|
32
32
|
*/
|
|
33
33
|
static build(documentModel, options = {}) {
|
|
34
34
|
const pcb = documentModel?.pcb || {}
|
|
@@ -63,6 +63,10 @@ export class PcbScene3dBuilder {
|
|
|
63
63
|
centerY:
|
|
64
64
|
Number(boardOutline.minY || 0) +
|
|
65
65
|
Number(boardOutline.heightMil || 0) / 2,
|
|
66
|
+
surfaceColor: Number.isInteger(appearance3d.solderMaskTopColor)
|
|
67
|
+
? appearance3d.solderMaskTopColor
|
|
68
|
+
: appearance3d.solderMaskBottomColor,
|
|
69
|
+
edgeColor: appearance3d.boardCoreColor,
|
|
66
70
|
segments: Array.isArray(boardOutline.segments)
|
|
67
71
|
? boardOutline.segments
|
|
68
72
|
: []
|
|
@@ -106,6 +110,9 @@ export class PcbScene3dBuilder {
|
|
|
106
110
|
const sceneDescription = {
|
|
107
111
|
sourceFormat: 'altium',
|
|
108
112
|
board,
|
|
113
|
+
boardAssemblyModel:
|
|
114
|
+
modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
|
|
115
|
+
null,
|
|
109
116
|
components: components.map((component) =>
|
|
110
117
|
PcbScene3dBuilder.#buildComponent(
|
|
111
118
|
component,
|
|
@@ -121,6 +128,7 @@ export class PcbScene3dBuilder {
|
|
|
121
128
|
componentBody,
|
|
122
129
|
bodyMatches[index],
|
|
123
130
|
components,
|
|
131
|
+
pads,
|
|
124
132
|
board,
|
|
125
133
|
thicknessMil,
|
|
126
134
|
modelRegistry
|
|
@@ -208,6 +216,7 @@ export class PcbScene3dBuilder {
|
|
|
208
216
|
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
|
|
209
217
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
|
|
210
218
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
|
|
219
|
+
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
211
220
|
* @param {{ centerX: number, centerY: number }} board
|
|
212
221
|
* @param {number} thicknessMil
|
|
213
222
|
* @param {{ resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null} modelRegistry
|
|
@@ -217,6 +226,7 @@ export class PcbScene3dBuilder {
|
|
|
217
226
|
componentBody,
|
|
218
227
|
matchedComponent,
|
|
219
228
|
components,
|
|
229
|
+
pads,
|
|
220
230
|
board,
|
|
221
231
|
thicknessMil,
|
|
222
232
|
modelRegistry
|
|
@@ -275,10 +285,134 @@ export class PcbScene3dBuilder {
|
|
|
275
285
|
rotationDeg: modelRotation,
|
|
276
286
|
dzMil: Number(componentBody.dzMil || 0)
|
|
277
287
|
},
|
|
288
|
+
projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
|
|
289
|
+
componentBody,
|
|
290
|
+
matchedComponent,
|
|
291
|
+
pads,
|
|
292
|
+
resolvedModel
|
|
293
|
+
),
|
|
278
294
|
externalModel: resolvedModel
|
|
279
295
|
}
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Explains which footprint projection source informed one external model.
|
|
300
|
+
* @param {object} componentBody Normalized component body row.
|
|
301
|
+
* @param {{ x: number, y: number, height?: number | null } | null} matchedComponent Matched component.
|
|
302
|
+
* @param {object[]} pads Normalized pad rows.
|
|
303
|
+
* @param {object | null} resolvedModel Resolved model metadata.
|
|
304
|
+
* @returns {{ source: string, reason: string, boundsMil: { width: number, depth: number, height: number } }}
|
|
305
|
+
*/
|
|
306
|
+
static #resolveProjectionDiagnostics(
|
|
307
|
+
componentBody,
|
|
308
|
+
matchedComponent,
|
|
309
|
+
pads,
|
|
310
|
+
resolvedModel
|
|
311
|
+
) {
|
|
312
|
+
const authoredBounds = PcbScene3dBuilder.#firstBounds([
|
|
313
|
+
componentBody?.projectionOverrideMil,
|
|
314
|
+
componentBody?.projectionOverride?.boundsMil,
|
|
315
|
+
componentBody?.projectionBoundsMil
|
|
316
|
+
])
|
|
317
|
+
if (authoredBounds) {
|
|
318
|
+
return {
|
|
319
|
+
source: 'authored-override',
|
|
320
|
+
reason: 'Component body carried an explicit projection override.',
|
|
321
|
+
boundsMil: authoredBounds
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const modelBounds = PcbScene3dBuilder.#firstBounds([
|
|
326
|
+
componentBody?.modelBoundsMil,
|
|
327
|
+
resolvedModel?.boundsMil
|
|
328
|
+
])
|
|
329
|
+
if (modelBounds) {
|
|
330
|
+
return {
|
|
331
|
+
source: 'model-bounds',
|
|
332
|
+
reason: 'Resolved 3D model bounds were available.',
|
|
333
|
+
boundsMil: modelBounds
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (matchedComponent) {
|
|
338
|
+
const padSpan = PcbScene3dBuilder.#resolvePadSpan(
|
|
339
|
+
matchedComponent,
|
|
340
|
+
pads
|
|
341
|
+
)
|
|
342
|
+
if (padSpan.width > 0 || padSpan.depth > 0) {
|
|
343
|
+
return {
|
|
344
|
+
source: 'pad-fallback',
|
|
345
|
+
reason: 'Projection fell back to nearby component pad span.',
|
|
346
|
+
boundsMil: {
|
|
347
|
+
width: padSpan.width,
|
|
348
|
+
depth: padSpan.depth,
|
|
349
|
+
height: Number(matchedComponent.height || 0)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const body = PcbScene3dPackages.resolve(matchedComponent, padSpan)
|
|
355
|
+
return {
|
|
356
|
+
source: 'component-fallback',
|
|
357
|
+
reason: 'Projection fell back to the procedural component body.',
|
|
358
|
+
boundsMil: {
|
|
359
|
+
width: body.sizeMil.width,
|
|
360
|
+
depth: body.sizeMil.depth,
|
|
361
|
+
height: body.sizeMil.height
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
source: 'model-anchor-fallback',
|
|
368
|
+
reason: 'Projection used the model anchor because no owner geometry was available.',
|
|
369
|
+
boundsMil: { width: 0, depth: 0, height: 0 }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Returns the first complete bounds object from candidate metadata.
|
|
375
|
+
* @param {unknown[]} candidates Candidate bounds records.
|
|
376
|
+
* @returns {{ width: number, depth: number, height: number } | null}
|
|
377
|
+
*/
|
|
378
|
+
static #firstBounds(candidates) {
|
|
379
|
+
for (const candidate of candidates || []) {
|
|
380
|
+
const bounds = PcbScene3dBuilder.#normalizeBounds(candidate)
|
|
381
|
+
if (bounds) {
|
|
382
|
+
return bounds
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Normalizes width/depth/height bounds metadata.
|
|
391
|
+
* @param {unknown} candidate Candidate bounds record.
|
|
392
|
+
* @returns {{ width: number, depth: number, height: number } | null}
|
|
393
|
+
*/
|
|
394
|
+
static #normalizeBounds(candidate) {
|
|
395
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const width = Number(candidate.width ?? candidate.x ?? candidate.sizeX)
|
|
400
|
+
const depth = Number(candidate.depth ?? candidate.y ?? candidate.sizeY)
|
|
401
|
+
const height = Number(
|
|
402
|
+
candidate.height ?? candidate.z ?? candidate.sizeZ
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
!Number.isFinite(width) ||
|
|
407
|
+
!Number.isFinite(depth) ||
|
|
408
|
+
!Number.isFinite(height)
|
|
409
|
+
) {
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { width, depth, height }
|
|
414
|
+
}
|
|
415
|
+
|
|
282
416
|
/**
|
|
283
417
|
* Resolves explicit body placements to component anchors using a unique
|
|
284
418
|
* nearest-neighbor pass plus an ordered-affinity fallback for repeated
|