altium-toolkit 1.1.24 → 1.1.26
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 +5 -3
- package/docs/api.md +43 -17
- package/docs/model-format.md +51 -11
- package/docs/schemas/altium_toolkit/library_compatibility_a1.schema.json +116 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/spec/library-compatibility.md +65 -0
- package/src/core/altium/AltiumParser.mjs +8 -1
- package/src/core/altium/LibraryCompatibilityGeometry.mjs +618 -0
- package/src/core/altium/LibraryCompatibilityModelHintBuilder.mjs +296 -0
- package/src/core/altium/LibraryCompatibilityReportBuilder.mjs +930 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +29 -1
- package/src/core/altium/PcbDimensionParser.mjs +1 -0
- package/src/core/altium/PcbDimensionReportBuilder.mjs +190 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +140 -10
- package/src/core/altium/PcbFabricationReadinessReportBuilder.mjs +607 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +56 -3
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +138 -3
- package/src/core/altium/PcbLibModelParser.mjs +24 -2
- package/src/core/altium/PcbLibStorageNameResolver.mjs +171 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +15 -93
- package/src/core/altium/PcbPrimitiveFieldOffsetReportBuilder.mjs +352 -0
- package/src/core/altium/PcbRuleImpactReportBuilder.mjs +313 -0
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +514 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +442 -2
- package/src/core/altium/SchematicImageDiagnosticsBuilder.mjs +214 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +523 -3
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +36 -0
- package/src/ui/PcbDimensionPrimitiveRenderer.mjs +422 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +6 -0
- package/src/ui/PcbScene3dBuilder.mjs +10 -2
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +225 -0
- package/src/ui/PcbSvgRenderer.mjs +78 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { LibraryCompatibilityReportBuilder } from './LibraryCompatibilityReportBuilder.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Builds deterministic QA reports across parsed schematic and PCB libraries.
|
|
7
9
|
*/
|
|
@@ -33,6 +35,10 @@ export class LibraryQaReportBuilder {
|
|
|
33
35
|
schematicLibraries,
|
|
34
36
|
pcbLibraries
|
|
35
37
|
)
|
|
38
|
+
const compatibility = LibraryCompatibilityReportBuilder.build({
|
|
39
|
+
schematicLibraries,
|
|
40
|
+
pcbLibraries
|
|
41
|
+
})
|
|
36
42
|
const mergePlan =
|
|
37
43
|
LibraryQaReportBuilder.#schematicLibraryMergePlan(
|
|
38
44
|
schematicLibraries
|
|
@@ -74,7 +80,8 @@ export class LibraryQaReportBuilder {
|
|
|
74
80
|
diagnostic.symbolName
|
|
75
81
|
)
|
|
76
82
|
),
|
|
77
|
-
...libraryLint.issues
|
|
83
|
+
...libraryLint.issues,
|
|
84
|
+
...(compatibility.issues || [])
|
|
78
85
|
]
|
|
79
86
|
const issuesBySeverity =
|
|
80
87
|
LibraryQaReportBuilder.#issueSeverityCounts(issues)
|
|
@@ -91,6 +98,7 @@ export class LibraryQaReportBuilder {
|
|
|
91
98
|
multipartMismatchCount: multipartMismatches.length,
|
|
92
99
|
mergePlanConflictCount: mergePlan.summary.conflictCount,
|
|
93
100
|
libraryLintIssueCount: libraryLint.summary.issueCount,
|
|
101
|
+
...LibraryQaReportBuilder.#compatibilitySummary(compatibility),
|
|
94
102
|
issuesBySeverity,
|
|
95
103
|
issueCount: issues.length
|
|
96
104
|
},
|
|
@@ -102,6 +110,7 @@ export class LibraryQaReportBuilder {
|
|
|
102
110
|
missingModels,
|
|
103
111
|
multipartMismatches,
|
|
104
112
|
libraryLint,
|
|
113
|
+
compatibility,
|
|
105
114
|
mergePlan,
|
|
106
115
|
issues
|
|
107
116
|
}
|
|
@@ -680,6 +689,25 @@ export class LibraryQaReportBuilder {
|
|
|
680
689
|
return counts
|
|
681
690
|
}
|
|
682
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Builds optional compatibility counters for non-empty reports.
|
|
694
|
+
* @param {object} compatibility Compatibility report.
|
|
695
|
+
* @returns {object}
|
|
696
|
+
*/
|
|
697
|
+
static #compatibilitySummary(compatibility) {
|
|
698
|
+
const summary = compatibility?.summary || {}
|
|
699
|
+
const issueCount = Number(summary.issueCount || 0)
|
|
700
|
+
|
|
701
|
+
if (!issueCount) return {}
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
compatibilityIssueCount: issueCount,
|
|
705
|
+
hiddenPinCount: Number(summary.hiddenPinCount || 0),
|
|
706
|
+
padDiagnosticCount: Number(summary.padDiagnosticCount || 0),
|
|
707
|
+
modelSuggestionCount: Number(summary.modelSuggestionCount || 0)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
683
711
|
/**
|
|
684
712
|
* Builds duplicate-symbol merge-plan rows.
|
|
685
713
|
* @param {object[]} schematicLibraries Schematic libraries.
|
|
@@ -173,6 +173,7 @@ export class PcbDimensionParser {
|
|
|
173
173
|
static #normalizeKind(kindCode) {
|
|
174
174
|
const normalized = String(kindCode || '').toLowerCase()
|
|
175
175
|
if (/ang/u.test(normalized)) return 'angular'
|
|
176
|
+
if (/diam/u.test(normalized)) return 'diameter'
|
|
176
177
|
if (/radial|radius/u.test(normalized)) return 'radial'
|
|
177
178
|
if (/datum/u.test(normalized)) return 'datum'
|
|
178
179
|
if (/baseline/u.test(normalized)) return 'baseline'
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic QA summaries for recovered PCB dimension records.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbDimensionReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.pcb.dimensions.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a dimension QA report from a parser root or PCB model.
|
|
13
|
+
* @param {object} input Parser root, PCB model, or options object.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const pcb = PcbDimensionReportBuilder.#pcb(input)
|
|
18
|
+
const dimensions = PcbDimensionReportBuilder.#dimensions(pcb)
|
|
19
|
+
const rows = dimensions.map((dimension) =>
|
|
20
|
+
PcbDimensionReportBuilder.#dimensionRow(dimension)
|
|
21
|
+
)
|
|
22
|
+
const findings = rows.flatMap((row) =>
|
|
23
|
+
PcbDimensionReportBuilder.#findingsForRow(row)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
schema: PcbDimensionReportBuilder.SCHEMA,
|
|
28
|
+
summary: PcbDimensionReportBuilder.#summary(rows),
|
|
29
|
+
dimensions: rows,
|
|
30
|
+
findings
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the PCB payload from a parser root or direct PCB model.
|
|
36
|
+
* @param {object} input Parser root or PCB model.
|
|
37
|
+
* @returns {object}
|
|
38
|
+
*/
|
|
39
|
+
static #pcb(input) {
|
|
40
|
+
return input?.pcb || input || {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolves normalized dimension rows from a PCB payload.
|
|
45
|
+
* @param {object} pcb PCB model.
|
|
46
|
+
* @returns {object[]}
|
|
47
|
+
*/
|
|
48
|
+
static #dimensions(pcb) {
|
|
49
|
+
return Array.isArray(pcb?.dimensions) ? pcb.dimensions : []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Builds one public dimension report row.
|
|
54
|
+
* @param {object} dimension Normalized dimension record.
|
|
55
|
+
* @returns {object}
|
|
56
|
+
*/
|
|
57
|
+
static #dimensionRow(dimension) {
|
|
58
|
+
const kind = PcbDimensionReportBuilder.#kind(dimension)
|
|
59
|
+
const references = Array.isArray(dimension?.references)
|
|
60
|
+
? dimension.references
|
|
61
|
+
: []
|
|
62
|
+
const requiredReferenceCount =
|
|
63
|
+
PcbDimensionReportBuilder.#requiredReferenceCount(kind)
|
|
64
|
+
const status =
|
|
65
|
+
references.length >= requiredReferenceCount
|
|
66
|
+
? 'renderable'
|
|
67
|
+
: 'unresolved'
|
|
68
|
+
|
|
69
|
+
return PcbDimensionReportBuilder.#stripEmpty({
|
|
70
|
+
dimensionIndex: PcbDimensionReportBuilder.#integerOrFallback(
|
|
71
|
+
dimension?.dimensionIndex,
|
|
72
|
+
0
|
|
73
|
+
),
|
|
74
|
+
kind,
|
|
75
|
+
name: dimension?.name || '',
|
|
76
|
+
layer: dimension?.layer || '',
|
|
77
|
+
text: dimension?.text || '',
|
|
78
|
+
status,
|
|
79
|
+
referenceCount: references.length,
|
|
80
|
+
requiredReferenceCount,
|
|
81
|
+
hasTextLocation: Boolean(dimension?.textLocation),
|
|
82
|
+
measuredValue: dimension?.measuredValue ?? undefined,
|
|
83
|
+
angleValue: dimension?.angleValue ?? undefined,
|
|
84
|
+
unit: dimension?.unit || ''
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds report findings for one dimension row.
|
|
90
|
+
* @param {object} row Dimension report row.
|
|
91
|
+
* @returns {object[]}
|
|
92
|
+
*/
|
|
93
|
+
static #findingsForRow(row) {
|
|
94
|
+
if (row.status !== 'unresolved') {
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return [
|
|
99
|
+
PcbDimensionReportBuilder.#stripEmpty({
|
|
100
|
+
code: 'pcb.dimension.missing-reference',
|
|
101
|
+
severity: 'warning',
|
|
102
|
+
dimensionIndex: row.dimensionIndex,
|
|
103
|
+
kind: row.kind,
|
|
104
|
+
name: row.name,
|
|
105
|
+
referenceCount: row.referenceCount,
|
|
106
|
+
requiredReferenceCount: row.requiredReferenceCount
|
|
107
|
+
})
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds top-level dimension counters.
|
|
113
|
+
* @param {object[]} rows Dimension report rows.
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
static #summary(rows) {
|
|
117
|
+
const byKind = new Map()
|
|
118
|
+
|
|
119
|
+
for (const row of rows) {
|
|
120
|
+
byKind.set(row.kind, Number(byKind.get(row.kind) || 0) + 1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
dimensionCount: rows.length,
|
|
125
|
+
renderableCount: rows.filter((row) => row.status === 'renderable')
|
|
126
|
+
.length,
|
|
127
|
+
unresolvedCount: rows.filter((row) => row.status === 'unresolved')
|
|
128
|
+
.length,
|
|
129
|
+
missingTextLocationCount: rows.filter(
|
|
130
|
+
(row) => row.hasTextLocation !== true
|
|
131
|
+
).length,
|
|
132
|
+
byKind: [...byKind.entries()]
|
|
133
|
+
.map(([kind, count]) => ({ kind, count }))
|
|
134
|
+
.sort((left, right) =>
|
|
135
|
+
left.kind.localeCompare(right.kind, undefined, {
|
|
136
|
+
numeric: true
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolves the canonical dimension kind.
|
|
144
|
+
* @param {object} dimension Dimension record.
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
static #kind(dimension) {
|
|
148
|
+
const kind = String(dimension?.kind || '')
|
|
149
|
+
.trim()
|
|
150
|
+
.toLowerCase()
|
|
151
|
+
return kind || 'linear'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the minimum reference count needed for deterministic rendering.
|
|
156
|
+
* @param {string} kind Dimension kind.
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
159
|
+
static #requiredReferenceCount(kind) {
|
|
160
|
+
if (kind === 'angular') {
|
|
161
|
+
return 3
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return 2
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parses an integer with a fallback.
|
|
169
|
+
* @param {unknown} value Numeric candidate.
|
|
170
|
+
* @param {number} fallback Fallback value.
|
|
171
|
+
* @returns {number}
|
|
172
|
+
*/
|
|
173
|
+
static #integerOrFallback(value, fallback) {
|
|
174
|
+
const number = Number(value)
|
|
175
|
+
return Number.isInteger(number) ? number : fallback
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Removes undefined and blank-string values from a shallow object.
|
|
180
|
+
* @param {Record<string, unknown>} row Input row.
|
|
181
|
+
* @returns {Record<string, unknown>}
|
|
182
|
+
*/
|
|
183
|
+
static #stripEmpty(row) {
|
|
184
|
+
return Object.fromEntries(
|
|
185
|
+
Object.entries(row).filter(
|
|
186
|
+
([, value]) => value !== undefined && value !== ''
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// specifier.
|
|
8
8
|
import { unzlibSync } from 'fflate'
|
|
9
9
|
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
10
|
+
import { PcbShapeBasedBodyGeometryParser } from './PcbShapeBasedBodyGeometryParser.mjs'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Extracts embedded 3D model payloads and component-body placement metadata
|
|
@@ -17,7 +18,7 @@ export class PcbEmbeddedModelExtractor {
|
|
|
17
18
|
* Extracts embedded model payloads and component-body placements from one
|
|
18
19
|
* stream map.
|
|
19
20
|
* @param {Map<string, Uint8Array>} streams
|
|
20
|
-
* @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[] }}
|
|
21
|
+
* @returns {{ models: { id: string, checksum: number, name: string, format: string, payloadText: string, sourceStream: string, transform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number } }[], componentBodies: { sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[] }}
|
|
21
22
|
*/
|
|
22
23
|
static extractFromStreams(streams) {
|
|
23
24
|
const modelStreamPrefix =
|
|
@@ -51,7 +52,7 @@ export class PcbEmbeddedModelExtractor {
|
|
|
51
52
|
streams.get('ComponentBodies6/Data'),
|
|
52
53
|
'ComponentBodies6/Data'
|
|
53
54
|
),
|
|
54
|
-
...PcbEmbeddedModelExtractor.#
|
|
55
|
+
...PcbEmbeddedModelExtractor.#parseShapeBasedComponentBodyStream(
|
|
55
56
|
streams.get('ShapeBasedComponentBodies6/Data'),
|
|
56
57
|
'ShapeBasedComponentBodies6/Data'
|
|
57
58
|
)
|
|
@@ -134,9 +135,10 @@ export class PcbEmbeddedModelExtractor {
|
|
|
134
135
|
* Parses one component-body printable stream into model-placement records.
|
|
135
136
|
* @param {Uint8Array | undefined} bytes
|
|
136
137
|
* @param {string} sourceStream
|
|
137
|
-
* @
|
|
138
|
+
* @param {{ inferStaticGeometry?: boolean }} [options]
|
|
139
|
+
* @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
|
|
138
140
|
*/
|
|
139
|
-
static #parseComponentBodyStream(bytes, sourceStream) {
|
|
141
|
+
static #parseComponentBodyStream(bytes, sourceStream, options = {}) {
|
|
140
142
|
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
141
143
|
return []
|
|
142
144
|
}
|
|
@@ -153,12 +155,53 @@ export class PcbEmbeddedModelExtractor {
|
|
|
153
155
|
.map((fields) =>
|
|
154
156
|
PcbEmbeddedModelExtractor.#normalizeComponentBody(
|
|
155
157
|
fields,
|
|
156
|
-
sourceStream
|
|
158
|
+
sourceStream,
|
|
159
|
+
options.inferStaticGeometry
|
|
160
|
+
? {
|
|
161
|
+
staticGeometry:
|
|
162
|
+
PcbShapeBasedBodyGeometryParser.buildStaticGeometry(
|
|
163
|
+
fields,
|
|
164
|
+
[]
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
: {}
|
|
157
168
|
)
|
|
158
169
|
)
|
|
159
170
|
.filter(Boolean)
|
|
160
171
|
}
|
|
161
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Parses shape-based component bodies, including optional vertex payloads.
|
|
175
|
+
* @param {Uint8Array | undefined} bytes
|
|
176
|
+
* @param {string} sourceStream
|
|
177
|
+
* @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
|
|
178
|
+
*/
|
|
179
|
+
static #parseShapeBasedComponentBodyStream(bytes, sourceStream) {
|
|
180
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const records = PcbShapeBasedBodyGeometryParser.parse(bytes)
|
|
185
|
+
|
|
186
|
+
if (records.length) {
|
|
187
|
+
return records
|
|
188
|
+
.map((record) =>
|
|
189
|
+
PcbEmbeddedModelExtractor.#normalizeComponentBody(
|
|
190
|
+
record.fields,
|
|
191
|
+
sourceStream,
|
|
192
|
+
{ staticGeometry: record.staticGeometry }
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return PcbEmbeddedModelExtractor.#parseComponentBodyStream(
|
|
199
|
+
bytes,
|
|
200
|
+
sourceStream,
|
|
201
|
+
{ inferStaticGeometry: true }
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
162
205
|
/**
|
|
163
206
|
* Parses one printable field record without requiring a specific leading
|
|
164
207
|
* marker such as `|RECORD=` or `|KIND=`.
|
|
@@ -266,9 +309,10 @@ export class PcbEmbeddedModelExtractor {
|
|
|
266
309
|
* Normalizes one component-body record into model-placement metadata.
|
|
267
310
|
* @param {Record<string, string | string[]>} fields
|
|
268
311
|
* @param {string} sourceStream
|
|
269
|
-
* @
|
|
312
|
+
* @param {{ staticGeometry?: object }} [extra]
|
|
313
|
+
* @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object } | null}
|
|
270
314
|
*/
|
|
271
|
-
static #normalizeComponentBody(fields, sourceStream) {
|
|
315
|
+
static #normalizeComponentBody(fields, sourceStream, extra = {}) {
|
|
272
316
|
const modelId = PcbEmbeddedModelExtractor.#getField(fields, 'MODELID')
|
|
273
317
|
const name = PcbEmbeddedModelExtractor.#getField(fields, 'MODEL.NAME')
|
|
274
318
|
|
|
@@ -339,7 +383,82 @@ export class PcbEmbeddedModelExtractor {
|
|
|
339
383
|
standoffHeightMil: PcbEmbeddedModelExtractor.#parseMilLikeField(
|
|
340
384
|
fields,
|
|
341
385
|
'STANDOFFHEIGHT'
|
|
342
|
-
)
|
|
386
|
+
),
|
|
387
|
+
...PcbEmbeddedModelExtractor.#shapeBodyMetadata(fields),
|
|
388
|
+
...PcbEmbeddedModelExtractor.#stripUndefined(extra)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Normalizes shape-based body display metadata.
|
|
394
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
395
|
+
* @returns {object}
|
|
396
|
+
*/
|
|
397
|
+
static #shapeBodyMetadata(fields) {
|
|
398
|
+
const modelType = PcbEmbeddedModelExtractor.#parseIntegerField(
|
|
399
|
+
fields,
|
|
400
|
+
'MODEL.MODELTYPE'
|
|
401
|
+
)
|
|
402
|
+
const bodyColorRaw = PcbEmbeddedModelExtractor.#parseIntegerField(
|
|
403
|
+
fields,
|
|
404
|
+
'BODYCOLOR3D'
|
|
405
|
+
)
|
|
406
|
+
const bodyOpacity = PcbEmbeddedModelExtractor.#parseNumberField(
|
|
407
|
+
fields,
|
|
408
|
+
'BODYOPACITY3D'
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return PcbEmbeddedModelExtractor.#stripUndefined({
|
|
412
|
+
modelType: modelType === null ? undefined : modelType,
|
|
413
|
+
modelTypeName:
|
|
414
|
+
modelType === null
|
|
415
|
+
? undefined
|
|
416
|
+
: PcbEmbeddedModelExtractor.#modelTypeName(modelType),
|
|
417
|
+
bodyColor:
|
|
418
|
+
bodyColorRaw === null
|
|
419
|
+
? undefined
|
|
420
|
+
: PcbEmbeddedModelExtractor.#decodeBodyColor(bodyColorRaw),
|
|
421
|
+
bodyOpacity:
|
|
422
|
+
bodyOpacity === null || !Number.isFinite(bodyOpacity)
|
|
423
|
+
? undefined
|
|
424
|
+
: bodyOpacity
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Maps a numeric body model type to a stable semantic label.
|
|
430
|
+
* @param {number} modelType Numeric model type.
|
|
431
|
+
* @returns {string}
|
|
432
|
+
*/
|
|
433
|
+
static #modelTypeName(modelType) {
|
|
434
|
+
const modelTypeNames = new Map([
|
|
435
|
+
[0, 'extruded-polygon'],
|
|
436
|
+
[1, 'cone'],
|
|
437
|
+
[2, 'cylinder'],
|
|
438
|
+
[3, 'sphere']
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
return modelTypeNames.get(modelType) || 'unknown-' + modelType
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Decodes one packed native RGB color value.
|
|
446
|
+
* @param {number} rawColor Packed color value.
|
|
447
|
+
* @returns {{ raw: number, hex: string, rgb: { red: number, green: number, blue: number } }}
|
|
448
|
+
*/
|
|
449
|
+
static #decodeBodyColor(rawColor) {
|
|
450
|
+
const red = rawColor & 0xff
|
|
451
|
+
const green = (rawColor >> 8) & 0xff
|
|
452
|
+
const blue = (rawColor >> 16) & 0xff
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
raw: rawColor,
|
|
456
|
+
hex:
|
|
457
|
+
'#' +
|
|
458
|
+
[red, green, blue]
|
|
459
|
+
.map((component) => component.toString(16).padStart(2, '0'))
|
|
460
|
+
.join(''),
|
|
461
|
+
rgb: { red, green, blue }
|
|
343
462
|
}
|
|
344
463
|
}
|
|
345
464
|
|
|
@@ -359,8 +478,8 @@ export class PcbEmbeddedModelExtractor {
|
|
|
359
478
|
|
|
360
479
|
/**
|
|
361
480
|
* Deduplicates shape-based body records shared across body streams.
|
|
362
|
-
* @param {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[]} componentBodies
|
|
363
|
-
* @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null }[]}
|
|
481
|
+
* @param {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]} componentBodies
|
|
482
|
+
* @returns {{ sourceStream: string, layer: string, identifier: string, modelId: string, checksum: number | null, embedded: boolean, name: string, positionMil: { x: number, y: number }, rotationDeg: number, modelRotationDeg: { x: number, y: number, z: number }, dzMil: number, overallHeightMil: number | null, standoffHeightMil: number | null, modelType?: number, modelTypeName?: string, bodyColor?: object, bodyOpacity?: number, staticGeometry?: object }[]}
|
|
364
483
|
*/
|
|
365
484
|
static #dedupeComponentBodies(componentBodies) {
|
|
366
485
|
const uniqueBodies = new Map()
|
|
@@ -747,4 +866,15 @@ export class PcbEmbeddedModelExtractor {
|
|
|
747
866
|
.filter(Number.isInteger)
|
|
748
867
|
)
|
|
749
868
|
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Removes undefined values from a normalized metadata row.
|
|
872
|
+
* @param {object} row Metadata row.
|
|
873
|
+
* @returns {object}
|
|
874
|
+
*/
|
|
875
|
+
static #stripUndefined(row) {
|
|
876
|
+
return Object.fromEntries(
|
|
877
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
878
|
+
)
|
|
879
|
+
}
|
|
750
880
|
}
|