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,480 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AltiumParser,
|
|
7
|
+
BomTableRenderer,
|
|
8
|
+
PcbScene3dSummaryRenderer,
|
|
9
|
+
PcbSvgRenderer,
|
|
10
|
+
SchematicSvgRenderer
|
|
11
|
+
} from '../../src/index.mjs'
|
|
12
|
+
import { PcbThreeSceneRenderer } from './PcbThreeSceneRenderer.mjs'
|
|
13
|
+
import { SvgViewportController } from './SvgViewportController.mjs'
|
|
14
|
+
|
|
15
|
+
const SOURCE_PROJECT_URL =
|
|
16
|
+
'https://github.com/Mehdi-KHALFALLAH/My-Arduino-UNO-Design'
|
|
17
|
+
const SOURCE_DOCUMENTS = [
|
|
18
|
+
{
|
|
19
|
+
key: 'schematic',
|
|
20
|
+
label: 'source schematic',
|
|
21
|
+
fileName: '[03] - 28PINS SHEMATIC.SchDoc',
|
|
22
|
+
defaultView: 'schematic',
|
|
23
|
+
url: 'https://raw.githubusercontent.com/Mehdi-KHALFALLAH/My-Arduino-UNO-Design/master/Design%20Files/28_Pin%20Project%20V1.1/%5B03%5D%20-%2028PINS%20SHEMATIC.SchDoc'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'pcb',
|
|
27
|
+
label: 'source PCB',
|
|
28
|
+
fileName: '28Pins_Project_1.1_PCB.PcbDoc',
|
|
29
|
+
defaultView: 'pcb',
|
|
30
|
+
url: 'https://raw.githubusercontent.com/Mehdi-KHALFALLAH/My-Arduino-UNO-Design/master/Design%20Files/28_Pin%20Project%20V1.1/28Pins_Project_1.1_PCB.PcbDoc'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Coordinates the local-file example page.
|
|
36
|
+
*/
|
|
37
|
+
class ArduinoUnoExample {
|
|
38
|
+
#activeView = 'schematic'
|
|
39
|
+
#documentModel = null
|
|
40
|
+
#elements
|
|
41
|
+
#sourceDocumentModels = new Map()
|
|
42
|
+
#svgViewportController = null
|
|
43
|
+
#threeRenderer = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Starts the browser example.
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*/
|
|
49
|
+
static boot() {
|
|
50
|
+
new ArduinoUnoExample().#bind()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates the page controller.
|
|
55
|
+
*/
|
|
56
|
+
constructor() {
|
|
57
|
+
this.#elements = {
|
|
58
|
+
input: document.querySelector('#document-file'),
|
|
59
|
+
output: document.querySelector('#output'),
|
|
60
|
+
status: document.querySelector('#status'),
|
|
61
|
+
tabs: [...document.querySelectorAll('[data-view]')]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Wires DOM events.
|
|
67
|
+
* @returns {void}
|
|
68
|
+
*/
|
|
69
|
+
#bind() {
|
|
70
|
+
this.#elements.input?.addEventListener('change', (event) => {
|
|
71
|
+
this.#handleFileSelection(event)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
for (const tab of this.#elements.tabs) {
|
|
75
|
+
tab.addEventListener('click', () => {
|
|
76
|
+
this.#setActiveView(tab.dataset.view)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.#loadSourceDocuments()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads and parses the selected file.
|
|
85
|
+
* @param {Event} event
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
async #handleFileSelection(event) {
|
|
89
|
+
const [file] = event.target.files || []
|
|
90
|
+
if (!file) return
|
|
91
|
+
|
|
92
|
+
this.#setStatus('Parsing ' + file.name + '...', 'busy')
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
this.#disposeSvgViewportController()
|
|
96
|
+
this.#disposeThreeRenderer()
|
|
97
|
+
const arrayBuffer = await file.arrayBuffer()
|
|
98
|
+
this.#documentModel = AltiumParser.parseArrayBuffer(
|
|
99
|
+
file.name,
|
|
100
|
+
arrayBuffer
|
|
101
|
+
)
|
|
102
|
+
this.#activeView =
|
|
103
|
+
this.#documentModel.kind === 'pcb' ? 'pcb' : 'schematic'
|
|
104
|
+
this.#setStatus(
|
|
105
|
+
'Loaded ' +
|
|
106
|
+
file.name +
|
|
107
|
+
' as ' +
|
|
108
|
+
this.#documentModel.fileType +
|
|
109
|
+
'.',
|
|
110
|
+
'ready'
|
|
111
|
+
)
|
|
112
|
+
this.#syncTabs()
|
|
113
|
+
this.#render()
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this.#disposeSvgViewportController()
|
|
116
|
+
this.#disposeThreeRenderer()
|
|
117
|
+
this.#documentModel = null
|
|
118
|
+
this.#setStatus(this.#formatError(error), 'error')
|
|
119
|
+
this.#renderError(error)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetches and parses every document from the credited source project.
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
async #loadSourceDocuments() {
|
|
128
|
+
this.#disposeSvgViewportController()
|
|
129
|
+
this.#disposeThreeRenderer()
|
|
130
|
+
this.#documentModel = null
|
|
131
|
+
this.#activeView = 'schematic'
|
|
132
|
+
this.#syncTabs()
|
|
133
|
+
this.#setStatus(
|
|
134
|
+
'Loading credited source documents from GitHub...',
|
|
135
|
+
'busy'
|
|
136
|
+
)
|
|
137
|
+
this.#elements.output.innerHTML = this.#renderLoadingState()
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const loadedDocuments = await Promise.all(
|
|
141
|
+
SOURCE_DOCUMENTS.map((sourceDocument) =>
|
|
142
|
+
this.#fetchSourceDocument(sourceDocument)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
this.#sourceDocumentModels = new Map(
|
|
146
|
+
loadedDocuments.map(({ sourceDocument, documentModel }) => [
|
|
147
|
+
sourceDocument.key,
|
|
148
|
+
documentModel
|
|
149
|
+
])
|
|
150
|
+
)
|
|
151
|
+
this.#setStatus(
|
|
152
|
+
'Loaded credited source schematic and source PCB from Mehdi KHALFALLAH via raw.githubusercontent.com.',
|
|
153
|
+
'ready'
|
|
154
|
+
)
|
|
155
|
+
this.#syncTabs()
|
|
156
|
+
this.#render()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this.#sourceDocumentModels = new Map()
|
|
159
|
+
this.#setStatus(this.#formatError(error), 'error')
|
|
160
|
+
this.#renderError(error)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fetches and parses one credited source document.
|
|
166
|
+
* @param {{ key: string, label: string, fileName: string, defaultView: string, url: string }} sourceDocument
|
|
167
|
+
* @returns {Promise<{ sourceDocument: { key: string, label: string, fileName: string, defaultView: string, url: string }, documentModel: ReturnType<typeof AltiumParser.parseArrayBuffer> }>}
|
|
168
|
+
*/
|
|
169
|
+
async #fetchSourceDocument(sourceDocument) {
|
|
170
|
+
const response = await fetch(sourceDocument.url)
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'GitHub returned HTTP ' +
|
|
174
|
+
response.status +
|
|
175
|
+
' for ' +
|
|
176
|
+
sourceDocument.fileName +
|
|
177
|
+
'.'
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
182
|
+
return {
|
|
183
|
+
sourceDocument,
|
|
184
|
+
documentModel: AltiumParser.parseArrayBuffer(
|
|
185
|
+
sourceDocument.fileName,
|
|
186
|
+
arrayBuffer
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Changes the active renderer tab.
|
|
193
|
+
* @param {string | undefined} view
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
196
|
+
#setActiveView(view) {
|
|
197
|
+
if (!view) return
|
|
198
|
+
this.#activeView = view
|
|
199
|
+
this.#syncTabs()
|
|
200
|
+
this.#render()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Updates tab ARIA and selected state.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
#syncTabs() {
|
|
208
|
+
for (const tab of this.#elements.tabs) {
|
|
209
|
+
const isActive = tab.dataset.view === this.#activeView
|
|
210
|
+
tab.classList.toggle('is-active', isActive)
|
|
211
|
+
tab.setAttribute('aria-selected', String(isActive))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Selects the parsed document model for the current view.
|
|
217
|
+
* @returns {ReturnType<typeof AltiumParser.parseArrayBuffer> | null}
|
|
218
|
+
*/
|
|
219
|
+
#getActiveDocumentModel() {
|
|
220
|
+
if (this.#documentModel) return this.#documentModel
|
|
221
|
+
|
|
222
|
+
const sourceKeyByView = {
|
|
223
|
+
'3d': 'pcb',
|
|
224
|
+
bom: 'schematic',
|
|
225
|
+
metadata: 'schematic',
|
|
226
|
+
pcb: 'pcb',
|
|
227
|
+
schematic: 'schematic',
|
|
228
|
+
summary: 'pcb'
|
|
229
|
+
}
|
|
230
|
+
const sourceKey = sourceKeyByView[this.#activeView]
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
this.#sourceDocumentModels.get(sourceKey) ||
|
|
234
|
+
this.#sourceDocumentModels.values().next().value ||
|
|
235
|
+
null
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Writes the current view to the output panel.
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
243
|
+
#render() {
|
|
244
|
+
this.#disposeSvgViewportController()
|
|
245
|
+
this.#disposeThreeRenderer()
|
|
246
|
+
const documentModel = this.#getActiveDocumentModel()
|
|
247
|
+
if (!documentModel) {
|
|
248
|
+
this.#elements.output.innerHTML = this.#renderEmptyState()
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const renderers = {
|
|
253
|
+
schematic: () => SchematicSvgRenderer.render(documentModel),
|
|
254
|
+
pcb: () => PcbSvgRenderer.render(documentModel),
|
|
255
|
+
bom: () => BomTableRenderer.render(documentModel.bom || []),
|
|
256
|
+
'3d': () => this.#renderThreeScene(documentModel),
|
|
257
|
+
summary: () => PcbScene3dSummaryRenderer.render(documentModel),
|
|
258
|
+
metadata: () => this.#renderMetadata(documentModel)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.#elements.output.innerHTML =
|
|
262
|
+
renderers[this.#activeView]?.() || this.#renderEmptyState()
|
|
263
|
+
|
|
264
|
+
if (this.#activeView === 'schematic') {
|
|
265
|
+
this.#mountSvgViewport('.schematic-svg')
|
|
266
|
+
} else if (this.#activeView === 'pcb') {
|
|
267
|
+
this.#mountSvgViewport('.pcb-svg')
|
|
268
|
+
} else if (this.#activeView === '3d') {
|
|
269
|
+
this.#mountThreeScene(documentModel)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Mounts pan and zoom controls on the active SVG renderer output.
|
|
275
|
+
* @param {string} selector
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
#mountSvgViewport(selector) {
|
|
279
|
+
const svgElement = this.#elements.output.querySelector(selector)
|
|
280
|
+
if (!svgElement) return
|
|
281
|
+
|
|
282
|
+
this.#svgViewportController = new SvgViewportController(svgElement)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Releases the active SVG viewport controller when the view changes.
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
#disposeSvgViewportController() {
|
|
290
|
+
this.#svgViewportController?.dispose()
|
|
291
|
+
this.#svgViewportController = null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Mounts the browser-only Three.js PCB view.
|
|
296
|
+
* @param {ReturnType<typeof AltiumParser.parseArrayBuffer>} documentModel
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
#mountThreeScene(documentModel) {
|
|
300
|
+
const rootNode = this.#elements.output.querySelector(
|
|
301
|
+
'[data-three-scene-3d]'
|
|
302
|
+
)
|
|
303
|
+
if (!rootNode) return
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
this.#threeRenderer = PcbThreeSceneRenderer.renderInto(
|
|
307
|
+
rootNode,
|
|
308
|
+
documentModel
|
|
309
|
+
)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
this.#renderError(error)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Releases the active Three.js renderer when the view changes.
|
|
317
|
+
* @returns {void}
|
|
318
|
+
*/
|
|
319
|
+
#disposeThreeRenderer() {
|
|
320
|
+
this.#threeRenderer?.dispose()
|
|
321
|
+
this.#threeRenderer = null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Renders the interactive Three.js PCB shell.
|
|
326
|
+
* @param {ReturnType<typeof AltiumParser.parseArrayBuffer>} documentModel
|
|
327
|
+
* @returns {string}
|
|
328
|
+
*/
|
|
329
|
+
#renderThreeScene(documentModel) {
|
|
330
|
+
const pcb = documentModel?.pcb
|
|
331
|
+
if (!pcb) {
|
|
332
|
+
return '<section class="empty-state"><h2>No PCB document loaded</h2><p>The interactive 3D view renders after the credited source PCB or another PCB document is loaded.</p></section>'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const widthMil = Math.round(pcb.boardOutline?.widthMil || 0)
|
|
336
|
+
const heightMil = Math.round(pcb.boardOutline?.heightMil || 0)
|
|
337
|
+
const componentCount = pcb.components?.length || 0
|
|
338
|
+
const padCount = pcb.pads?.length || 0
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
'<section class="scene-3d" data-three-scene-3d><header class="scene-3d__header"><div><h2>Interactive 3D PCB</h2><p>' +
|
|
342
|
+
widthMil +
|
|
343
|
+
' x ' +
|
|
344
|
+
heightMil +
|
|
345
|
+
' mil board envelope</p></div><dl class="scene-3d__stats"><div><dt>Components</dt><dd>' +
|
|
346
|
+
componentCount +
|
|
347
|
+
'</dd></div><div><dt>Pads</dt><dd>' +
|
|
348
|
+
padCount +
|
|
349
|
+
'</dd></div></dl></header><div class="scene-3d__toolbar" aria-label="3D camera presets">' +
|
|
350
|
+
'<button class="scene-3d__preset is-active" type="button" data-three-scene-3d-preset="isometric" aria-pressed="true">Isometric</button>' +
|
|
351
|
+
'<button class="scene-3d__preset" type="button" data-three-scene-3d-preset="top" aria-pressed="false">Top</button>' +
|
|
352
|
+
'<button class="scene-3d__preset" type="button" data-three-scene-3d-preset="bottom" aria-pressed="false">Bottom</button>' +
|
|
353
|
+
'</div><div class="scene-3d__stage"><div class="scene-3d__viewport" aria-label="Interactive 3D PCB view">' +
|
|
354
|
+
'<div class="scene-3d__canvas-mount" data-three-scene-3d-viewport></div>' +
|
|
355
|
+
'<div class="scene-3d__loading" data-three-scene-3d-loading aria-live="polite"><p>Rendering PCB scene...</p></div></div>' +
|
|
356
|
+
'<aside class="scene-3d__controls" aria-label="3D visibility controls">' +
|
|
357
|
+
'<label class="scene-3d__toggle"><input type="checkbox" checked data-three-scene-3d-toggle="components" />Components</label>' +
|
|
358
|
+
'<label class="scene-3d__toggle"><input type="checkbox" checked data-three-scene-3d-toggle="copper" />Copper</label>' +
|
|
359
|
+
'</aside></div><p class="scene-3d__diagnostics" data-three-scene-3d-diagnostics aria-live="polite">Rendering PCB scene...</p></section>'
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Renders parsed metadata and diagnostics.
|
|
365
|
+
* @param {ReturnType<typeof AltiumParser.parseArrayBuffer>} documentModel
|
|
366
|
+
* @returns {string}
|
|
367
|
+
*/
|
|
368
|
+
#renderMetadata(documentModel) {
|
|
369
|
+
const summaryRows = Object.entries(documentModel.summary || {})
|
|
370
|
+
.map(
|
|
371
|
+
([label, value]) =>
|
|
372
|
+
'<tr><th>' +
|
|
373
|
+
this.#escapeHtml(label) +
|
|
374
|
+
'</th><td>' +
|
|
375
|
+
this.#escapeHtml(String(value)) +
|
|
376
|
+
'</td></tr>'
|
|
377
|
+
)
|
|
378
|
+
.join('')
|
|
379
|
+
const diagnostics = (documentModel.diagnostics || [])
|
|
380
|
+
.map(
|
|
381
|
+
(diagnostic) =>
|
|
382
|
+
'<li><strong>' +
|
|
383
|
+
this.#escapeHtml(diagnostic.severity) +
|
|
384
|
+
'</strong> ' +
|
|
385
|
+
this.#escapeHtml(diagnostic.message) +
|
|
386
|
+
'</li>'
|
|
387
|
+
)
|
|
388
|
+
.join('')
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
'<section class="metadata-panel"><header><h2>' +
|
|
392
|
+
this.#escapeHtml(documentModel.fileName) +
|
|
393
|
+
'</h2><p>' +
|
|
394
|
+
this.#escapeHtml(documentModel.fileType) +
|
|
395
|
+
' parsed locally. Source inspiration: <a href="' +
|
|
396
|
+
SOURCE_PROJECT_URL +
|
|
397
|
+
'" target="_blank" rel="noreferrer">Mehdi KHALFALLAH's Arduino Uno Altium project</a>.</p></header>' +
|
|
398
|
+
'<table><tbody>' +
|
|
399
|
+
(summaryRows ||
|
|
400
|
+
'<tr><td colspan="2">No summary fields were recovered.</td></tr>') +
|
|
401
|
+
'</tbody></table><h3>Diagnostics</h3><ul>' +
|
|
402
|
+
(diagnostics || '<li>No diagnostics were reported.</li>') +
|
|
403
|
+
'</ul></section>'
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Renders the initial empty state.
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
#renderEmptyState() {
|
|
412
|
+
return (
|
|
413
|
+
'<section class="empty-state"><h2>No document loaded</h2><p>' +
|
|
414
|
+
'The example can fetch credited source documents from GitHub or load another local Altium design.</p></section>'
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Renders the source project loading state.
|
|
420
|
+
* @returns {string}
|
|
421
|
+
*/
|
|
422
|
+
#renderLoadingState() {
|
|
423
|
+
return (
|
|
424
|
+
'<section class="empty-state"><h2>Loading credited source documents</h2><p>' +
|
|
425
|
+
'Fetching the source schematic and source PCB from Mehdi KHALFALLAH's public GitHub project. The files are parsed in this browser session and are not stored in this repository.</p></section>'
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Renders a parse error.
|
|
431
|
+
* @param {unknown} error
|
|
432
|
+
* @returns {void}
|
|
433
|
+
*/
|
|
434
|
+
#renderError(error) {
|
|
435
|
+
this.#elements.output.innerHTML =
|
|
436
|
+
'<section class="empty-state empty-state--error"><h2>Unable to render document</h2><p>' +
|
|
437
|
+
this.#escapeHtml(this.#formatError(error)) +
|
|
438
|
+
'</p></section>'
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Updates the status text.
|
|
443
|
+
* @param {string} message
|
|
444
|
+
* @param {'busy' | 'error' | 'ready'} tone
|
|
445
|
+
* @returns {void}
|
|
446
|
+
*/
|
|
447
|
+
#setStatus(message, tone = 'ready') {
|
|
448
|
+
this.#elements.status.textContent = message
|
|
449
|
+
this.#elements.status.dataset.tone = tone
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Formats thrown values for display.
|
|
454
|
+
* @param {unknown} error
|
|
455
|
+
* @returns {string}
|
|
456
|
+
*/
|
|
457
|
+
#formatError(error) {
|
|
458
|
+
return error instanceof Error ? error.message : String(error)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Escapes text for trusted example markup assembly.
|
|
463
|
+
* @param {string} value
|
|
464
|
+
* @returns {string}
|
|
465
|
+
*/
|
|
466
|
+
#escapeHtml(value) {
|
|
467
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
468
|
+
const escapes = {
|
|
469
|
+
'&': '&',
|
|
470
|
+
'<': '<',
|
|
471
|
+
'>': '>',
|
|
472
|
+
'"': '"',
|
|
473
|
+
"'": '''
|
|
474
|
+
}
|
|
475
|
+
return escapes[character]
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
ArduinoUnoExample.boot()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 André Fiedler
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: GPL-3.0-or-later
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<!doctype html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<title>Arduino Uno Altium Example | Altium Toolkit</title>
|
|
13
|
+
<link
|
|
14
|
+
rel="icon"
|
|
15
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%231f7a68'/%3E%3Cpath d='M4 5h8v6H4z' fill='%23fffaf3'/%3E%3Cpath d='M5 8h6' stroke='%23c35f35' stroke-width='1.5'/%3E%3C/svg%3E"
|
|
16
|
+
/>
|
|
17
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
18
|
+
<script type="importmap">
|
|
19
|
+
{
|
|
20
|
+
"imports": {
|
|
21
|
+
"fflate": "../../node_modules/fflate/esm/browser.js",
|
|
22
|
+
"three": "../../node_modules/three/build/three.module.js",
|
|
23
|
+
"three/addons/": "../../node_modules/three/examples/jsm/"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
<script type="module" src="./example.mjs"></script>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<main class="app-shell">
|
|
31
|
+
<header class="app-header">
|
|
32
|
+
<div>
|
|
33
|
+
<p class="eyebrow">Altium Toolkit example</p>
|
|
34
|
+
<h1>Arduino Uno design viewer</h1>
|
|
35
|
+
<p class="credit">
|
|
36
|
+
Based on the public My-Arduino-UNO-Design Altium project
|
|
37
|
+
by
|
|
38
|
+
<a
|
|
39
|
+
href="https://github.com/Mehdi-KHALFALLAH/My-Arduino-UNO-Design"
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noreferrer"
|
|
42
|
+
>Mehdi KHALFALLAH</a
|
|
43
|
+
>. All credit for that Altium project belongs to him.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
<a
|
|
47
|
+
class="source-link"
|
|
48
|
+
href="https://github.com/Mehdi-KHALFALLAH/My-Arduino-UNO-Design"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noreferrer"
|
|
51
|
+
>Source project</a
|
|
52
|
+
>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<section class="workspace" aria-label="Arduino Uno design example">
|
|
56
|
+
<aside class="control-panel">
|
|
57
|
+
<label class="file-picker" for="document-file">
|
|
58
|
+
<span>Load local Altium document</span>
|
|
59
|
+
<input
|
|
60
|
+
id="document-file"
|
|
61
|
+
type="file"
|
|
62
|
+
accept=".SchDoc,.PcbDoc,.schdoc,.pcbdoc"
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
65
|
+
|
|
66
|
+
<p id="status" class="status" role="status">
|
|
67
|
+
Loading credited source documents from GitHub...
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<dl class="source-facts">
|
|
71
|
+
<div>
|
|
72
|
+
<dt>Reference</dt>
|
|
73
|
+
<dd>Arduino Uno recreated in Altium Designer</dd>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<dt>Creator</dt>
|
|
77
|
+
<dd>Mehdi KHALFALLAH</dd>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<dt>Data flow</dt>
|
|
81
|
+
<dd>
|
|
82
|
+
Auto-fetch source documents or load local files
|
|
83
|
+
</dd>
|
|
84
|
+
</div>
|
|
85
|
+
</dl>
|
|
86
|
+
</aside>
|
|
87
|
+
|
|
88
|
+
<section class="viewer-panel" aria-label="Rendered output">
|
|
89
|
+
<div class="viewer-toolbar">
|
|
90
|
+
<div class="tabs" role="tablist" aria-label="Views">
|
|
91
|
+
<button
|
|
92
|
+
class="tab is-active"
|
|
93
|
+
type="button"
|
|
94
|
+
data-view="schematic"
|
|
95
|
+
role="tab"
|
|
96
|
+
aria-selected="true"
|
|
97
|
+
>
|
|
98
|
+
Schematic
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
class="tab"
|
|
102
|
+
type="button"
|
|
103
|
+
data-view="pcb"
|
|
104
|
+
role="tab"
|
|
105
|
+
aria-selected="false"
|
|
106
|
+
>
|
|
107
|
+
PCB
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
class="tab"
|
|
111
|
+
type="button"
|
|
112
|
+
data-view="bom"
|
|
113
|
+
role="tab"
|
|
114
|
+
aria-selected="false"
|
|
115
|
+
>
|
|
116
|
+
BOM
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
class="tab"
|
|
120
|
+
type="button"
|
|
121
|
+
data-view="3d"
|
|
122
|
+
role="tab"
|
|
123
|
+
aria-selected="false"
|
|
124
|
+
>
|
|
125
|
+
3D
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
class="tab"
|
|
129
|
+
type="button"
|
|
130
|
+
data-view="summary"
|
|
131
|
+
role="tab"
|
|
132
|
+
aria-selected="false"
|
|
133
|
+
>
|
|
134
|
+
Summary
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
class="tab"
|
|
138
|
+
type="button"
|
|
139
|
+
data-view="metadata"
|
|
140
|
+
role="tab"
|
|
141
|
+
aria-selected="false"
|
|
142
|
+
>
|
|
143
|
+
Metadata
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div id="output" class="output" role="tabpanel">
|
|
149
|
+
<section class="empty-state">
|
|
150
|
+
<h2>Loading credited source documents</h2>
|
|
151
|
+
<p>
|
|
152
|
+
The example fetches Mehdi KHALFALLAH's public
|
|
153
|
+
Arduino Uno schematic and PCB documents from
|
|
154
|
+
GitHub at runtime. The Altium files are not
|
|
155
|
+
redistributed in this repository.
|
|
156
|
+
</p>
|
|
157
|
+
</section>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
</section>
|
|
161
|
+
</main>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|