altium-toolkit 0.1.1 → 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.
Files changed (54) hide show
  1. package/README.md +24 -6
  2. package/docs/api.md +42 -4
  3. package/docs/model-format.md +95 -5
  4. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
  5. package/docs/testing.md +7 -2
  6. package/package.json +6 -2
  7. package/spec/library-scope.md +7 -1
  8. package/src/core/altium/AltiumParser.mjs +22 -325
  9. package/src/core/altium/NormalizedModelSchema.mjs +28 -0
  10. package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
  11. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
  12. package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
  13. package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
  14. package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
  15. package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
  16. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
  17. package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
  18. package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
  19. package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
  20. package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
  21. package/src/core/altium/PcbLibModelParser.mjs +202 -0
  22. package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
  23. package/src/core/altium/PcbModelParser.mjs +618 -66
  24. package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
  25. package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
  26. package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
  27. package/src/core/altium/PcbPadStackParser.mjs +903 -0
  28. package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
  29. package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
  30. package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
  31. package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
  32. package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
  33. package/src/core/altium/PcbRuleParser.mjs +587 -0
  34. package/src/core/altium/PcbStreamExtractor.mjs +127 -4
  35. package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
  36. package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
  37. package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
  38. package/src/core/altium/PcbViaStackParser.mjs +548 -0
  39. package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
  40. package/src/core/altium/PrjPcbModelParser.mjs +797 -0
  41. package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
  42. package/src/parser.mjs +13 -0
  43. package/src/renderers.mjs +5 -0
  44. package/src/styles/altium-renderers.css +11 -6
  45. package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
  46. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
  47. package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
  48. package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
  49. package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
  50. package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
  51. package/src/ui/PcbSvgRenderer.mjs +101 -109
  52. package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
  53. package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
  54. 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
+ }