altium-toolkit 1.0.8 → 1.0.9
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 +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
const DEFAULT_POSITION_MODE = 'altium-pick-place'
|
|
6
|
+
const COMPONENT_ORIGIN_MODE = 'component-origin'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves PCB pick-and-place coordinates from component origins and owned pad
|
|
10
|
+
* anchors.
|
|
11
|
+
*/
|
|
12
|
+
export class PcbPickPlacePositionResolver {
|
|
13
|
+
/**
|
|
14
|
+
* Builds the public PnP model with the default mode and alternatives.
|
|
15
|
+
* @param {{ componentIndex: number, designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }[]} components
|
|
16
|
+
* @param {{ componentIndex: number, pads?: { x?: number, y?: number }[] }[]} componentPrimitiveGroups
|
|
17
|
+
* @param {{ sourceComponents?: { componentIndex: number, rotation?: number }[] }} [options] Resolver options.
|
|
18
|
+
* @returns {{ positionMode: string, entries: object[], modes: { componentOrigin: { positionMode: string, entries: object[] } } }}
|
|
19
|
+
*/
|
|
20
|
+
static buildModel(components, componentPrimitiveGroups, options = {}) {
|
|
21
|
+
return {
|
|
22
|
+
positionMode: DEFAULT_POSITION_MODE,
|
|
23
|
+
entries: PcbPickPlacePositionResolver.buildEntries(
|
|
24
|
+
components,
|
|
25
|
+
componentPrimitiveGroups,
|
|
26
|
+
DEFAULT_POSITION_MODE,
|
|
27
|
+
options
|
|
28
|
+
),
|
|
29
|
+
modes: {
|
|
30
|
+
componentOrigin: {
|
|
31
|
+
positionMode: COMPONENT_ORIGIN_MODE,
|
|
32
|
+
entries: PcbPickPlacePositionResolver.buildEntries(
|
|
33
|
+
components,
|
|
34
|
+
componentPrimitiveGroups,
|
|
35
|
+
COMPONENT_ORIGIN_MODE,
|
|
36
|
+
options
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds PnP entries for one coordinate mode.
|
|
45
|
+
* @param {{ componentIndex: number, designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }[]} components
|
|
46
|
+
* @param {{ componentIndex: number, pads?: { x?: number, y?: number }[] }[]} componentPrimitiveGroups
|
|
47
|
+
* @param {'altium-pick-place' | 'component-origin' | string} mode
|
|
48
|
+
* @param {{ sourceComponents?: { componentIndex: number, rotation?: number }[] }} [options] Resolver options.
|
|
49
|
+
* @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }[]}
|
|
50
|
+
*/
|
|
51
|
+
static buildEntries(
|
|
52
|
+
components,
|
|
53
|
+
componentPrimitiveGroups,
|
|
54
|
+
mode,
|
|
55
|
+
options = {}
|
|
56
|
+
) {
|
|
57
|
+
const groupsByIndex = PcbPickPlacePositionResolver.#buildGroupLookup(
|
|
58
|
+
componentPrimitiveGroups
|
|
59
|
+
)
|
|
60
|
+
const sourceComponentsByIndex =
|
|
61
|
+
PcbPickPlacePositionResolver.#buildGroupLookup(
|
|
62
|
+
options.sourceComponents || []
|
|
63
|
+
)
|
|
64
|
+
const normalizedMode =
|
|
65
|
+
PcbPickPlacePositionResolver.normalizePositionMode(mode)
|
|
66
|
+
|
|
67
|
+
return (components || []).map((component) =>
|
|
68
|
+
PcbPickPlacePositionResolver.#buildEntry(
|
|
69
|
+
component,
|
|
70
|
+
groupsByIndex.get(Number(component.componentIndex)),
|
|
71
|
+
normalizedMode,
|
|
72
|
+
sourceComponentsByIndex.get(Number(component.componentIndex))
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes one public PnP coordinate-mode token.
|
|
79
|
+
* @param {string | null | undefined} mode
|
|
80
|
+
* @returns {'altium-pick-place' | 'component-origin'}
|
|
81
|
+
*/
|
|
82
|
+
static normalizePositionMode(mode) {
|
|
83
|
+
const normalized = String(mode || DEFAULT_POSITION_MODE)
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/_/gu, '-')
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
normalized === '' ||
|
|
90
|
+
normalized === 'altium' ||
|
|
91
|
+
normalized === 'altium-pick-place' ||
|
|
92
|
+
normalized === 'pick-place'
|
|
93
|
+
) {
|
|
94
|
+
return DEFAULT_POSITION_MODE
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
normalized === 'component-origin' ||
|
|
99
|
+
normalized === 'origin' ||
|
|
100
|
+
normalized === 'part-origin' ||
|
|
101
|
+
normalized === 'footprint-origin'
|
|
102
|
+
) {
|
|
103
|
+
return COMPONENT_ORIGIN_MODE
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return DEFAULT_POSITION_MODE
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds one PnP entry.
|
|
111
|
+
* @param {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }} component
|
|
112
|
+
* @param {{ pads?: { x?: number, y?: number }[] } | undefined} group
|
|
113
|
+
* @param {'altium-pick-place' | 'component-origin'} mode
|
|
114
|
+
* @param {{ rotation?: number } | undefined} sourceComponent Source component row.
|
|
115
|
+
* @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }}
|
|
116
|
+
*/
|
|
117
|
+
static #buildEntry(component, group, mode, sourceComponent) {
|
|
118
|
+
const componentOriginX = Number(component.x || 0)
|
|
119
|
+
const componentOriginY = Number(component.y || 0)
|
|
120
|
+
const rotation = Number.isFinite(Number(sourceComponent?.rotation))
|
|
121
|
+
? Number(sourceComponent.rotation)
|
|
122
|
+
: Number(component.rotation || 0)
|
|
123
|
+
const padAnchors = PcbPickPlacePositionResolver.#padAnchors(group?.pads)
|
|
124
|
+
const padCenter =
|
|
125
|
+
mode === DEFAULT_POSITION_MODE
|
|
126
|
+
? PcbPickPlacePositionResolver.#padAnchorBoundsCenter(
|
|
127
|
+
padAnchors
|
|
128
|
+
)
|
|
129
|
+
: null
|
|
130
|
+
const position = padCenter || {
|
|
131
|
+
x: componentOriginX,
|
|
132
|
+
y: componentOriginY,
|
|
133
|
+
source: 'component-origin'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
designator: component.designator || '',
|
|
138
|
+
pattern: component.pattern || '',
|
|
139
|
+
layer: component.layer || '',
|
|
140
|
+
rotation: PcbPickPlacePositionResolver.#roundCoordinate(rotation),
|
|
141
|
+
x: PcbPickPlacePositionResolver.#roundCoordinate(position.x),
|
|
142
|
+
y: PcbPickPlacePositionResolver.#roundCoordinate(position.y),
|
|
143
|
+
componentOriginX:
|
|
144
|
+
PcbPickPlacePositionResolver.#roundCoordinate(componentOriginX),
|
|
145
|
+
componentOriginY:
|
|
146
|
+
PcbPickPlacePositionResolver.#roundCoordinate(componentOriginY),
|
|
147
|
+
padAnchorCount: padAnchors.length,
|
|
148
|
+
positionSource:
|
|
149
|
+
mode === COMPONENT_ORIGIN_MODE
|
|
150
|
+
? 'component-origin'
|
|
151
|
+
: position.source
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds a lookup of component primitive groups by native component index.
|
|
157
|
+
* @param {{ componentIndex: number }[]} componentPrimitiveGroups
|
|
158
|
+
* @returns {Map<number, object>}
|
|
159
|
+
*/
|
|
160
|
+
static #buildGroupLookup(componentPrimitiveGroups) {
|
|
161
|
+
const groupsByIndex = new Map()
|
|
162
|
+
|
|
163
|
+
for (const group of componentPrimitiveGroups || []) {
|
|
164
|
+
const componentIndex = Number(group?.componentIndex)
|
|
165
|
+
if (Number.isInteger(componentIndex)) {
|
|
166
|
+
groupsByIndex.set(componentIndex, group)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return groupsByIndex
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Returns pad anchor points with finite coordinates.
|
|
175
|
+
* @param {{ x?: number, y?: number }[] | undefined} pads
|
|
176
|
+
* @returns {{ x: number, y: number }[]}
|
|
177
|
+
*/
|
|
178
|
+
static #padAnchors(pads) {
|
|
179
|
+
return (pads || [])
|
|
180
|
+
.map((pad) => ({
|
|
181
|
+
x: Number(pad?.x),
|
|
182
|
+
y: Number(pad?.y)
|
|
183
|
+
}))
|
|
184
|
+
.filter(
|
|
185
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolves the center of the owned pad-anchor bounds.
|
|
191
|
+
* @param {{ x: number, y: number }[]} padAnchors
|
|
192
|
+
* @returns {{ x: number, y: number, source: string } | null}
|
|
193
|
+
*/
|
|
194
|
+
static #padAnchorBoundsCenter(padAnchors) {
|
|
195
|
+
if (!padAnchors.length) {
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const xs = padAnchors.map((point) => point.x)
|
|
200
|
+
const ys = padAnchors.map((point) => point.y)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
204
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
|
205
|
+
source: 'pad-anchor-bounds'
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Rounds one PnP coordinate for deterministic JSON output.
|
|
211
|
+
* @param {number} value
|
|
212
|
+
* @returns {number}
|
|
213
|
+
*/
|
|
214
|
+
static #roundCoordinate(value) {
|
|
215
|
+
return Number(Number(value || 0).toFixed(6))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Decodes Altium PrimitiveParameters/Data sidecar records.
|
|
7
9
|
*/
|
|
@@ -153,8 +155,7 @@ export class PcbPrimitiveParameterParser {
|
|
|
153
155
|
* @returns {Record<string, string>}
|
|
154
156
|
*/
|
|
155
157
|
static #parseRecordFields(bytes) {
|
|
156
|
-
const text =
|
|
157
|
-
.decode(bytes)
|
|
158
|
+
const text = PrintableTextDecoder.decodeBytes(bytes)
|
|
158
159
|
.replace(/\u0000/gu, '')
|
|
159
160
|
.replace(/\r\n?/gu, '\n')
|
|
160
161
|
.trim()
|
|
@@ -365,82 +365,149 @@ export class PcbRawRecordRegistry {
|
|
|
365
365
|
static #sliceSubrecordListRecords(descriptor, headerBytes, dataBytes) {
|
|
366
366
|
const count = PcbRawRecordRegistry.#readRecordCount(headerBytes)
|
|
367
367
|
const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
|
|
368
|
-
const
|
|
369
|
-
|
|
368
|
+
const boundaries = PcbRawRecordRegistry.#findSubrecordListBoundaries(
|
|
369
|
+
bytes,
|
|
370
|
+
0,
|
|
371
|
+
descriptor,
|
|
372
|
+
count
|
|
373
|
+
)
|
|
370
374
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
375
|
+
if (!boundaries) {
|
|
376
|
+
return []
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return boundaries.map((boundary, index) => {
|
|
380
|
+
const endOffset = boundaries[index + 1]?.offset ?? bytes.byteLength
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
recordBytes: bytes.slice(boundary.offset, endOffset),
|
|
384
|
+
offset: boundary.offset,
|
|
385
|
+
byteLength: endOffset - boundary.offset,
|
|
386
|
+
payloadByteLength: null,
|
|
387
|
+
encoding: 'subrecord-list',
|
|
388
|
+
objectId: descriptor.typeId,
|
|
389
|
+
recordIndex: index
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Finds all subrecord-list record boundaries without recursive validation.
|
|
396
|
+
* @param {Uint8Array} bytes
|
|
397
|
+
* @param {number} offset
|
|
398
|
+
* @param {object} descriptor
|
|
399
|
+
* @param {number} count
|
|
400
|
+
* @returns {{ offset: number, minimumEnd: number }[] | null}
|
|
401
|
+
*/
|
|
402
|
+
static #findSubrecordListBoundaries(bytes, offset, descriptor, count) {
|
|
403
|
+
if (!count) {
|
|
404
|
+
return []
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const firstMinimumEnd =
|
|
408
|
+
PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
|
|
374
409
|
bytes,
|
|
375
410
|
offset,
|
|
376
|
-
descriptor
|
|
377
|
-
remainingCount
|
|
411
|
+
descriptor
|
|
378
412
|
)
|
|
379
413
|
|
|
380
|
-
|
|
381
|
-
|
|
414
|
+
if (!firstMinimumEnd) {
|
|
415
|
+
return null
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const boundaries = [{ offset, minimumEnd: firstMinimumEnd }]
|
|
419
|
+
const alternativeScanOffsets = [null]
|
|
420
|
+
let depth = 1
|
|
421
|
+
let scanOffset = firstMinimumEnd
|
|
422
|
+
|
|
423
|
+
while (depth < count) {
|
|
424
|
+
const candidate =
|
|
425
|
+
PcbRawRecordRegistry.#findNextSubrecordListCandidate(
|
|
426
|
+
bytes,
|
|
427
|
+
scanOffset,
|
|
428
|
+
descriptor
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if (!candidate) {
|
|
432
|
+
let foundAlternative = false
|
|
433
|
+
|
|
434
|
+
while (depth > 1 && !foundAlternative) {
|
|
435
|
+
depth -= 1
|
|
436
|
+
boundaries.length = depth
|
|
437
|
+
|
|
438
|
+
const alternativeOffset = alternativeScanOffsets[depth]
|
|
439
|
+
alternativeScanOffsets.length = depth
|
|
440
|
+
|
|
441
|
+
if (alternativeOffset !== null) {
|
|
442
|
+
scanOffset = alternativeOffset
|
|
443
|
+
foundAlternative = true
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!foundAlternative) {
|
|
448
|
+
return null
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
continue
|
|
382
452
|
}
|
|
383
453
|
|
|
384
|
-
|
|
385
|
-
|
|
454
|
+
boundaries[depth] = {
|
|
455
|
+
offset: candidate.offset,
|
|
456
|
+
minimumEnd: candidate.minimumEnd
|
|
457
|
+
}
|
|
458
|
+
alternativeScanOffsets[depth] = candidate.alternativeOffset
|
|
459
|
+
depth += 1
|
|
460
|
+
scanOffset = candidate.minimumEnd
|
|
386
461
|
}
|
|
387
462
|
|
|
388
|
-
return
|
|
463
|
+
return boundaries
|
|
389
464
|
}
|
|
390
465
|
|
|
391
466
|
/**
|
|
392
|
-
*
|
|
467
|
+
* Finds the next plausible subrecord-list record boundary.
|
|
393
468
|
* @param {Uint8Array} bytes
|
|
394
469
|
* @param {number} offset
|
|
395
470
|
* @param {object} descriptor
|
|
396
|
-
* @
|
|
397
|
-
* @returns {object | null}
|
|
471
|
+
* @returns {{ offset: number, minimumEnd: number, alternativeOffset: number | null } | null}
|
|
398
472
|
*/
|
|
399
|
-
static #
|
|
400
|
-
|
|
401
|
-
offset,
|
|
402
|
-
descriptor,
|
|
403
|
-
remainingCount
|
|
404
|
-
) {
|
|
405
|
-
if (
|
|
406
|
-
offset + 1 > bytes.byteLength ||
|
|
407
|
-
bytes[offset] !== descriptor.typeId
|
|
408
|
-
) {
|
|
409
|
-
return null
|
|
410
|
-
}
|
|
473
|
+
static #findNextSubrecordListCandidate(bytes, offset, descriptor) {
|
|
474
|
+
let cursor = offset
|
|
411
475
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
476
|
+
while (cursor < bytes.byteLength) {
|
|
477
|
+
const minimumEnd =
|
|
478
|
+
PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
|
|
479
|
+
bytes,
|
|
480
|
+
cursor,
|
|
481
|
+
descriptor
|
|
482
|
+
)
|
|
483
|
+
const unknownSubrecord = PcbRawRecordRegistry.#readSubrecordAt(
|
|
484
|
+
bytes,
|
|
485
|
+
cursor
|
|
486
|
+
)
|
|
417
487
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
488
|
+
if (minimumEnd) {
|
|
489
|
+
const alternativeOffset =
|
|
490
|
+
unknownSubrecord?.nextOffset ?? cursor + 1
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
offset: cursor,
|
|
494
|
+
minimumEnd,
|
|
495
|
+
alternativeOffset:
|
|
496
|
+
alternativeOffset < bytes.byteLength
|
|
497
|
+
? alternativeOffset
|
|
498
|
+
: null
|
|
499
|
+
}
|
|
500
|
+
}
|
|
421
501
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
descriptor,
|
|
427
|
-
remainingCount
|
|
428
|
-
)
|
|
429
|
-
: bytes.byteLength
|
|
502
|
+
if (!unknownSubrecord) {
|
|
503
|
+
cursor += 1
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
430
506
|
|
|
431
|
-
|
|
432
|
-
return null
|
|
507
|
+
cursor = unknownSubrecord.nextOffset
|
|
433
508
|
}
|
|
434
509
|
|
|
435
|
-
return
|
|
436
|
-
recordBytes: bytes.slice(offset, endOffset),
|
|
437
|
-
offset,
|
|
438
|
-
byteLength: endOffset - offset,
|
|
439
|
-
payloadByteLength: null,
|
|
440
|
-
encoding: 'subrecord-list',
|
|
441
|
-
objectId: descriptor.typeId,
|
|
442
|
-
recordIndex: 0
|
|
443
|
-
}
|
|
510
|
+
return null
|
|
444
511
|
}
|
|
445
512
|
|
|
446
513
|
/**
|
|
@@ -484,82 +551,6 @@ export class PcbRawRecordRegistry {
|
|
|
484
551
|
return cursor
|
|
485
552
|
}
|
|
486
553
|
|
|
487
|
-
/**
|
|
488
|
-
* Finds the next known subrecord-list primitive boundary.
|
|
489
|
-
* @param {Uint8Array} bytes
|
|
490
|
-
* @param {number} offset
|
|
491
|
-
* @param {object} descriptor
|
|
492
|
-
* @param {number} remainingCount
|
|
493
|
-
* @returns {number | null}
|
|
494
|
-
*/
|
|
495
|
-
static #findNextSubrecordListRecordOffset(
|
|
496
|
-
bytes,
|
|
497
|
-
offset,
|
|
498
|
-
descriptor,
|
|
499
|
-
remainingCount
|
|
500
|
-
) {
|
|
501
|
-
let cursor = offset
|
|
502
|
-
|
|
503
|
-
while (cursor < bytes.byteLength) {
|
|
504
|
-
if (
|
|
505
|
-
PcbRawRecordRegistry.#canReadSubrecordListSequence(
|
|
506
|
-
bytes,
|
|
507
|
-
cursor,
|
|
508
|
-
descriptor,
|
|
509
|
-
remainingCount
|
|
510
|
-
)
|
|
511
|
-
) {
|
|
512
|
-
return cursor
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const unknownSubrecord = PcbRawRecordRegistry.#readSubrecordAt(
|
|
516
|
-
bytes,
|
|
517
|
-
cursor
|
|
518
|
-
)
|
|
519
|
-
cursor = unknownSubrecord ? unknownSubrecord.nextOffset : cursor + 1
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return null
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Checks whether the remaining subrecord-list records are readable.
|
|
527
|
-
* @param {Uint8Array} bytes
|
|
528
|
-
* @param {number} offset
|
|
529
|
-
* @param {object} descriptor
|
|
530
|
-
* @param {number} remainingCount
|
|
531
|
-
* @returns {boolean}
|
|
532
|
-
*/
|
|
533
|
-
static #canReadSubrecordListSequence(
|
|
534
|
-
bytes,
|
|
535
|
-
offset,
|
|
536
|
-
descriptor,
|
|
537
|
-
remainingCount
|
|
538
|
-
) {
|
|
539
|
-
const minimumEnd = PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
|
|
540
|
-
bytes,
|
|
541
|
-
offset,
|
|
542
|
-
descriptor
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
if (!minimumEnd) {
|
|
546
|
-
return false
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (remainingCount <= 1) {
|
|
550
|
-
return true
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return (
|
|
554
|
-
PcbRawRecordRegistry.#findNextSubrecordListRecordOffset(
|
|
555
|
-
bytes,
|
|
556
|
-
minimumEnd,
|
|
557
|
-
descriptor,
|
|
558
|
-
remainingCount - 1
|
|
559
|
-
) !== null
|
|
560
|
-
)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
554
|
/**
|
|
564
555
|
* Slices PCB text records including their variable string tails.
|
|
565
556
|
* @param {object} descriptor
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
|
|
6
|
+
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Decodes Altium PCB region primitive streams.
|
|
@@ -254,7 +255,10 @@ export class PcbRegionPrimitiveParser {
|
|
|
254
255
|
* @returns {Record<string, string>}
|
|
255
256
|
*/
|
|
256
257
|
static #parsePropertyBytes(bytes) {
|
|
257
|
-
const text =
|
|
258
|
+
const text = PrintableTextDecoder.decodeBytes(bytes).replace(
|
|
259
|
+
/\u0000+$/u,
|
|
260
|
+
''
|
|
261
|
+
)
|
|
258
262
|
const properties = {}
|
|
259
263
|
|
|
260
264
|
for (const part of text.split('|')) {
|