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.
Files changed (82) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +20 -0
  3. package/CONTRIBUTING.md +19 -0
  4. package/LICENSE +22 -0
  5. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  6. package/LICENSES/GPL-3.0-or-later.txt +232 -0
  7. package/NOTICE.md +32 -0
  8. package/README.md +116 -0
  9. package/docs/api.md +73 -0
  10. package/docs/model-format.md +36 -0
  11. package/docs/testing.md +25 -0
  12. package/examples/README.md +47 -0
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +635 -0
  14. package/examples/arduino-uno/SvgViewportController.mjs +306 -0
  15. package/examples/arduino-uno/example.mjs +480 -0
  16. package/examples/arduino-uno/index.html +163 -0
  17. package/examples/arduino-uno/styles.css +552 -0
  18. package/examples/server.mjs +212 -0
  19. package/package.json +53 -0
  20. package/spec/library-scope.md +32 -0
  21. package/src/core/BinaryReader.mjs +127 -0
  22. package/src/core/altium/AltiumLayoutParser.mjs +485 -0
  23. package/src/core/altium/AltiumParser.mjs +1007 -0
  24. package/src/core/altium/AsciiRecordParser.mjs +151 -0
  25. package/src/core/altium/ParserUtils.mjs +173 -0
  26. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +424 -0
  27. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +505 -0
  28. package/src/core/altium/PcbModelParser.mjs +336 -0
  29. package/src/core/altium/PcbOutlineRasterizer.mjs +852 -0
  30. package/src/core/altium/PcbOutlineRecovery.mjs +957 -0
  31. package/src/core/altium/PcbStreamExtractor.mjs +210 -0
  32. package/src/core/altium/PrintableTextDecoder.mjs +156 -0
  33. package/src/core/altium/SchematicAnnotationParser.mjs +220 -0
  34. package/src/core/altium/SchematicBusEntryParser.mjs +48 -0
  35. package/src/core/altium/SchematicDirectiveParser.mjs +47 -0
  36. package/src/core/altium/SchematicImageParser.mjs +173 -0
  37. package/src/core/altium/SchematicJunctionParser.mjs +43 -0
  38. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +564 -0
  39. package/src/core/altium/SchematicNetlistBuilder.mjs +351 -0
  40. package/src/core/altium/SchematicPinParser.mjs +767 -0
  41. package/src/core/altium/SchematicPrimitiveParser.mjs +716 -0
  42. package/src/core/altium/SchematicSheetParser.mjs +241 -0
  43. package/src/core/altium/SchematicSheetStyleResolver.mjs +46 -0
  44. package/src/core/altium/SchematicStandaloneCalloutNormalizer.mjs +592 -0
  45. package/src/core/altium/SchematicTextParser.mjs +708 -0
  46. package/src/core/altium/SchematicTextPostProcessor.mjs +801 -0
  47. package/src/core/ole/OleCompoundDocument.mjs +439 -0
  48. package/src/core/ole/OleConstants.mjs +64 -0
  49. package/src/core/ole/OleDirectoryEntry.mjs +95 -0
  50. package/src/index.mjs +7 -0
  51. package/src/parser.mjs +21 -0
  52. package/src/renderers.mjs +15 -0
  53. package/src/scene3d.mjs +9 -0
  54. package/src/styles/altium-renderers.css +358 -0
  55. package/src/ui/BomTableRenderer.mjs +46 -0
  56. package/src/ui/PcbArcUtils.mjs +189 -0
  57. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +808 -0
  58. package/src/ui/PcbFootprintPrimitiveSelector.mjs +128 -0
  59. package/src/ui/PcbScene3dBuilder.mjs +742 -0
  60. package/src/ui/PcbScene3dModelRegistry.mjs +309 -0
  61. package/src/ui/PcbScene3dPackages.mjs +137 -0
  62. package/src/ui/PcbScene3dScenePreparator.mjs +36 -0
  63. package/src/ui/PcbScene3dSummaryRenderer.mjs +65 -0
  64. package/src/ui/PcbSvgRenderer.mjs +906 -0
  65. package/src/ui/SchematicColorResolver.mjs +132 -0
  66. package/src/ui/SchematicContentLayout.mjs +661 -0
  67. package/src/ui/SchematicDirectiveRenderer.mjs +184 -0
  68. package/src/ui/SchematicImageRenderer.mjs +135 -0
  69. package/src/ui/SchematicJunctionRenderer.mjs +381 -0
  70. package/src/ui/SchematicNoteRenderer.mjs +427 -0
  71. package/src/ui/SchematicOwnerPinLabelLayout.mjs +173 -0
  72. package/src/ui/SchematicPinSvgRenderer.mjs +495 -0
  73. package/src/ui/SchematicPortRenderer.mjs +558 -0
  74. package/src/ui/SchematicPowerPortRenderer.mjs +574 -0
  75. package/src/ui/SchematicRegionRenderer.mjs +94 -0
  76. package/src/ui/SchematicShapeRenderer.mjs +398 -0
  77. package/src/ui/SchematicSheetChromeRenderer.mjs +1025 -0
  78. package/src/ui/SchematicSheetSymbolRenderer.mjs +228 -0
  79. package/src/ui/SchematicSvgRenderer.mjs +756 -0
  80. package/src/ui/SchematicSvgUtils.mjs +182 -0
  81. package/src/ui/SchematicTypography.mjs +204 -0
  82. 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
+ }