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,439 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { BinaryReader } from '../BinaryReader.mjs'
|
|
6
|
+
import { OleConstants } from './OleConstants.mjs'
|
|
7
|
+
import { OleDirectoryEntry } from './OleDirectoryEntry.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads OLE Compound Document streams.
|
|
11
|
+
*/
|
|
12
|
+
export class OleCompoundDocument {
|
|
13
|
+
#directoryEntries
|
|
14
|
+
|
|
15
|
+
#fatEntries
|
|
16
|
+
|
|
17
|
+
#header
|
|
18
|
+
|
|
19
|
+
#miniFatEntries
|
|
20
|
+
|
|
21
|
+
#miniStreamBytes
|
|
22
|
+
|
|
23
|
+
#reader
|
|
24
|
+
|
|
25
|
+
#streamPaths
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
29
|
+
*/
|
|
30
|
+
constructor(arrayBuffer) {
|
|
31
|
+
this.#reader = new BinaryReader(arrayBuffer)
|
|
32
|
+
this.#header = this.#parseHeader()
|
|
33
|
+
this.#fatEntries = this.#parseFatEntries()
|
|
34
|
+
this.#directoryEntries = this.#parseDirectoryEntries()
|
|
35
|
+
this.#miniFatEntries = this.#parseMiniFatEntries()
|
|
36
|
+
this.#miniStreamBytes = this.#parseMiniStreamBytes()
|
|
37
|
+
this.#streamPaths = this.#collectStreamPaths()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates one compound document reader from raw bytes.
|
|
42
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
43
|
+
* @returns {OleCompoundDocument}
|
|
44
|
+
*/
|
|
45
|
+
static fromArrayBuffer(arrayBuffer) {
|
|
46
|
+
return new OleCompoundDocument(arrayBuffer)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the sector size in bytes.
|
|
51
|
+
* @returns {number}
|
|
52
|
+
*/
|
|
53
|
+
get sectorByteLength() {
|
|
54
|
+
return this.#header.sectorByteLength
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the mini-sector size in bytes.
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
get miniSectorByteLength() {
|
|
62
|
+
return this.#header.miniSectorByteLength
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Lists all stream paths.
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
listStreams() {
|
|
70
|
+
return [...this.#streamPaths.keys()].sort((left, right) =>
|
|
71
|
+
left.localeCompare(right)
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves one stream by path or unique leaf name.
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @returns {Uint8Array}
|
|
79
|
+
*/
|
|
80
|
+
getStream(name) {
|
|
81
|
+
const directMatch = this.#streamPaths.get(name)
|
|
82
|
+
if (directMatch) {
|
|
83
|
+
return this.#readEntryStream(directMatch)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const leafMatches = [...this.#streamPaths.entries()].filter(
|
|
87
|
+
([path]) => path.split('/').at(-1) === name
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if (!leafMatches.length) {
|
|
91
|
+
throw new Error('OLE stream not found: ' + name)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (leafMatches.length > 1) {
|
|
95
|
+
throw new Error('OLE stream name is ambiguous: ' + name)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.#readEntryStream(leafMatches[0][1])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parses and validates the OLE header.
|
|
103
|
+
* @returns {{ sectorByteLength: number, miniSectorByteLength: number, numberOfFatSectors: number, firstDirectorySector: number, miniStreamCutoff: number, firstMiniFatSector: number, numberOfMiniFatSectors: number, firstDifatSector: number, numberOfDifatSectors: number, difatEntries: number[] }}
|
|
104
|
+
*/
|
|
105
|
+
#parseHeader() {
|
|
106
|
+
const signature = [
|
|
107
|
+
...this.#reader.readBytes(0, OleConstants.HEADER_SIGNATURE.length)
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
signature.some(
|
|
112
|
+
(value, index) => value !== OleConstants.HEADER_SIGNATURE[index]
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error('Invalid OLE header signature.')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sectorByteLength = 2 ** this.#reader.readUint16(30)
|
|
119
|
+
const miniSectorByteLength = 2 ** this.#reader.readUint16(32)
|
|
120
|
+
const difatEntries = []
|
|
121
|
+
|
|
122
|
+
for (let index = 0; index < 109; index += 1) {
|
|
123
|
+
difatEntries.push(this.#reader.readInt32(76 + index * 4))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
sectorByteLength,
|
|
128
|
+
miniSectorByteLength,
|
|
129
|
+
numberOfFatSectors: this.#reader.readUint32(44),
|
|
130
|
+
firstDirectorySector: this.#reader.readInt32(48),
|
|
131
|
+
miniStreamCutoff: this.#reader.readUint32(56),
|
|
132
|
+
firstMiniFatSector: this.#reader.readInt32(60),
|
|
133
|
+
numberOfMiniFatSectors: this.#reader.readUint32(64),
|
|
134
|
+
firstDifatSector: this.#reader.readInt32(68),
|
|
135
|
+
numberOfDifatSectors: this.#reader.readUint32(72),
|
|
136
|
+
difatEntries
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Parses all FAT entries.
|
|
142
|
+
* @returns {number[]}
|
|
143
|
+
*/
|
|
144
|
+
#parseFatEntries() {
|
|
145
|
+
const fatSectorIds = this.#collectFatSectorIds()
|
|
146
|
+
const entries = []
|
|
147
|
+
const intsPerSector = this.#header.sectorByteLength / 4
|
|
148
|
+
|
|
149
|
+
for (const sectorId of fatSectorIds) {
|
|
150
|
+
const sectorBytes = this.#readSectorBytes(sectorId)
|
|
151
|
+
const reader = new BinaryReader(sectorBytes.buffer)
|
|
152
|
+
|
|
153
|
+
for (let index = 0; index < intsPerSector; index += 1) {
|
|
154
|
+
entries.push(reader.readInt32(index * 4))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return entries
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Collects all FAT sector identifiers from the header and optional DIFAT
|
|
163
|
+
* sectors.
|
|
164
|
+
* @returns {number[]}
|
|
165
|
+
*/
|
|
166
|
+
#collectFatSectorIds() {
|
|
167
|
+
const fatSectorIds = this.#header.difatEntries.filter(
|
|
168
|
+
(sectorId) => sectorId >= 0
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
this.#header.firstDifatSector < 0 ||
|
|
173
|
+
this.#header.numberOfDifatSectors === 0
|
|
174
|
+
) {
|
|
175
|
+
return fatSectorIds.slice(0, this.#header.numberOfFatSectors)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const intsPerSector = this.#header.sectorByteLength / 4
|
|
179
|
+
let currentSectorId = this.#header.firstDifatSector
|
|
180
|
+
|
|
181
|
+
for (
|
|
182
|
+
let difatIndex = 0;
|
|
183
|
+
difatIndex < this.#header.numberOfDifatSectors &&
|
|
184
|
+
currentSectorId >= 0;
|
|
185
|
+
difatIndex += 1
|
|
186
|
+
) {
|
|
187
|
+
const sectorReader = new BinaryReader(
|
|
188
|
+
this.#readSectorBytes(currentSectorId).buffer
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for (let index = 0; index < intsPerSector - 1; index += 1) {
|
|
192
|
+
const fatSectorId = sectorReader.readInt32(index * 4)
|
|
193
|
+
if (fatSectorId >= 0) {
|
|
194
|
+
fatSectorIds.push(fatSectorId)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
currentSectorId = sectorReader.readInt32((intsPerSector - 1) * 4)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return fatSectorIds.slice(0, this.#header.numberOfFatSectors)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parses all directory entries reachable from the directory stream chain.
|
|
206
|
+
* @returns {OleDirectoryEntry[]}
|
|
207
|
+
*/
|
|
208
|
+
#parseDirectoryEntries() {
|
|
209
|
+
const directoryChain = this.#readRegularChain(
|
|
210
|
+
this.#header.firstDirectorySector
|
|
211
|
+
)
|
|
212
|
+
const entryCount =
|
|
213
|
+
directoryChain.length / OleConstants.DIRECTORY_ENTRY_BYTE_LENGTH
|
|
214
|
+
const entries = []
|
|
215
|
+
|
|
216
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
217
|
+
const offset = index * OleConstants.DIRECTORY_ENTRY_BYTE_LENGTH
|
|
218
|
+
entries.push(
|
|
219
|
+
OleDirectoryEntry.fromBytes(
|
|
220
|
+
directoryChain.slice(
|
|
221
|
+
offset,
|
|
222
|
+
offset + OleConstants.DIRECTORY_ENTRY_BYTE_LENGTH
|
|
223
|
+
),
|
|
224
|
+
index
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return entries
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parses all mini FAT entries.
|
|
234
|
+
* @returns {number[]}
|
|
235
|
+
*/
|
|
236
|
+
#parseMiniFatEntries() {
|
|
237
|
+
if (
|
|
238
|
+
this.#header.firstMiniFatSector < 0 ||
|
|
239
|
+
this.#header.numberOfMiniFatSectors === 0
|
|
240
|
+
) {
|
|
241
|
+
return []
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const chain = this.#readRegularChain(this.#header.firstMiniFatSector)
|
|
245
|
+
const reader = new BinaryReader(chain.buffer)
|
|
246
|
+
const entryCount = chain.length / 4
|
|
247
|
+
const entries = []
|
|
248
|
+
|
|
249
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
250
|
+
entries.push(reader.readInt32(index * 4))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return entries
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Reads the root mini-stream bytes.
|
|
258
|
+
* @returns {Uint8Array}
|
|
259
|
+
*/
|
|
260
|
+
#parseMiniStreamBytes() {
|
|
261
|
+
const rootEntry = this.#directoryEntries.find((entry) =>
|
|
262
|
+
entry.isRootStorage()
|
|
263
|
+
)
|
|
264
|
+
if (!rootEntry || rootEntry.startSector < 0 || !rootEntry.streamSize) {
|
|
265
|
+
return new Uint8Array(0)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return this.#readRegularChain(rootEntry.startSector).slice(
|
|
269
|
+
0,
|
|
270
|
+
rootEntry.streamSize
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Builds a path map for all reachable stream entries.
|
|
276
|
+
* @returns {Map<string, OleDirectoryEntry>}
|
|
277
|
+
*/
|
|
278
|
+
#collectStreamPaths() {
|
|
279
|
+
const rootEntry = this.#directoryEntries.find((entry) =>
|
|
280
|
+
entry.isRootStorage()
|
|
281
|
+
)
|
|
282
|
+
const paths = new Map()
|
|
283
|
+
|
|
284
|
+
if (!rootEntry || rootEntry.childId < 0) {
|
|
285
|
+
return paths
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.#walkDirectoryTree(rootEntry.childId, '', paths)
|
|
289
|
+
|
|
290
|
+
return paths
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Traverses one directory sibling tree.
|
|
295
|
+
* @param {number} entryId
|
|
296
|
+
* @param {string} basePath
|
|
297
|
+
* @param {Map<string, OleDirectoryEntry>} paths
|
|
298
|
+
*/
|
|
299
|
+
#walkDirectoryTree(entryId, basePath, paths) {
|
|
300
|
+
if (entryId < 0) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const entry = this.#directoryEntries[entryId]
|
|
305
|
+
if (!entry) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.#walkDirectoryTree(entry.leftSiblingId, basePath, paths)
|
|
310
|
+
|
|
311
|
+
const path = basePath ? basePath + '/' + entry.name : entry.name
|
|
312
|
+
|
|
313
|
+
if (entry.isStream()) {
|
|
314
|
+
paths.set(path, entry)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (entry.isStorage() && !entry.isRootStorage() && entry.childId >= 0) {
|
|
318
|
+
this.#walkDirectoryTree(entry.childId, path, paths)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.#walkDirectoryTree(entry.rightSiblingId, basePath, paths)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reads one stream entry using the standard or mini FAT.
|
|
326
|
+
* @param {OleDirectoryEntry} entry
|
|
327
|
+
* @returns {Uint8Array}
|
|
328
|
+
*/
|
|
329
|
+
#readEntryStream(entry) {
|
|
330
|
+
if (entry.streamSize < this.#header.miniStreamCutoff) {
|
|
331
|
+
return this.#readMiniStream(entry)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return this.#readRegularChain(entry.startSector).slice(
|
|
335
|
+
0,
|
|
336
|
+
entry.streamSize
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Reads one standard-sector chain.
|
|
342
|
+
* @param {number} startSectorId
|
|
343
|
+
* @returns {Uint8Array}
|
|
344
|
+
*/
|
|
345
|
+
#readRegularChain(startSectorId) {
|
|
346
|
+
return this.#concatenateSectors(
|
|
347
|
+
this.#readSectorChain(startSectorId, this.#fatEntries),
|
|
348
|
+
this.#header.sectorByteLength,
|
|
349
|
+
(sectorId) => this.#readSectorBytes(sectorId)
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Reads one mini-sector-backed stream.
|
|
355
|
+
* @param {OleDirectoryEntry} entry
|
|
356
|
+
* @returns {Uint8Array}
|
|
357
|
+
*/
|
|
358
|
+
#readMiniStream(entry) {
|
|
359
|
+
const sectorIds = this.#readSectorChain(
|
|
360
|
+
entry.startSector,
|
|
361
|
+
this.#miniFatEntries
|
|
362
|
+
)
|
|
363
|
+
const bytes = this.#concatenateSectors(
|
|
364
|
+
sectorIds,
|
|
365
|
+
this.#header.miniSectorByteLength,
|
|
366
|
+
(miniSectorId) =>
|
|
367
|
+
this.#miniStreamBytes.slice(
|
|
368
|
+
miniSectorId * this.#header.miniSectorByteLength,
|
|
369
|
+
(miniSectorId + 1) * this.#header.miniSectorByteLength
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return bytes.slice(0, entry.streamSize)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Reads one chain of sector identifiers.
|
|
378
|
+
* @param {number} startSectorId
|
|
379
|
+
* @param {number[]} entries
|
|
380
|
+
* @returns {number[]}
|
|
381
|
+
*/
|
|
382
|
+
#readSectorChain(startSectorId, entries) {
|
|
383
|
+
const sectorIds = []
|
|
384
|
+
const visited = new Set()
|
|
385
|
+
let currentSectorId = startSectorId
|
|
386
|
+
|
|
387
|
+
while (currentSectorId >= 0) {
|
|
388
|
+
if (visited.has(currentSectorId)) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
'OLE sector chain loop detected at sector ' +
|
|
391
|
+
currentSectorId +
|
|
392
|
+
'.'
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
visited.add(currentSectorId)
|
|
397
|
+
sectorIds.push(currentSectorId)
|
|
398
|
+
|
|
399
|
+
const nextSectorId = entries[currentSectorId]
|
|
400
|
+
if (nextSectorId === OleConstants.END_OF_CHAIN) {
|
|
401
|
+
break
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
currentSectorId = nextSectorId
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return sectorIds
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Concatenates sector bytes from one chain.
|
|
412
|
+
* @param {number[]} sectorIds
|
|
413
|
+
* @param {number} sectorByteLength
|
|
414
|
+
* @param {(sectorId: number) => Uint8Array} byteResolver
|
|
415
|
+
* @returns {Uint8Array}
|
|
416
|
+
*/
|
|
417
|
+
#concatenateSectors(sectorIds, sectorByteLength, byteResolver) {
|
|
418
|
+
const bytes = new Uint8Array(sectorIds.length * sectorByteLength)
|
|
419
|
+
|
|
420
|
+
for (let index = 0; index < sectorIds.length; index += 1) {
|
|
421
|
+
bytes.set(byteResolver(sectorIds[index]), index * sectorByteLength)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return bytes
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Reads one regular sector.
|
|
429
|
+
* @param {number} sectorId
|
|
430
|
+
* @returns {Uint8Array}
|
|
431
|
+
*/
|
|
432
|
+
#readSectorBytes(sectorId) {
|
|
433
|
+
const offset =
|
|
434
|
+
OleConstants.HEADER_BYTE_LENGTH +
|
|
435
|
+
sectorId * this.#header.sectorByteLength
|
|
436
|
+
|
|
437
|
+
return this.#reader.readBytes(offset, this.#header.sectorByteLength)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared OLE Compound Document constants.
|
|
7
|
+
*/
|
|
8
|
+
export class OleConstants {
|
|
9
|
+
/**
|
|
10
|
+
* @returns {number}
|
|
11
|
+
*/
|
|
12
|
+
static get HEADER_BYTE_LENGTH() {
|
|
13
|
+
return 512
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*/
|
|
19
|
+
static get DIRECTORY_ENTRY_BYTE_LENGTH() {
|
|
20
|
+
return 128
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @returns {number[]}
|
|
25
|
+
*/
|
|
26
|
+
static get HEADER_SIGNATURE() {
|
|
27
|
+
return [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
static get FREE_SECTOR() {
|
|
34
|
+
return -1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
static get END_OF_CHAIN() {
|
|
41
|
+
return -2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
static get FAT_SECTOR() {
|
|
48
|
+
return -3
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns {number}
|
|
53
|
+
*/
|
|
54
|
+
static get DIFAT_SECTOR() {
|
|
55
|
+
return -4
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
static get NO_STREAM() {
|
|
62
|
+
return -1
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { BinaryReader } from '../BinaryReader.mjs'
|
|
6
|
+
import { OleConstants } from './OleConstants.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents one decoded OLE directory entry.
|
|
10
|
+
*/
|
|
11
|
+
export class OleDirectoryEntry {
|
|
12
|
+
/**
|
|
13
|
+
* @param {{ id: number, name: string, type: number, leftSiblingId: number, rightSiblingId: number, childId: number, startSector: number, streamSize: number }} options
|
|
14
|
+
*/
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.id = options.id
|
|
17
|
+
this.name = options.name
|
|
18
|
+
this.type = options.type
|
|
19
|
+
this.leftSiblingId = options.leftSiblingId
|
|
20
|
+
this.rightSiblingId = options.rightSiblingId
|
|
21
|
+
this.childId = options.childId
|
|
22
|
+
this.startSector = options.startSector
|
|
23
|
+
this.streamSize = options.streamSize
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decodes one raw directory-entry buffer.
|
|
28
|
+
* @param {Uint8Array} bytes
|
|
29
|
+
* @param {number} id
|
|
30
|
+
* @returns {OleDirectoryEntry}
|
|
31
|
+
*/
|
|
32
|
+
static fromBytes(bytes, id) {
|
|
33
|
+
const reader = new BinaryReader(
|
|
34
|
+
bytes.buffer.slice(
|
|
35
|
+
bytes.byteOffset,
|
|
36
|
+
bytes.byteOffset + bytes.byteLength
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
const nameByteLength = Math.max(
|
|
40
|
+
Math.min(reader.readUint16(64), 64) - 2,
|
|
41
|
+
0
|
|
42
|
+
)
|
|
43
|
+
const name = new TextDecoder('utf-16le')
|
|
44
|
+
.decode(reader.readBytes(0, nameByteLength))
|
|
45
|
+
.replace(/\u0000+$/g, '')
|
|
46
|
+
|
|
47
|
+
return new OleDirectoryEntry({
|
|
48
|
+
id,
|
|
49
|
+
name,
|
|
50
|
+
type: reader.readUint8(66),
|
|
51
|
+
leftSiblingId: reader.readInt32(68),
|
|
52
|
+
rightSiblingId: reader.readInt32(72),
|
|
53
|
+
childId: reader.readInt32(76),
|
|
54
|
+
startSector: reader.readInt32(116),
|
|
55
|
+
streamSize: reader.readUint64(120)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns true when this entry stores stream bytes.
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
isStream() {
|
|
64
|
+
return this.type === 2
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns true when this entry is the root storage.
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
isRootStorage() {
|
|
72
|
+
return this.type === 5
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns true when this entry is a non-root storage.
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
isStorage() {
|
|
80
|
+
return this.type === 1 || this.type === 5
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns true when the entry has no useful payload.
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
isEmpty() {
|
|
88
|
+
return (
|
|
89
|
+
!this.name &&
|
|
90
|
+
this.type === 0 &&
|
|
91
|
+
this.startSector === OleConstants.NO_STREAM &&
|
|
92
|
+
this.streamSize === 0
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/index.mjs
ADDED
package/src/parser.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
export { BinaryReader } from './core/BinaryReader.mjs'
|
|
6
|
+
export { OleCompoundDocument } from './core/ole/OleCompoundDocument.mjs'
|
|
7
|
+
export { OleConstants } from './core/ole/OleConstants.mjs'
|
|
8
|
+
export { OleDirectoryEntry } from './core/ole/OleDirectoryEntry.mjs'
|
|
9
|
+
export { AltiumParser } from './core/altium/AltiumParser.mjs'
|
|
10
|
+
export { AltiumLayoutParser } from './core/altium/AltiumLayoutParser.mjs'
|
|
11
|
+
export { AsciiRecordParser } from './core/altium/AsciiRecordParser.mjs'
|
|
12
|
+
export { ParserUtils } from './core/altium/ParserUtils.mjs'
|
|
13
|
+
export { PcbBinaryPrimitiveParser } from './core/altium/PcbBinaryPrimitiveParser.mjs'
|
|
14
|
+
export { PcbEmbeddedModelExtractor } from './core/altium/PcbEmbeddedModelExtractor.mjs'
|
|
15
|
+
export { PcbModelParser } from './core/altium/PcbModelParser.mjs'
|
|
16
|
+
export { PcbOutlineRasterizer } from './core/altium/PcbOutlineRasterizer.mjs'
|
|
17
|
+
export { PcbOutlineRecovery } from './core/altium/PcbOutlineRecovery.mjs'
|
|
18
|
+
export { PcbStreamExtractor } from './core/altium/PcbStreamExtractor.mjs'
|
|
19
|
+
export { PrintableTextDecoder } from './core/altium/PrintableTextDecoder.mjs'
|
|
20
|
+
export { SchematicMultipartOwnerMatcher } from './core/altium/SchematicMultipartOwnerMatcher.mjs'
|
|
21
|
+
export { SchematicStandaloneCalloutNormalizer } from './core/altium/SchematicStandaloneCalloutNormalizer.mjs'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
export { BomTableRenderer } from './ui/BomTableRenderer.mjs'
|
|
6
|
+
export { PcbArcUtils } from './ui/PcbArcUtils.mjs'
|
|
7
|
+
export { PcbEdgeFacingGlyphNormalizer } from './ui/PcbEdgeFacingGlyphNormalizer.mjs'
|
|
8
|
+
export { PcbFootprintPrimitiveSelector } from './ui/PcbFootprintPrimitiveSelector.mjs'
|
|
9
|
+
export { PcbSvgRenderer } from './ui/PcbSvgRenderer.mjs'
|
|
10
|
+
export { SchematicColorResolver } from './ui/SchematicColorResolver.mjs'
|
|
11
|
+
export { SchematicContentLayout } from './ui/SchematicContentLayout.mjs'
|
|
12
|
+
export { SchematicOwnerPinLabelLayout } from './ui/SchematicOwnerPinLabelLayout.mjs'
|
|
13
|
+
export { SchematicSvgRenderer } from './ui/SchematicSvgRenderer.mjs'
|
|
14
|
+
export { SchematicSvgUtils } from './ui/SchematicSvgUtils.mjs'
|
|
15
|
+
export { SchematicTypography } from './ui/SchematicTypography.mjs'
|
package/src/scene3d.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
export { PcbScene3dBuilder } from './ui/PcbScene3dBuilder.mjs'
|
|
6
|
+
export { PcbScene3dModelRegistry } from './ui/PcbScene3dModelRegistry.mjs'
|
|
7
|
+
export { PcbScene3dPackages } from './ui/PcbScene3dPackages.mjs'
|
|
8
|
+
export { PcbScene3dScenePreparator } from './ui/PcbScene3dScenePreparator.mjs'
|
|
9
|
+
export { PcbScene3dSummaryRenderer } from './ui/PcbScene3dSummaryRenderer.mjs'
|