altium-toolkit 0.1.0
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +20 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +22 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/GPL-3.0-or-later.txt +232 -0
- package/NOTICE.md +32 -0
- package/README.md +116 -0
- package/docs/api.md +73 -0
- package/docs/model-format.md +36 -0
- package/docs/testing.md +25 -0
- package/examples/README.md +47 -0
- package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
- package/examples/arduino-uno/SvgViewportController.mjs +306 -0
- package/examples/arduino-uno/example.mjs +480 -0
- package/examples/arduino-uno/index.html +163 -0
- package/examples/arduino-uno/styles.css +552 -0
- package/examples/server.mjs +212 -0
- package/package.json +53 -0
- package/spec/library-scope.md +32 -0
- package/src/core/BinaryReader.mjs +127 -0
- package/src/core/altium/AltiumLayoutParser.mjs +485 -0
- package/src/core/altium/AltiumParser.mjs +1007 -0
- package/src/core/altium/AsciiRecordParser.mjs +151 -0
- package/src/core/altium/ParserUtils.mjs +173 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
- package/src/core/altium/PcbModelParser.mjs +336 -0
- package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
- package/src/core/altium/PcbStreamExtractor.mjs +210 -0
- package/src/core/altium/PrintableTextDecoder.mjs +156 -0
- package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
- package/src/core/altium/SchematicImageParser.mjs +173 -0
- package/src/core/altium/SchematicJunctionParser.mjs +43 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
- package/src/core/altium/SchematicPinParser.mjs +767 -0
- package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
- package/src/core/altium/SchematicSheetParser.mjs +241 -0
- package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
- package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
- package/src/core/altium/SchematicTextParser.mjs +708 -0
- package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
- package/src/core/ole/OleCompoundDocument.mjs +439 -0
- package/src/core/ole/OleConstants.mjs +64 -0
- package/src/core/ole/OleDirectoryEntry.mjs +95 -0
- package/src/index.mjs +7 -0
- package/src/parser.mjs +21 -0
- package/src/renderers.mjs +15 -0
- package/src/scene3d.mjs +9 -0
- package/src/styles/altium-renderers.css +358 -0
- package/src/ui/BomTableRenderer.mjs +46 -0
- package/src/ui/PcbArcUtils.mjs +189 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
- package/src/ui/PcbScene3dBuilder.mjs +742 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
- package/src/ui/PcbScene3dPackages.mjs +137 -0
- package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
- package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
- package/src/ui/PcbSvgRenderer.mjs +906 -0
- package/src/ui/SchematicColorResolver.mjs +132 -0
- package/src/ui/SchematicContentLayout.mjs +661 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
- package/src/ui/SchematicImageRenderer.mjs +135 -0
- package/src/ui/SchematicJunctionRenderer.mjs +381 -0
- package/src/ui/SchematicNoteRenderer.mjs +427 -0
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
- package/src/ui/SchematicPortRenderer.mjs +558 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
- package/src/ui/SchematicRegionRenderer.mjs +94 -0
- package/src/ui/SchematicShapeRenderer.mjs +398 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
- package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
- package/src/ui/SchematicSvgRenderer.mjs +756 -0
- package/src/ui/SchematicSvgUtils.mjs +182 -0
- package/src/ui/SchematicTypography.mjs +204 -0
- package/src/workers/altium-parser.worker.mjs +29 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Indexes session companion assets for 3D model lookup.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dModelRegistry {
|
|
9
|
+
/** @type {{ file?: File | Blob | null, name: string, relativePath: string, format: string, normalizedPath: string, normalizedBaseName: string }[]} */
|
|
10
|
+
#modelFiles
|
|
11
|
+
|
|
12
|
+
/** @type {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string }[]} */
|
|
13
|
+
#embeddedModels
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ file?: File | Blob | null, name: string, relativePath: string, format: string, normalizedPath: string, normalizedBaseName: string }[]} modelFiles
|
|
17
|
+
* @param {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string }[]} embeddedModels
|
|
18
|
+
*/
|
|
19
|
+
constructor(modelFiles, embeddedModels) {
|
|
20
|
+
this.#modelFiles = modelFiles
|
|
21
|
+
this.#embeddedModels = embeddedModels
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates one model registry from session files.
|
|
26
|
+
* @param {{ name?: string, relativePath?: string }[]} sessionFiles
|
|
27
|
+
* @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }[]} [embeddedModels]
|
|
28
|
+
* @returns {PcbScene3dModelRegistry}
|
|
29
|
+
*/
|
|
30
|
+
static create(sessionFiles, embeddedModels = []) {
|
|
31
|
+
const modelFiles = (Array.isArray(sessionFiles) ? sessionFiles : [])
|
|
32
|
+
.map((file) => PcbScene3dModelRegistry.#normalizeFile(file))
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
const normalizedEmbeddedModels = (
|
|
35
|
+
Array.isArray(embeddedModels) ? embeddedModels : []
|
|
36
|
+
)
|
|
37
|
+
.map((model) =>
|
|
38
|
+
PcbScene3dModelRegistry.#normalizeEmbeddedModel(model)
|
|
39
|
+
)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
|
|
42
|
+
return new PcbScene3dModelRegistry(modelFiles, normalizedEmbeddedModels)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the best available external model for one component.
|
|
47
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string }} component
|
|
48
|
+
* @returns {{ file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
|
|
49
|
+
*/
|
|
50
|
+
resolveComponentModel(component) {
|
|
51
|
+
const explicitMatch = this.#resolveExplicitMatch(component.modelPath)
|
|
52
|
+
if (explicitMatch) {
|
|
53
|
+
return explicitMatch
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const heuristicMatch = this.#resolveHeuristicMatch(component)
|
|
57
|
+
if (heuristicMatch) {
|
|
58
|
+
return heuristicMatch
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes one session file into registry metadata.
|
|
66
|
+
* @param {{ name?: string, relativePath?: string }} file
|
|
67
|
+
* @returns {{ file?: File | Blob | null, name: string, relativePath: string, format: string, normalizedPath: string, normalizedBaseName: string } | null}
|
|
68
|
+
*/
|
|
69
|
+
static #normalizeFile(file) {
|
|
70
|
+
const relativePath = String(file?.relativePath || file?.name || '')
|
|
71
|
+
const name = String(file?.name || relativePath.split('/').pop() || '')
|
|
72
|
+
const format = PcbScene3dModelRegistry.#resolveFormat(relativePath)
|
|
73
|
+
|
|
74
|
+
if (!name || !format) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
file: file?.file || file?.blob || null,
|
|
80
|
+
name,
|
|
81
|
+
relativePath,
|
|
82
|
+
format,
|
|
83
|
+
normalizedPath:
|
|
84
|
+
PcbScene3dModelRegistry.#normalizeToken(relativePath),
|
|
85
|
+
normalizedBaseName: PcbScene3dModelRegistry.#normalizeToken(
|
|
86
|
+
name.replace(/\.[^.]+$/, '')
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolves the supported model format from one file path.
|
|
93
|
+
* @param {string} filePath
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
static #resolveFormat(filePath) {
|
|
97
|
+
const lowerCasePath = String(filePath || '').toLowerCase()
|
|
98
|
+
if (lowerCasePath.endsWith('.wrl') || lowerCasePath.endsWith('.vrml')) {
|
|
99
|
+
return 'wrl'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (lowerCasePath.endsWith('.step') || lowerCasePath.endsWith('.stp')) {
|
|
103
|
+
return 'step'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ''
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolves one explicit model-path match.
|
|
111
|
+
* @param {string | undefined} modelPath
|
|
112
|
+
* @returns {{ file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
|
|
113
|
+
*/
|
|
114
|
+
#resolveExplicitMatch(modelPath) {
|
|
115
|
+
const normalizedPath =
|
|
116
|
+
PcbScene3dModelRegistry.#normalizeToken(modelPath)
|
|
117
|
+
if (!normalizedPath) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const byPath = this.#modelFiles.find(
|
|
122
|
+
(file) => file.normalizedPath === normalizedPath
|
|
123
|
+
)
|
|
124
|
+
if (byPath) {
|
|
125
|
+
return {
|
|
126
|
+
origin: 'session',
|
|
127
|
+
file: byPath.file,
|
|
128
|
+
name: byPath.name,
|
|
129
|
+
relativePath: byPath.relativePath,
|
|
130
|
+
format: byPath.format
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fileName =
|
|
135
|
+
String(modelPath || '')
|
|
136
|
+
.split('/')
|
|
137
|
+
.pop() || ''
|
|
138
|
+
const normalizedBaseName = PcbScene3dModelRegistry.#normalizeToken(
|
|
139
|
+
fileName.replace(/\.[^.]+$/, '')
|
|
140
|
+
)
|
|
141
|
+
return this.#resolveByBaseName(normalizedBaseName)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves a heuristic model match from component naming fields.
|
|
146
|
+
* @param {{ pattern?: string, source?: string }} component
|
|
147
|
+
* @returns {{ file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
|
|
148
|
+
*/
|
|
149
|
+
#resolveHeuristicMatch(component) {
|
|
150
|
+
const candidates = [component?.pattern, component?.source]
|
|
151
|
+
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
const normalized = PcbScene3dModelRegistry.#normalizeToken(
|
|
154
|
+
String(candidate || '')
|
|
155
|
+
)
|
|
156
|
+
const match = this.#resolveByBaseName(normalized)
|
|
157
|
+
if (match) {
|
|
158
|
+
return match
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolves one indexed file by normalized basename and format priority.
|
|
167
|
+
* @param {string} normalizedBaseName
|
|
168
|
+
* @returns {{ file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
|
|
169
|
+
*/
|
|
170
|
+
#resolveByBaseName(normalizedBaseName) {
|
|
171
|
+
if (!normalizedBaseName) {
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rankedMatches = this.#modelFiles
|
|
176
|
+
.filter((file) => file.normalizedBaseName === normalizedBaseName)
|
|
177
|
+
.sort(
|
|
178
|
+
(left, right) =>
|
|
179
|
+
PcbScene3dModelRegistry.#formatRank(left.format) -
|
|
180
|
+
PcbScene3dModelRegistry.#formatRank(right.format)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if (!rankedMatches.length) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
origin: 'session',
|
|
189
|
+
file: rankedMatches[0].file,
|
|
190
|
+
name: rankedMatches[0].name,
|
|
191
|
+
relativePath: rankedMatches[0].relativePath,
|
|
192
|
+
format: rankedMatches[0].format
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves the best available model for one normalized component-body
|
|
198
|
+
* placement.
|
|
199
|
+
* @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody
|
|
200
|
+
* @returns {{ origin: 'embedded' | 'session', file?: File | Blob | null, name: string, relativePath?: string, format: string, payloadText?: string, sourceStream?: string } | null}
|
|
201
|
+
*/
|
|
202
|
+
resolveComponentBodyModel(componentBody) {
|
|
203
|
+
const embeddedMatch = this.#resolveEmbeddedMatch(componentBody)
|
|
204
|
+
if (embeddedMatch) {
|
|
205
|
+
return embeddedMatch
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return this.#resolveExplicitMatch(componentBody?.name)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Normalizes one embedded payload for registry lookup.
|
|
213
|
+
* @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }} model
|
|
214
|
+
* @returns {{ id: string, checksum: number | null, name: string, format: string, payloadText: string, sourceStream: string, normalizedId: string, normalizedBaseName: string } | null}
|
|
215
|
+
*/
|
|
216
|
+
static #normalizeEmbeddedModel(model) {
|
|
217
|
+
const id = String(model?.id || '').trim()
|
|
218
|
+
const name = String(model?.name || '').trim()
|
|
219
|
+
const format = String(model?.format || '').trim()
|
|
220
|
+
const payloadText = String(model?.payloadText || '')
|
|
221
|
+
const sourceStream = String(model?.sourceStream || '').trim()
|
|
222
|
+
|
|
223
|
+
if (!id || !name || !format || !payloadText || !sourceStream) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
checksum: Number.isFinite(Number(model?.checksum))
|
|
230
|
+
? Number(model?.checksum)
|
|
231
|
+
: null,
|
|
232
|
+
name,
|
|
233
|
+
format,
|
|
234
|
+
payloadText,
|
|
235
|
+
sourceStream,
|
|
236
|
+
normalizedId: PcbScene3dModelRegistry.#normalizeToken(id),
|
|
237
|
+
normalizedBaseName: PcbScene3dModelRegistry.#normalizeToken(
|
|
238
|
+
name.replace(/\.[^.]+$/, '')
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolves one embedded model match from authored model metadata.
|
|
245
|
+
* @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody
|
|
246
|
+
* @returns {{ origin: 'embedded', name: string, format: string, payloadText: string, sourceStream: string } | null}
|
|
247
|
+
*/
|
|
248
|
+
#resolveEmbeddedMatch(componentBody) {
|
|
249
|
+
const normalizedId = PcbScene3dModelRegistry.#normalizeToken(
|
|
250
|
+
componentBody?.modelId
|
|
251
|
+
)
|
|
252
|
+
const checksum = Number.isFinite(Number(componentBody?.checksum))
|
|
253
|
+
? Number(componentBody?.checksum)
|
|
254
|
+
: null
|
|
255
|
+
const normalizedBaseName = PcbScene3dModelRegistry.#normalizeToken(
|
|
256
|
+
String(componentBody?.name || '').replace(/\.[^.]+$/, '')
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const embeddedMatch =
|
|
260
|
+
this.#embeddedModels.find(
|
|
261
|
+
(model) => normalizedId && model.normalizedId === normalizedId
|
|
262
|
+
) ||
|
|
263
|
+
this.#embeddedModels.find(
|
|
264
|
+
(model) =>
|
|
265
|
+
checksum !== null &&
|
|
266
|
+
model.checksum === checksum &&
|
|
267
|
+
normalizedBaseName &&
|
|
268
|
+
model.normalizedBaseName === normalizedBaseName
|
|
269
|
+
) ||
|
|
270
|
+
this.#embeddedModels.find(
|
|
271
|
+
(model) =>
|
|
272
|
+
normalizedBaseName &&
|
|
273
|
+
model.normalizedBaseName === normalizedBaseName
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if (!embeddedMatch) {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
origin: 'embedded',
|
|
282
|
+
name: embeddedMatch.name,
|
|
283
|
+
format: embeddedMatch.format,
|
|
284
|
+
payloadText: embeddedMatch.payloadText,
|
|
285
|
+
sourceStream: embeddedMatch.sourceStream
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Resolves the format priority for ties.
|
|
291
|
+
* @param {string} format
|
|
292
|
+
* @returns {number}
|
|
293
|
+
*/
|
|
294
|
+
static #formatRank(format) {
|
|
295
|
+
return format === 'wrl' ? 0 : 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Normalizes one lookup token.
|
|
300
|
+
* @param {string | undefined} value
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
static #normalizeToken(value) {
|
|
304
|
+
return String(value || '')
|
|
305
|
+
.toLowerCase()
|
|
306
|
+
.replaceAll('\\', '/')
|
|
307
|
+
.replace(/[^a-z0-9/]+/g, '')
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves procedural PCB package families and dimensions.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dPackages {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves one procedural package description for a component.
|
|
11
|
+
* @param {{ pattern?: string, height?: number | null }} component
|
|
12
|
+
* @param {{ width: number, depth: number }} [padSpan]
|
|
13
|
+
* @returns {{ family: string, sizeMil: { width: number, depth: number, height: number } }}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(component, padSpan = { width: 0, depth: 0 }) {
|
|
16
|
+
const family = PcbScene3dPackages.#resolveFamily(component.pattern)
|
|
17
|
+
const defaults = PcbScene3dPackages.#resolveDefaultSize(
|
|
18
|
+
family,
|
|
19
|
+
component.pattern
|
|
20
|
+
)
|
|
21
|
+
const explicitHeight = Number(component.height)
|
|
22
|
+
const height =
|
|
23
|
+
Number.isFinite(explicitHeight) && explicitHeight > 0
|
|
24
|
+
? explicitHeight
|
|
25
|
+
: defaults.height
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
family,
|
|
29
|
+
sizeMil: {
|
|
30
|
+
width: Math.max(defaults.width, Number(padSpan.width) || 0),
|
|
31
|
+
depth: Math.max(defaults.depth, Number(padSpan.depth) || 0),
|
|
32
|
+
height
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves a generic package family from one footprint pattern.
|
|
39
|
+
* @param {string | undefined} pattern
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
static #resolveFamily(pattern) {
|
|
43
|
+
const normalized = String(pattern || '').toUpperCase()
|
|
44
|
+
|
|
45
|
+
if (/(0402|0603|0805|1206|C0805|C0603)/.test(normalized)) {
|
|
46
|
+
return 'chip'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (normalized.includes('SOT')) {
|
|
50
|
+
return 'sot'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
normalized.includes('QFN') ||
|
|
55
|
+
normalized.includes('QFP') ||
|
|
56
|
+
normalized.includes('DFN') ||
|
|
57
|
+
normalized.includes('SOIC') ||
|
|
58
|
+
normalized.includes('SO16') ||
|
|
59
|
+
normalized.includes('TSSOP') ||
|
|
60
|
+
normalized.includes('SSOP')
|
|
61
|
+
) {
|
|
62
|
+
return 'ic'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (/C\d+(?:\.\d+)?A/.test(normalized)) {
|
|
66
|
+
return 'radial-capacitor'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (normalized.includes('TP')) {
|
|
70
|
+
return 'test-point'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
normalized.includes('CON/') ||
|
|
75
|
+
normalized.includes('PH') ||
|
|
76
|
+
normalized.includes('CK-')
|
|
77
|
+
) {
|
|
78
|
+
return 'connector-block'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (normalized.includes('SMA') || normalized.includes('SMB')) {
|
|
82
|
+
return 'diode'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 'generic'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolves one default body size for the chosen family.
|
|
90
|
+
* @param {string} family
|
|
91
|
+
* @param {string | undefined} pattern
|
|
92
|
+
* @returns {{ width: number, depth: number, height: number }}
|
|
93
|
+
*/
|
|
94
|
+
static #resolveDefaultSize(family, pattern) {
|
|
95
|
+
const normalized = String(pattern || '').toUpperCase()
|
|
96
|
+
|
|
97
|
+
if (family === 'chip') {
|
|
98
|
+
if (normalized.includes('0402')) {
|
|
99
|
+
return { width: 24, depth: 12, height: 14 }
|
|
100
|
+
}
|
|
101
|
+
if (normalized.includes('0805')) {
|
|
102
|
+
return { width: 80, depth: 50, height: 24 }
|
|
103
|
+
}
|
|
104
|
+
if (normalized.includes('1206')) {
|
|
105
|
+
return { width: 126, depth: 63, height: 28 }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { width: 60, depth: 30, height: 20 }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (family === 'sot') {
|
|
112
|
+
return { width: 110, depth: 90, height: 45 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (family === 'ic') {
|
|
116
|
+
return { width: 180, depth: 180, height: 55 }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (family === 'radial-capacitor') {
|
|
120
|
+
return { width: 120, depth: 120, height: 180 }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (family === 'test-point') {
|
|
124
|
+
return { width: 36, depth: 36, height: 60 }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (family === 'connector-block') {
|
|
128
|
+
return { width: 320, depth: 120, height: 150 }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (family === 'diode') {
|
|
132
|
+
return { width: 95, depth: 60, height: 34 }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { width: 96, depth: 72, height: 48 }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbScene3dBuilder } from './PcbScene3dBuilder.mjs'
|
|
6
|
+
import { PcbScene3dModelRegistry } from './PcbScene3dModelRegistry.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds renderer-ready 3D scene descriptions for the initial viewport mount.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbScene3dScenePreparator {
|
|
12
|
+
/**
|
|
13
|
+
* Builds one scene description for the initial runtime mount.
|
|
14
|
+
* @param {any} documentModel
|
|
15
|
+
* @param {{ sessionAssets?: any[], modelRegistry?: PcbScene3dModelRegistry | null, buildScene?: (documentModel: any, options: { modelRegistry: PcbScene3dModelRegistry }) => any }} [options]
|
|
16
|
+
* @returns {Promise<any>}
|
|
17
|
+
*/
|
|
18
|
+
static async prepare(documentModel, options = {}) {
|
|
19
|
+
const modelRegistry =
|
|
20
|
+
options.modelRegistry ||
|
|
21
|
+
PcbScene3dModelRegistry.create(
|
|
22
|
+
options.sessionAssets || [],
|
|
23
|
+
Array.isArray(documentModel?.pcb?.embeddedModels)
|
|
24
|
+
? documentModel.pcb.embeddedModels
|
|
25
|
+
: []
|
|
26
|
+
)
|
|
27
|
+
const buildScene =
|
|
28
|
+
options.buildScene ||
|
|
29
|
+
((nextDocumentModel, buildOptions) =>
|
|
30
|
+
PcbScene3dBuilder.build(nextDocumentModel, buildOptions))
|
|
31
|
+
|
|
32
|
+
return buildScene(documentModel, {
|
|
33
|
+
modelRegistry
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders a non-interactive summary of recovered PCB 3D scene inputs.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dSummaryRenderer {
|
|
9
|
+
/**
|
|
10
|
+
* Renders static 3D summary markup for a parsed PCB document.
|
|
11
|
+
* @param {{ pcb?: { boardOutline?: { widthMil?: number, heightMil?: number }, components?: any[] }, bom?: any[] }} documentModel
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
static render(documentModel) {
|
|
15
|
+
const pcb = documentModel?.pcb
|
|
16
|
+
if (!pcb) {
|
|
17
|
+
return '<section class="altium-3d-summary altium-3d-summary--empty">3D summary is available after parsing a PCB document.</section>'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const widthMil = PcbScene3dSummaryRenderer.#roundMil(
|
|
21
|
+
pcb.boardOutline?.widthMil
|
|
22
|
+
)
|
|
23
|
+
const heightMil = PcbScene3dSummaryRenderer.#roundMil(
|
|
24
|
+
pcb.boardOutline?.heightMil
|
|
25
|
+
)
|
|
26
|
+
const componentCount = Array.isArray(pcb.components)
|
|
27
|
+
? pcb.components.length
|
|
28
|
+
: 0
|
|
29
|
+
const bomRows = Array.isArray(documentModel?.bom)
|
|
30
|
+
? documentModel.bom.length
|
|
31
|
+
: 0
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
'<section class="altium-3d-summary">' +
|
|
35
|
+
'<header class="altium-3d-summary__header"><h3>3D summary</h3><p>' +
|
|
36
|
+
widthMil +
|
|
37
|
+
' x ' +
|
|
38
|
+
heightMil +
|
|
39
|
+
' mil board envelope</p></header>' +
|
|
40
|
+
'<dl class="altium-3d-summary__stats"><div><dt>Footprint</dt><dd>' +
|
|
41
|
+
widthMil +
|
|
42
|
+
' x ' +
|
|
43
|
+
heightMil +
|
|
44
|
+
' mil</dd></div><div><dt>Placements</dt><dd>' +
|
|
45
|
+
componentCount +
|
|
46
|
+
' components</dd></div><div><dt>BOM groups</dt><dd>' +
|
|
47
|
+
bomRows +
|
|
48
|
+
'</dd></div></dl></section>'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Rounds a recovered mil dimension for display.
|
|
54
|
+
* @param {unknown} value
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
static #roundMil(value) {
|
|
58
|
+
const numericValue = Number(value)
|
|
59
|
+
if (!Number.isFinite(numericValue)) {
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Math.round(numericValue)
|
|
64
|
+
}
|
|
65
|
+
}
|