altium-toolkit 1.0.7 → 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/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- 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,99 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbInteractionIndex } from './PcbInteractionIndex.mjs'
|
|
6
|
+
|
|
7
|
+
const VIRTUAL_LAYER_DEFINITIONS = [
|
|
8
|
+
{ key: 'tracks', label: 'Tracks' },
|
|
9
|
+
{ key: 'vias', label: 'Vias' },
|
|
10
|
+
{ key: 'pads', label: 'Pads' },
|
|
11
|
+
{ key: 'holes', label: 'Holes' },
|
|
12
|
+
{ key: 'zones', label: 'Zones' },
|
|
13
|
+
{ key: 'footprint-text', label: 'Footprint text' }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds a PCB layer summary with physical layers and virtual controls.
|
|
18
|
+
*/
|
|
19
|
+
export class PcbInteractionLayerModel {
|
|
20
|
+
/**
|
|
21
|
+
* Resolves physical and virtual interaction layers.
|
|
22
|
+
* @param {object} documentModel Toolkit document model.
|
|
23
|
+
* @returns {{ physicalLayers: object[], virtualLayers: object[] }}
|
|
24
|
+
*/
|
|
25
|
+
static resolve(documentModel) {
|
|
26
|
+
const pcb = documentModel?.pcb || {}
|
|
27
|
+
const physicalLayers = PcbInteractionLayerModel.#physicalLayers(pcb)
|
|
28
|
+
const items = PcbInteractionIndex.build(documentModel)
|
|
29
|
+
const layersByObject = PcbInteractionLayerModel.#layersByObject(items)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
physicalLayers,
|
|
33
|
+
virtualLayers: VIRTUAL_LAYER_DEFINITIONS.map((definition) => ({
|
|
34
|
+
...definition,
|
|
35
|
+
physicalLayerKeys: Array.from(
|
|
36
|
+
layersByObject.get(definition.key) || []
|
|
37
|
+
)
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves physical layer rows from board and primitive layer metadata.
|
|
44
|
+
* @param {object} pcb PCB model.
|
|
45
|
+
* @returns {object[]}
|
|
46
|
+
*/
|
|
47
|
+
static #physicalLayers(pcb) {
|
|
48
|
+
const seen = new Set()
|
|
49
|
+
const layers = []
|
|
50
|
+
const sources = [
|
|
51
|
+
...(Array.isArray(pcb?.layers) ? pcb.layers : []),
|
|
52
|
+
...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for (const layer of sources) {
|
|
56
|
+
const key = String(layer?.name || layer?.layer || '').trim()
|
|
57
|
+
if (!key || seen.has(key)) continue
|
|
58
|
+
seen.add(key)
|
|
59
|
+
layers.push({
|
|
60
|
+
key,
|
|
61
|
+
label: key,
|
|
62
|
+
layerId: Number.isFinite(Number(layer?.layerId))
|
|
63
|
+
? Number(layer.layerId)
|
|
64
|
+
: null
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return layers
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collects referenced physical layer keys by virtual object key.
|
|
73
|
+
* @param {object[]} items Interaction items.
|
|
74
|
+
* @returns {Map<string, Set<string>>}
|
|
75
|
+
*/
|
|
76
|
+
static #layersByObject(items) {
|
|
77
|
+
const layersByObject = new Map()
|
|
78
|
+
|
|
79
|
+
for (const item of items) {
|
|
80
|
+
if (!layersByObject.has(item.objectKey)) {
|
|
81
|
+
layersByObject.set(item.objectKey, new Set())
|
|
82
|
+
}
|
|
83
|
+
const layerSet = layersByObject.get(item.objectKey)
|
|
84
|
+
for (const layerKey of item.layerKeys || []) {
|
|
85
|
+
layerSet.add(layerKey)
|
|
86
|
+
}
|
|
87
|
+
if (item.type === 'pad' || item.type === 'via') {
|
|
88
|
+
if (!layersByObject.has('holes')) {
|
|
89
|
+
layersByObject.set('holes', new Set())
|
|
90
|
+
}
|
|
91
|
+
for (const layerKey of item.layerKeys || []) {
|
|
92
|
+
layersByObject.get('holes').add(layerKey)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return layersByObject
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -46,17 +46,82 @@ export class PcbScene3dBoardOutlineRefiner {
|
|
|
46
46
|
return sceneDescription
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const refinedBoard = {
|
|
50
|
+
...board,
|
|
51
|
+
minX: candidate.minX,
|
|
52
|
+
minY: candidate.minY,
|
|
53
|
+
widthMil: candidate.widthMil,
|
|
54
|
+
heightMil: candidate.heightMil,
|
|
55
|
+
centerX: candidate.minX + candidate.widthMil / 2,
|
|
56
|
+
centerY: candidate.minY + candidate.heightMil / 2,
|
|
57
|
+
segments: candidate.segments
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
return {
|
|
50
61
|
...sceneDescription,
|
|
51
|
-
board:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
board: refinedBoard,
|
|
63
|
+
components: PcbScene3dBoardOutlineRefiner.#realignLocalPlacements(
|
|
64
|
+
sceneDescription?.components,
|
|
65
|
+
board,
|
|
66
|
+
refinedBoard
|
|
67
|
+
),
|
|
68
|
+
externalPlacements:
|
|
69
|
+
PcbScene3dBoardOutlineRefiner.#realignLocalPlacements(
|
|
70
|
+
sceneDescription?.externalPlacements,
|
|
71
|
+
board,
|
|
72
|
+
refinedBoard
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Realigns precomputed local placements to a refined board center.
|
|
79
|
+
* @param {object[] | undefined} placements Scene placements.
|
|
80
|
+
* @param {{ centerX?: number, centerY?: number }} previousBoard Previous board.
|
|
81
|
+
* @param {{ centerX?: number, centerY?: number }} refinedBoard Refined board.
|
|
82
|
+
* @returns {object[] | undefined}
|
|
83
|
+
*/
|
|
84
|
+
static #realignLocalPlacements(placements, previousBoard, refinedBoard) {
|
|
85
|
+
if (!Array.isArray(placements)) {
|
|
86
|
+
return placements
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const deltaX =
|
|
90
|
+
Number(previousBoard?.centerX || 0) -
|
|
91
|
+
Number(refinedBoard?.centerX || 0)
|
|
92
|
+
const deltaY =
|
|
93
|
+
Number(previousBoard?.centerY || 0) -
|
|
94
|
+
Number(refinedBoard?.centerY || 0)
|
|
95
|
+
|
|
96
|
+
if (!deltaX && !deltaY) {
|
|
97
|
+
return placements
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return placements.map((placement) =>
|
|
101
|
+
PcbScene3dBoardOutlineRefiner.#realignLocalPlacement(
|
|
102
|
+
placement,
|
|
103
|
+
deltaX,
|
|
104
|
+
deltaY
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Applies one local origin delta to a scene placement.
|
|
111
|
+
* @param {object} placement Scene placement.
|
|
112
|
+
* @param {number} deltaX Local X delta.
|
|
113
|
+
* @param {number} deltaY Local Y delta.
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
static #realignLocalPlacement(placement, deltaX, deltaY) {
|
|
117
|
+
const positionMil = placement?.positionMil || {}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...placement,
|
|
121
|
+
positionMil: {
|
|
122
|
+
...positionMil,
|
|
123
|
+
x: Number(positionMil.x || 0) + deltaX,
|
|
124
|
+
y: Number(positionMil.y || 0) + deltaY
|
|
60
125
|
}
|
|
61
126
|
}
|
|
62
127
|
}
|
|
@@ -18,14 +18,17 @@ export class PcbScene3dBuilder {
|
|
|
18
18
|
static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
|
|
19
19
|
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
20
20
|
static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
|
|
21
|
-
static #PRECISE_BODY_MATCH_TOLERANCE_MIL =
|
|
21
|
+
static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 20
|
|
22
|
+
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
23
|
+
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
24
|
+
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
22
25
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Builds a scene description for host 3D renderers.
|
|
26
29
|
* @param {{ pcb?: { boardOutline?: { widthMil?: number, heightMil?: number, minX?: number, minY?: number, segments?: Array<Record<string, number | string>> }, primitiveLayers?: { layerId: number, name: string }[], pads?: { x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[], tracks?: any[], arcs?: any[], fills?: any[], vias?: any[], polygons?: any[], embeddedModels?: any[], componentBodies?: { modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }[], components?: { designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }[] } }} documentModel
|
|
27
|
-
* @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
|
|
28
|
-
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
|
|
30
|
+
* @param {{ modelRegistry?: { resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null, resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null, resolveBoardAssemblyModel?: (documentModel: any) => { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null } | null, boardThicknessMil?: number }} [options]
|
|
31
|
+
* @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, boardAssemblyModel: { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
|
|
29
32
|
*/
|
|
30
33
|
static build(documentModel, options = {}) {
|
|
31
34
|
const pcb = documentModel?.pcb || {}
|
|
@@ -60,6 +63,10 @@ export class PcbScene3dBuilder {
|
|
|
60
63
|
centerY:
|
|
61
64
|
Number(boardOutline.minY || 0) +
|
|
62
65
|
Number(boardOutline.heightMil || 0) / 2,
|
|
66
|
+
surfaceColor: Number.isInteger(appearance3d.solderMaskTopColor)
|
|
67
|
+
? appearance3d.solderMaskTopColor
|
|
68
|
+
: appearance3d.solderMaskBottomColor,
|
|
69
|
+
edgeColor: appearance3d.boardCoreColor,
|
|
63
70
|
segments: Array.isArray(boardOutline.segments)
|
|
64
71
|
? boardOutline.segments
|
|
65
72
|
: []
|
|
@@ -103,6 +110,9 @@ export class PcbScene3dBuilder {
|
|
|
103
110
|
const sceneDescription = {
|
|
104
111
|
sourceFormat: 'altium',
|
|
105
112
|
board,
|
|
113
|
+
boardAssemblyModel:
|
|
114
|
+
modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
|
|
115
|
+
null,
|
|
106
116
|
components: components.map((component) =>
|
|
107
117
|
PcbScene3dBuilder.#buildComponent(
|
|
108
118
|
component,
|
|
@@ -118,6 +128,7 @@ export class PcbScene3dBuilder {
|
|
|
118
128
|
componentBody,
|
|
119
129
|
bodyMatches[index],
|
|
120
130
|
components,
|
|
131
|
+
pads,
|
|
121
132
|
board,
|
|
122
133
|
thicknessMil,
|
|
123
134
|
modelRegistry
|
|
@@ -205,6 +216,7 @@ export class PcbScene3dBuilder {
|
|
|
205
216
|
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
|
|
206
217
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
|
|
207
218
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
|
|
219
|
+
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
208
220
|
* @param {{ centerX: number, centerY: number }} board
|
|
209
221
|
* @param {number} thicknessMil
|
|
210
222
|
* @param {{ resolveComponentBodyModel?: (componentBody: any) => { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null } | null} modelRegistry
|
|
@@ -214,6 +226,7 @@ export class PcbScene3dBuilder {
|
|
|
214
226
|
componentBody,
|
|
215
227
|
matchedComponent,
|
|
216
228
|
components,
|
|
229
|
+
pads,
|
|
217
230
|
board,
|
|
218
231
|
thicknessMil,
|
|
219
232
|
modelRegistry
|
|
@@ -272,10 +285,134 @@ export class PcbScene3dBuilder {
|
|
|
272
285
|
rotationDeg: modelRotation,
|
|
273
286
|
dzMil: Number(componentBody.dzMil || 0)
|
|
274
287
|
},
|
|
288
|
+
projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
|
|
289
|
+
componentBody,
|
|
290
|
+
matchedComponent,
|
|
291
|
+
pads,
|
|
292
|
+
resolvedModel
|
|
293
|
+
),
|
|
275
294
|
externalModel: resolvedModel
|
|
276
295
|
}
|
|
277
296
|
}
|
|
278
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Explains which footprint projection source informed one external model.
|
|
300
|
+
* @param {object} componentBody Normalized component body row.
|
|
301
|
+
* @param {{ x: number, y: number, height?: number | null } | null} matchedComponent Matched component.
|
|
302
|
+
* @param {object[]} pads Normalized pad rows.
|
|
303
|
+
* @param {object | null} resolvedModel Resolved model metadata.
|
|
304
|
+
* @returns {{ source: string, reason: string, boundsMil: { width: number, depth: number, height: number } }}
|
|
305
|
+
*/
|
|
306
|
+
static #resolveProjectionDiagnostics(
|
|
307
|
+
componentBody,
|
|
308
|
+
matchedComponent,
|
|
309
|
+
pads,
|
|
310
|
+
resolvedModel
|
|
311
|
+
) {
|
|
312
|
+
const authoredBounds = PcbScene3dBuilder.#firstBounds([
|
|
313
|
+
componentBody?.projectionOverrideMil,
|
|
314
|
+
componentBody?.projectionOverride?.boundsMil,
|
|
315
|
+
componentBody?.projectionBoundsMil
|
|
316
|
+
])
|
|
317
|
+
if (authoredBounds) {
|
|
318
|
+
return {
|
|
319
|
+
source: 'authored-override',
|
|
320
|
+
reason: 'Component body carried an explicit projection override.',
|
|
321
|
+
boundsMil: authoredBounds
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const modelBounds = PcbScene3dBuilder.#firstBounds([
|
|
326
|
+
componentBody?.modelBoundsMil,
|
|
327
|
+
resolvedModel?.boundsMil
|
|
328
|
+
])
|
|
329
|
+
if (modelBounds) {
|
|
330
|
+
return {
|
|
331
|
+
source: 'model-bounds',
|
|
332
|
+
reason: 'Resolved 3D model bounds were available.',
|
|
333
|
+
boundsMil: modelBounds
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (matchedComponent) {
|
|
338
|
+
const padSpan = PcbScene3dBuilder.#resolvePadSpan(
|
|
339
|
+
matchedComponent,
|
|
340
|
+
pads
|
|
341
|
+
)
|
|
342
|
+
if (padSpan.width > 0 || padSpan.depth > 0) {
|
|
343
|
+
return {
|
|
344
|
+
source: 'pad-fallback',
|
|
345
|
+
reason: 'Projection fell back to nearby component pad span.',
|
|
346
|
+
boundsMil: {
|
|
347
|
+
width: padSpan.width,
|
|
348
|
+
depth: padSpan.depth,
|
|
349
|
+
height: Number(matchedComponent.height || 0)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const body = PcbScene3dPackages.resolve(matchedComponent, padSpan)
|
|
355
|
+
return {
|
|
356
|
+
source: 'component-fallback',
|
|
357
|
+
reason: 'Projection fell back to the procedural component body.',
|
|
358
|
+
boundsMil: {
|
|
359
|
+
width: body.sizeMil.width,
|
|
360
|
+
depth: body.sizeMil.depth,
|
|
361
|
+
height: body.sizeMil.height
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
source: 'model-anchor-fallback',
|
|
368
|
+
reason: 'Projection used the model anchor because no owner geometry was available.',
|
|
369
|
+
boundsMil: { width: 0, depth: 0, height: 0 }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Returns the first complete bounds object from candidate metadata.
|
|
375
|
+
* @param {unknown[]} candidates Candidate bounds records.
|
|
376
|
+
* @returns {{ width: number, depth: number, height: number } | null}
|
|
377
|
+
*/
|
|
378
|
+
static #firstBounds(candidates) {
|
|
379
|
+
for (const candidate of candidates || []) {
|
|
380
|
+
const bounds = PcbScene3dBuilder.#normalizeBounds(candidate)
|
|
381
|
+
if (bounds) {
|
|
382
|
+
return bounds
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Normalizes width/depth/height bounds metadata.
|
|
391
|
+
* @param {unknown} candidate Candidate bounds record.
|
|
392
|
+
* @returns {{ width: number, depth: number, height: number } | null}
|
|
393
|
+
*/
|
|
394
|
+
static #normalizeBounds(candidate) {
|
|
395
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const width = Number(candidate.width ?? candidate.x ?? candidate.sizeX)
|
|
400
|
+
const depth = Number(candidate.depth ?? candidate.y ?? candidate.sizeY)
|
|
401
|
+
const height = Number(
|
|
402
|
+
candidate.height ?? candidate.z ?? candidate.sizeZ
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
!Number.isFinite(width) ||
|
|
407
|
+
!Number.isFinite(depth) ||
|
|
408
|
+
!Number.isFinite(height)
|
|
409
|
+
) {
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { width, depth, height }
|
|
414
|
+
}
|
|
415
|
+
|
|
279
416
|
/**
|
|
280
417
|
* Resolves explicit body placements to component anchors using a unique
|
|
281
418
|
* nearest-neighbor pass plus an ordered-affinity fallback for repeated
|
|
@@ -1159,16 +1296,41 @@ export class PcbScene3dBuilder {
|
|
|
1159
1296
|
static #isBodyPositionNearBoard(componentBody, board) {
|
|
1160
1297
|
const bodyX = Number(componentBody?.positionMil?.x || 0)
|
|
1161
1298
|
const bodyY = Number(componentBody?.positionMil?.y || 0)
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1299
|
+
const xOverhang = PcbScene3dBuilder.#resolveUnmatchedBodyOverhang(
|
|
1300
|
+
board?.widthMil
|
|
1301
|
+
)
|
|
1302
|
+
const yOverhang = PcbScene3dBuilder.#resolveUnmatchedBodyOverhang(
|
|
1303
|
+
board?.heightMil
|
|
1304
|
+
)
|
|
1305
|
+
const minX = Number(board?.minX || 0) - xOverhang
|
|
1306
|
+
const minY = Number(board?.minY || 0) - yOverhang
|
|
1164
1307
|
const maxX =
|
|
1165
|
-
Number(board?.minX || 0) + Number(board?.widthMil || 0) +
|
|
1308
|
+
Number(board?.minX || 0) + Number(board?.widthMil || 0) + xOverhang
|
|
1166
1309
|
const maxY =
|
|
1167
|
-
Number(board?.minY || 0) + Number(board?.heightMil || 0) +
|
|
1310
|
+
Number(board?.minY || 0) + Number(board?.heightMil || 0) + yOverhang
|
|
1168
1311
|
|
|
1169
1312
|
return bodyX >= minX && bodyX <= maxX && bodyY >= minY && bodyY <= maxY
|
|
1170
1313
|
}
|
|
1171
1314
|
|
|
1315
|
+
/**
|
|
1316
|
+
* Resolves a proportional unresolved-body margin for one board axis.
|
|
1317
|
+
* @param {number | string | undefined} spanMil Board axis span.
|
|
1318
|
+
* @returns {number}
|
|
1319
|
+
*/
|
|
1320
|
+
static #resolveUnmatchedBodyOverhang(spanMil) {
|
|
1321
|
+
const proportional =
|
|
1322
|
+
Math.max(Number(spanMil || 0), 0) *
|
|
1323
|
+
PcbScene3dBuilder.#UNMATCHED_BODY_OVERHANG_RATIO
|
|
1324
|
+
|
|
1325
|
+
return Math.min(
|
|
1326
|
+
PcbScene3dBuilder.#UNMATCHED_BODY_MAX_OVERHANG_MIL,
|
|
1327
|
+
Math.max(
|
|
1328
|
+
proportional,
|
|
1329
|
+
PcbScene3dBuilder.#UNMATCHED_BODY_MIN_OVERHANG_MIL
|
|
1330
|
+
)
|
|
1331
|
+
)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1172
1334
|
/**
|
|
1173
1335
|
* Normalizes one angle into the range [0, 360).
|
|
1174
1336
|
* @param {number} angle
|
|
@@ -208,6 +208,49 @@ export class PcbScene3dModelRegistry {
|
|
|
208
208
|
return this.#resolveExplicitMatch(componentBody?.name)
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Resolves a project-level full board assembly model for one PCB document.
|
|
213
|
+
* @param {{ fileName?: string }} documentModel
|
|
214
|
+
* @returns {{ origin: 'board-assembly', file?: File | Blob | null, name: string, relativePath: string, format: string } | null}
|
|
215
|
+
*/
|
|
216
|
+
resolveBoardAssemblyModel(documentModel) {
|
|
217
|
+
const normalizedBoardBaseName = PcbScene3dModelRegistry.#normalizeToken(
|
|
218
|
+
PcbScene3dModelRegistry.#basenameWithoutExtension(
|
|
219
|
+
documentModel?.fileName
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
if (!normalizedBoardBaseName) {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rankedMatches = this.#modelFiles
|
|
227
|
+
.filter(
|
|
228
|
+
(file) =>
|
|
229
|
+
file.normalizedBaseName === normalizedBoardBaseName &&
|
|
230
|
+
PcbScene3dModelRegistry.#isBoardAssemblyPath(
|
|
231
|
+
file.relativePath
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
.sort(
|
|
235
|
+
(left, right) =>
|
|
236
|
+
PcbScene3dModelRegistry.#formatRank(left.format) -
|
|
237
|
+
PcbScene3dModelRegistry.#formatRank(right.format)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if (!rankedMatches.length) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const matchedFile = rankedMatches[0]
|
|
245
|
+
return {
|
|
246
|
+
origin: 'board-assembly',
|
|
247
|
+
file: matchedFile.file,
|
|
248
|
+
name: matchedFile.name,
|
|
249
|
+
relativePath: matchedFile.relativePath,
|
|
250
|
+
format: matchedFile.format
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
211
254
|
/**
|
|
212
255
|
* Normalizes one embedded payload for registry lookup.
|
|
213
256
|
* @param {{ id?: string, checksum?: number | null, name?: string, format?: string, payloadText?: string, sourceStream?: string }} model
|
|
@@ -295,6 +338,37 @@ export class PcbScene3dModelRegistry {
|
|
|
295
338
|
return format === 'wrl' ? 0 : 1
|
|
296
339
|
}
|
|
297
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Checks whether one model path is in a conventional board model folder.
|
|
343
|
+
* @param {string | undefined} relativePath
|
|
344
|
+
* @returns {boolean}
|
|
345
|
+
*/
|
|
346
|
+
static #isBoardAssemblyPath(relativePath) {
|
|
347
|
+
return String(relativePath || '')
|
|
348
|
+
.replaceAll('\\', '/')
|
|
349
|
+
.split('/')
|
|
350
|
+
.some(
|
|
351
|
+
(part) =>
|
|
352
|
+
PcbScene3dModelRegistry.#normalizeToken(part) === '3dbodies'
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Returns one path basename without its extension.
|
|
358
|
+
* @param {string | undefined} filePath
|
|
359
|
+
* @returns {string}
|
|
360
|
+
*/
|
|
361
|
+
static #basenameWithoutExtension(filePath) {
|
|
362
|
+
const baseName =
|
|
363
|
+
String(filePath || '')
|
|
364
|
+
.replaceAll('\\', '/')
|
|
365
|
+
.split('/')
|
|
366
|
+
.filter(Boolean)
|
|
367
|
+
.at(-1) || ''
|
|
368
|
+
|
|
369
|
+
return baseName.replace(/\.[^.]+$/u, '')
|
|
370
|
+
}
|
|
371
|
+
|
|
298
372
|
/**
|
|
299
373
|
* Normalizes one lookup token.
|
|
300
374
|
* @param {string | undefined} value
|