altium-toolkit 1.0.8 → 1.0.10
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/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -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 +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- 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/ParserCompatibilityFuzzer.mjs +192 -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 +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- 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 +541 -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 +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -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/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -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 +1252 -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,224 @@
|
|
|
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 {{ units: object, positionMode: string, entries: object[], modes: { componentOrigin: { units: object, positionMode: string, entries: object[] } } }}
|
|
19
|
+
*/
|
|
20
|
+
static buildModel(components, componentPrimitiveGroups, options = {}) {
|
|
21
|
+
const units = {
|
|
22
|
+
coordinate: 'mil',
|
|
23
|
+
angle: 'deg'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
units,
|
|
28
|
+
positionMode: DEFAULT_POSITION_MODE,
|
|
29
|
+
entries: PcbPickPlacePositionResolver.buildEntries(
|
|
30
|
+
components,
|
|
31
|
+
componentPrimitiveGroups,
|
|
32
|
+
DEFAULT_POSITION_MODE,
|
|
33
|
+
options
|
|
34
|
+
),
|
|
35
|
+
modes: {
|
|
36
|
+
componentOrigin: {
|
|
37
|
+
units,
|
|
38
|
+
positionMode: COMPONENT_ORIGIN_MODE,
|
|
39
|
+
entries: PcbPickPlacePositionResolver.buildEntries(
|
|
40
|
+
components,
|
|
41
|
+
componentPrimitiveGroups,
|
|
42
|
+
COMPONENT_ORIGIN_MODE,
|
|
43
|
+
options
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds PnP entries for one coordinate mode.
|
|
52
|
+
* @param {{ componentIndex: number, designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }[]} components
|
|
53
|
+
* @param {{ componentIndex: number, pads?: { x?: number, y?: number }[] }[]} componentPrimitiveGroups
|
|
54
|
+
* @param {'altium-pick-place' | 'component-origin' | string} mode
|
|
55
|
+
* @param {{ sourceComponents?: { componentIndex: number, rotation?: number }[] }} [options] Resolver options.
|
|
56
|
+
* @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }[]}
|
|
57
|
+
*/
|
|
58
|
+
static buildEntries(
|
|
59
|
+
components,
|
|
60
|
+
componentPrimitiveGroups,
|
|
61
|
+
mode,
|
|
62
|
+
options = {}
|
|
63
|
+
) {
|
|
64
|
+
const groupsByIndex = PcbPickPlacePositionResolver.#buildGroupLookup(
|
|
65
|
+
componentPrimitiveGroups
|
|
66
|
+
)
|
|
67
|
+
const sourceComponentsByIndex =
|
|
68
|
+
PcbPickPlacePositionResolver.#buildGroupLookup(
|
|
69
|
+
options.sourceComponents || []
|
|
70
|
+
)
|
|
71
|
+
const normalizedMode =
|
|
72
|
+
PcbPickPlacePositionResolver.normalizePositionMode(mode)
|
|
73
|
+
|
|
74
|
+
return (components || []).map((component) =>
|
|
75
|
+
PcbPickPlacePositionResolver.#buildEntry(
|
|
76
|
+
component,
|
|
77
|
+
groupsByIndex.get(Number(component.componentIndex)),
|
|
78
|
+
normalizedMode,
|
|
79
|
+
sourceComponentsByIndex.get(Number(component.componentIndex))
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalizes one public PnP coordinate-mode token.
|
|
86
|
+
* @param {string | null | undefined} mode
|
|
87
|
+
* @returns {'altium-pick-place' | 'component-origin'}
|
|
88
|
+
*/
|
|
89
|
+
static normalizePositionMode(mode) {
|
|
90
|
+
const normalized = String(mode || DEFAULT_POSITION_MODE)
|
|
91
|
+
.trim()
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/_/gu, '-')
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
normalized === '' ||
|
|
97
|
+
normalized === 'altium' ||
|
|
98
|
+
normalized === 'altium-pick-place' ||
|
|
99
|
+
normalized === 'pick-place'
|
|
100
|
+
) {
|
|
101
|
+
return DEFAULT_POSITION_MODE
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
normalized === 'component-origin' ||
|
|
106
|
+
normalized === 'origin' ||
|
|
107
|
+
normalized === 'part-origin' ||
|
|
108
|
+
normalized === 'footprint-origin'
|
|
109
|
+
) {
|
|
110
|
+
return COMPONENT_ORIGIN_MODE
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return DEFAULT_POSITION_MODE
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds one PnP entry.
|
|
118
|
+
* @param {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }} component
|
|
119
|
+
* @param {{ pads?: { x?: number, y?: number }[] } | undefined} group
|
|
120
|
+
* @param {'altium-pick-place' | 'component-origin'} mode
|
|
121
|
+
* @param {{ rotation?: number } | undefined} sourceComponent Source component row.
|
|
122
|
+
* @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }}
|
|
123
|
+
*/
|
|
124
|
+
static #buildEntry(component, group, mode, sourceComponent) {
|
|
125
|
+
const componentOriginX = Number(component.x || 0)
|
|
126
|
+
const componentOriginY = Number(component.y || 0)
|
|
127
|
+
const rotation = Number.isFinite(Number(sourceComponent?.rotation))
|
|
128
|
+
? Number(sourceComponent.rotation)
|
|
129
|
+
: Number(component.rotation || 0)
|
|
130
|
+
const padAnchors = PcbPickPlacePositionResolver.#padAnchors(group?.pads)
|
|
131
|
+
const padCenter =
|
|
132
|
+
mode === DEFAULT_POSITION_MODE
|
|
133
|
+
? PcbPickPlacePositionResolver.#padAnchorBoundsCenter(
|
|
134
|
+
padAnchors
|
|
135
|
+
)
|
|
136
|
+
: null
|
|
137
|
+
const position = padCenter || {
|
|
138
|
+
x: componentOriginX,
|
|
139
|
+
y: componentOriginY,
|
|
140
|
+
source: 'component-origin'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
designator: component.designator || '',
|
|
145
|
+
pattern: component.pattern || '',
|
|
146
|
+
layer: component.layer || '',
|
|
147
|
+
rotation: PcbPickPlacePositionResolver.#roundCoordinate(rotation),
|
|
148
|
+
x: PcbPickPlacePositionResolver.#roundCoordinate(position.x),
|
|
149
|
+
y: PcbPickPlacePositionResolver.#roundCoordinate(position.y),
|
|
150
|
+
componentOriginX:
|
|
151
|
+
PcbPickPlacePositionResolver.#roundCoordinate(componentOriginX),
|
|
152
|
+
componentOriginY:
|
|
153
|
+
PcbPickPlacePositionResolver.#roundCoordinate(componentOriginY),
|
|
154
|
+
padAnchorCount: padAnchors.length,
|
|
155
|
+
positionSource:
|
|
156
|
+
mode === COMPONENT_ORIGIN_MODE
|
|
157
|
+
? 'component-origin'
|
|
158
|
+
: position.source
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds a lookup of component primitive groups by native component index.
|
|
164
|
+
* @param {{ componentIndex: number }[]} componentPrimitiveGroups
|
|
165
|
+
* @returns {Map<number, object>}
|
|
166
|
+
*/
|
|
167
|
+
static #buildGroupLookup(componentPrimitiveGroups) {
|
|
168
|
+
const groupsByIndex = new Map()
|
|
169
|
+
|
|
170
|
+
for (const group of componentPrimitiveGroups || []) {
|
|
171
|
+
const componentIndex = Number(group?.componentIndex)
|
|
172
|
+
if (Number.isInteger(componentIndex)) {
|
|
173
|
+
groupsByIndex.set(componentIndex, group)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return groupsByIndex
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns pad anchor points with finite coordinates.
|
|
182
|
+
* @param {{ x?: number, y?: number }[] | undefined} pads
|
|
183
|
+
* @returns {{ x: number, y: number }[]}
|
|
184
|
+
*/
|
|
185
|
+
static #padAnchors(pads) {
|
|
186
|
+
return (pads || [])
|
|
187
|
+
.map((pad) => ({
|
|
188
|
+
x: Number(pad?.x),
|
|
189
|
+
y: Number(pad?.y)
|
|
190
|
+
}))
|
|
191
|
+
.filter(
|
|
192
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves the center of the owned pad-anchor bounds.
|
|
198
|
+
* @param {{ x: number, y: number }[]} padAnchors
|
|
199
|
+
* @returns {{ x: number, y: number, source: string } | null}
|
|
200
|
+
*/
|
|
201
|
+
static #padAnchorBoundsCenter(padAnchors) {
|
|
202
|
+
if (!padAnchors.length) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const xs = padAnchors.map((point) => point.x)
|
|
207
|
+
const ys = padAnchors.map((point) => point.y)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
211
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
|
212
|
+
source: 'pad-anchor-bounds'
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Rounds one PnP coordinate for deterministic JSON output.
|
|
218
|
+
* @param {number} value
|
|
219
|
+
* @returns {number}
|
|
220
|
+
*/
|
|
221
|
+
static #roundCoordinate(value) {
|
|
222
|
+
return Number(Number(value || 0).toFixed(6))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -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.
|
|
@@ -164,17 +165,26 @@ export class PcbRegionPrimitiveParser {
|
|
|
164
165
|
net: 3,
|
|
165
166
|
polygon: 5
|
|
166
167
|
})
|
|
168
|
+
const kind = PcbRegionPrimitiveParser.#numericKind(properties.KIND)
|
|
169
|
+
const legacyCutout =
|
|
170
|
+
PcbRegionPrimitiveParser.#legacyCutoutClassification(
|
|
171
|
+
properties,
|
|
172
|
+
kind
|
|
173
|
+
)
|
|
167
174
|
|
|
168
175
|
return {
|
|
169
176
|
region: {
|
|
170
177
|
layerId,
|
|
171
178
|
layerCode: layerId,
|
|
172
179
|
...ownershipIndexes,
|
|
173
|
-
kind
|
|
180
|
+
kind,
|
|
181
|
+
...legacyCutout.fields,
|
|
174
182
|
isKeepout: flags2 === 2,
|
|
175
183
|
isBoardCutout:
|
|
184
|
+
legacyCutout.isBoardCutout ||
|
|
176
185
|
String(properties.ISBOARDCUTOUT || '').toUpperCase() ===
|
|
177
|
-
|
|
186
|
+
'TRUE',
|
|
187
|
+
...legacyCutout.cutoutFlags,
|
|
178
188
|
isShapeBased:
|
|
179
189
|
shapeBased ||
|
|
180
190
|
String(properties.ISSHAPEBASED || '').toUpperCase() ===
|
|
@@ -187,6 +197,66 @@ export class PcbRegionPrimitiveParser {
|
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Parses a region kind while avoiding NaN for legacy symbolic labels.
|
|
202
|
+
* @param {string | undefined} rawKind Raw KIND value.
|
|
203
|
+
* @returns {number | null}
|
|
204
|
+
*/
|
|
205
|
+
static #numericKind(rawKind) {
|
|
206
|
+
if (rawKind === undefined || rawKind === null || rawKind === '') {
|
|
207
|
+
return 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const kind = Number(rawKind)
|
|
211
|
+
return Number.isFinite(kind) ? kind : null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Builds cutout fields from legacy string KIND labels.
|
|
216
|
+
* @param {Record<string, string>} properties Native property map.
|
|
217
|
+
* @param {number | null} numericKind Parsed numeric kind.
|
|
218
|
+
* @returns {{ isBoardCutout: boolean, fields: object, cutoutFlags: object }}
|
|
219
|
+
*/
|
|
220
|
+
static #legacyCutoutClassification(properties, numericKind) {
|
|
221
|
+
const rawKind = String(properties.KIND || '').trim()
|
|
222
|
+
if (numericKind !== null || !rawKind) {
|
|
223
|
+
return {
|
|
224
|
+
isBoardCutout: false,
|
|
225
|
+
fields: {},
|
|
226
|
+
cutoutFlags: {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalized = rawKind.replace(/[^a-z0-9]/giu, '').toLowerCase()
|
|
231
|
+
const isBoardCutout = normalized === 'boardcutout'
|
|
232
|
+
const isPolygonPourCutout =
|
|
233
|
+
normalized === 'polygonpourcutout' ||
|
|
234
|
+
normalized === 'polygoncutout' ||
|
|
235
|
+
normalized === 'pourcutout'
|
|
236
|
+
const classification =
|
|
237
|
+
isBoardCutout || isPolygonPourCutout
|
|
238
|
+
? {
|
|
239
|
+
isBoardCutout,
|
|
240
|
+
isPolygonPourCutout,
|
|
241
|
+
source: 'legacy-kind',
|
|
242
|
+
rawKind
|
|
243
|
+
}
|
|
244
|
+
: null
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
isBoardCutout,
|
|
248
|
+
fields: {
|
|
249
|
+
rawKind
|
|
250
|
+
},
|
|
251
|
+
cutoutFlags: classification
|
|
252
|
+
? {
|
|
253
|
+
isPolygonPourCutout,
|
|
254
|
+
cutoutClassification: classification
|
|
255
|
+
}
|
|
256
|
+
: {}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
190
260
|
/**
|
|
191
261
|
* Reads one simple double-coordinate region vertex list.
|
|
192
262
|
* @param {DataView} view
|
|
@@ -254,7 +324,10 @@ export class PcbRegionPrimitiveParser {
|
|
|
254
324
|
* @returns {Record<string, string>}
|
|
255
325
|
*/
|
|
256
326
|
static #parsePropertyBytes(bytes) {
|
|
257
|
-
const text =
|
|
327
|
+
const text = PrintableTextDecoder.decodeBytes(bytes).replace(
|
|
328
|
+
/\u0000+$/u,
|
|
329
|
+
''
|
|
330
|
+
)
|
|
258
331
|
const properties = {}
|
|
259
332
|
|
|
260
333
|
for (const part of text.split('|')) {
|