altium-toolkit 0.1.0 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +21 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes Altium PCB region primitive streams.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbRegionPrimitiveParser {
|
|
11
|
+
static #REGION_OBJECT_ID = 11
|
|
12
|
+
|
|
13
|
+
static #REGION_HEADER_BYTE_LENGTH = 18
|
|
14
|
+
|
|
15
|
+
static #REGION_VERTEX_BYTE_LENGTH = 16
|
|
16
|
+
|
|
17
|
+
static #SHAPE_REGION_VERTEX_BYTE_LENGTH = 37
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decodes one variable-length PCB region stream.
|
|
21
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
22
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
23
|
+
* @param {{ shapeBased?: boolean }} [options]
|
|
24
|
+
* @returns {{ layerId: number, layerCode: number, netIndex: number | null, polygonIndex: number | null, componentIndex: number | null, kind: number, isKeepout: boolean, isBoardCutout: boolean, isShapeBased: boolean, points: object[], holes: object[][], properties: Record<string, string> }[]}
|
|
25
|
+
*/
|
|
26
|
+
static parseRegionStream(headerBytes, dataBytes, options = {}) {
|
|
27
|
+
const count = PcbRegionPrimitiveParser.#readRecordCount(headerBytes)
|
|
28
|
+
const normalizedData = PcbRegionPrimitiveParser.#toUint8Array(dataBytes)
|
|
29
|
+
|
|
30
|
+
if (!count) {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let offset = 0
|
|
35
|
+
const regions = []
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < count; index += 1) {
|
|
38
|
+
const parsedRegion = PcbRegionPrimitiveParser.#parseRegionRecord(
|
|
39
|
+
normalizedData,
|
|
40
|
+
offset,
|
|
41
|
+
options.shapeBased === true
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (!parsedRegion) {
|
|
45
|
+
return regions
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
regions.push(parsedRegion.region)
|
|
49
|
+
offset += parsedRegion.byteLength
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return regions
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parses one variable-length PCB region record.
|
|
57
|
+
* @param {Uint8Array} bytes
|
|
58
|
+
* @param {number} offset
|
|
59
|
+
* @param {boolean} shapeBased
|
|
60
|
+
* @returns {{ region: { layerId: number, layerCode: number, netIndex: number | null, polygonIndex: number | null, componentIndex: number | null, kind: number, isKeepout: boolean, isBoardCutout: boolean, isShapeBased: boolean, points: object[], holes: object[][], properties: Record<string, string> }, byteLength: number } | null}
|
|
61
|
+
*/
|
|
62
|
+
static #parseRegionRecord(bytes, offset, shapeBased) {
|
|
63
|
+
if (
|
|
64
|
+
offset + 5 > bytes.byteLength ||
|
|
65
|
+
bytes[offset] !== PcbRegionPrimitiveParser.#REGION_OBJECT_ID
|
|
66
|
+
) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const payloadLength = PcbRegionPrimitiveParser.#readUint32FromBytes(
|
|
71
|
+
bytes,
|
|
72
|
+
offset + 1
|
|
73
|
+
)
|
|
74
|
+
const payloadOffset = offset + 5
|
|
75
|
+
const payloadEnd = payloadOffset + payloadLength
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
payloadLength <
|
|
79
|
+
PcbRegionPrimitiveParser.#REGION_HEADER_BYTE_LENGTH ||
|
|
80
|
+
payloadEnd > bytes.byteLength
|
|
81
|
+
) {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const view = new DataView(
|
|
86
|
+
bytes.buffer,
|
|
87
|
+
bytes.byteOffset + payloadOffset,
|
|
88
|
+
payloadLength
|
|
89
|
+
)
|
|
90
|
+
const layerId = view.getUint8(0)
|
|
91
|
+
const flags2 = view.getUint8(2)
|
|
92
|
+
const holeCount = view.getUint16(14, true)
|
|
93
|
+
let cursor = PcbRegionPrimitiveParser.#REGION_HEADER_BYTE_LENGTH
|
|
94
|
+
|
|
95
|
+
if (cursor + 4 > view.byteLength) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const propertyByteLength = view.getUint32(cursor, true)
|
|
100
|
+
cursor += 4
|
|
101
|
+
if (cursor + propertyByteLength > view.byteLength) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const properties = PcbRegionPrimitiveParser.#parsePropertyBytes(
|
|
106
|
+
new Uint8Array(
|
|
107
|
+
view.buffer,
|
|
108
|
+
view.byteOffset + cursor,
|
|
109
|
+
propertyByteLength
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
cursor += propertyByteLength
|
|
113
|
+
if (cursor < view.byteLength && view.getUint8(cursor) === 0) {
|
|
114
|
+
cursor += 1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (cursor + 4 > view.byteLength) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const authoredPointCount = view.getUint32(cursor, true)
|
|
122
|
+
cursor += 4
|
|
123
|
+
const pointCount = shapeBased
|
|
124
|
+
? authoredPointCount + 1
|
|
125
|
+
: authoredPointCount
|
|
126
|
+
const parsedPoints = shapeBased
|
|
127
|
+
? PcbRegionPrimitiveParser.#readShapeRegionVertices(
|
|
128
|
+
view,
|
|
129
|
+
cursor,
|
|
130
|
+
pointCount
|
|
131
|
+
)
|
|
132
|
+
: PcbRegionPrimitiveParser.#readRegionVertices(
|
|
133
|
+
view,
|
|
134
|
+
cursor,
|
|
135
|
+
pointCount
|
|
136
|
+
)
|
|
137
|
+
if (!parsedPoints) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
cursor = parsedPoints.offset
|
|
141
|
+
|
|
142
|
+
const holes = []
|
|
143
|
+
for (let holeIndex = 0; holeIndex < holeCount; holeIndex += 1) {
|
|
144
|
+
if (cursor + 4 > view.byteLength) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
const holeVertexCount = view.getUint32(cursor, true)
|
|
148
|
+
cursor += 4
|
|
149
|
+
const parsedHole = PcbRegionPrimitiveParser.#readRegionVertices(
|
|
150
|
+
view,
|
|
151
|
+
cursor,
|
|
152
|
+
holeVertexCount
|
|
153
|
+
)
|
|
154
|
+
if (!parsedHole) {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
holes.push(parsedHole.points)
|
|
158
|
+
cursor = parsedHole.offset
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const ownershipIndexes =
|
|
162
|
+
PcbPrimitiveOwnershipIndexParser.readOwnershipIndexes(view, {
|
|
163
|
+
component: 7,
|
|
164
|
+
net: 3,
|
|
165
|
+
polygon: 5
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
region: {
|
|
170
|
+
layerId,
|
|
171
|
+
layerCode: layerId,
|
|
172
|
+
...ownershipIndexes,
|
|
173
|
+
kind: Number(properties.KIND || 0),
|
|
174
|
+
isKeepout: flags2 === 2,
|
|
175
|
+
isBoardCutout:
|
|
176
|
+
String(properties.ISBOARDCUTOUT || '').toUpperCase() ===
|
|
177
|
+
'TRUE',
|
|
178
|
+
isShapeBased:
|
|
179
|
+
shapeBased ||
|
|
180
|
+
String(properties.ISSHAPEBASED || '').toUpperCase() ===
|
|
181
|
+
'TRUE',
|
|
182
|
+
points: parsedPoints.points,
|
|
183
|
+
holes,
|
|
184
|
+
properties
|
|
185
|
+
},
|
|
186
|
+
byteLength: 5 + payloadLength
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reads one simple double-coordinate region vertex list.
|
|
192
|
+
* @param {DataView} view
|
|
193
|
+
* @param {number} offset
|
|
194
|
+
* @param {number} count
|
|
195
|
+
* @returns {{ points: { x: number, y: number }[], offset: number } | null}
|
|
196
|
+
*/
|
|
197
|
+
static #readRegionVertices(view, offset, count) {
|
|
198
|
+
const byteLength =
|
|
199
|
+
count * PcbRegionPrimitiveParser.#REGION_VERTEX_BYTE_LENGTH
|
|
200
|
+
if (offset + byteLength > view.byteLength) {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const points = []
|
|
205
|
+
let cursor = offset
|
|
206
|
+
for (let index = 0; index < count; index += 1) {
|
|
207
|
+
points.push({
|
|
208
|
+
x: view.getFloat64(cursor, true) / 10000,
|
|
209
|
+
y: view.getFloat64(cursor + 8, true) / 10000
|
|
210
|
+
})
|
|
211
|
+
cursor += PcbRegionPrimitiveParser.#REGION_VERTEX_BYTE_LENGTH
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { points, offset: cursor }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Reads one shape-based region vertex list with optional arc metadata.
|
|
219
|
+
* @param {DataView} view
|
|
220
|
+
* @param {number} offset
|
|
221
|
+
* @param {number} count
|
|
222
|
+
* @returns {{ points: object[], offset: number } | null}
|
|
223
|
+
*/
|
|
224
|
+
static #readShapeRegionVertices(view, offset, count) {
|
|
225
|
+
const byteLength =
|
|
226
|
+
count * PcbRegionPrimitiveParser.#SHAPE_REGION_VERTEX_BYTE_LENGTH
|
|
227
|
+
if (offset + byteLength > view.byteLength) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const points = []
|
|
232
|
+
let cursor = offset
|
|
233
|
+
for (let index = 0; index < count; index += 1) {
|
|
234
|
+
const isArc = view.getUint8(cursor) !== 0
|
|
235
|
+
points.push({
|
|
236
|
+
x: view.getInt32(cursor + 1, true) / 10000,
|
|
237
|
+
y: view.getInt32(cursor + 5, true) / 10000,
|
|
238
|
+
isArc,
|
|
239
|
+
centerX: view.getInt32(cursor + 9, true) / 10000,
|
|
240
|
+
centerY: view.getInt32(cursor + 13, true) / 10000,
|
|
241
|
+
radius: view.getInt32(cursor + 17, true) / 10000,
|
|
242
|
+
startAngle: view.getFloat64(cursor + 21, true),
|
|
243
|
+
endAngle: view.getFloat64(cursor + 29, true)
|
|
244
|
+
})
|
|
245
|
+
cursor += PcbRegionPrimitiveParser.#SHAPE_REGION_VERTEX_BYTE_LENGTH
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { points, offset: cursor }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Parses Altium pipe-separated property bytes.
|
|
253
|
+
* @param {Uint8Array} bytes
|
|
254
|
+
* @returns {Record<string, string>}
|
|
255
|
+
*/
|
|
256
|
+
static #parsePropertyBytes(bytes) {
|
|
257
|
+
const text = new TextDecoder().decode(bytes).replace(/\u0000+$/u, '')
|
|
258
|
+
const properties = {}
|
|
259
|
+
|
|
260
|
+
for (const part of text.split('|')) {
|
|
261
|
+
const [key, ...valueParts] = part.split('=')
|
|
262
|
+
if (!key || !valueParts.length) {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
properties[key.trim()] = valueParts.join('=').trim()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return properties
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Normalizes one byte-like input into a Uint8Array view.
|
|
273
|
+
* @param {Uint8Array | ArrayBuffer} bytes
|
|
274
|
+
* @returns {Uint8Array}
|
|
275
|
+
*/
|
|
276
|
+
static #toUint8Array(bytes) {
|
|
277
|
+
if (bytes instanceof Uint8Array) {
|
|
278
|
+
return bytes
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return new Uint8Array(bytes)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reads one little-endian record count from a binary stream header.
|
|
286
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
287
|
+
* @returns {number}
|
|
288
|
+
*/
|
|
289
|
+
static #readRecordCount(headerBytes) {
|
|
290
|
+
const normalizedHeader =
|
|
291
|
+
PcbRegionPrimitiveParser.#toUint8Array(headerBytes)
|
|
292
|
+
|
|
293
|
+
if (normalizedHeader.byteLength < 4) {
|
|
294
|
+
return 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return new DataView(
|
|
298
|
+
normalizedHeader.buffer,
|
|
299
|
+
normalizedHeader.byteOffset,
|
|
300
|
+
4
|
|
301
|
+
).getUint32(0, true)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Reads one little-endian unsigned integer from a byte view.
|
|
306
|
+
* @param {Uint8Array} bytes
|
|
307
|
+
* @param {number} offset
|
|
308
|
+
* @returns {number}
|
|
309
|
+
*/
|
|
310
|
+
static #readUint32FromBytes(bytes, offset) {
|
|
311
|
+
return new DataView(
|
|
312
|
+
bytes.buffer,
|
|
313
|
+
bytes.byteOffset + offset,
|
|
314
|
+
4
|
|
315
|
+
).getUint32(0, true)
|
|
316
|
+
}
|
|
317
|
+
}
|