altium-toolkit 1.1.30 → 1.1.32
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/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +34 -1
- package/src/core/altium/AltiumSchematicArcAngleNormalizer.mjs +82 -0
- package/src/core/altium/AltiumSchematicFreeGraphicStrokeNormalizer.mjs +105 -0
- package/src/core/altium/AltiumSchematicHiddenDesignatorResolver.mjs +191 -0
- package/src/core/altium/AltiumSchematicPackedImageResolver.mjs +433 -0
- package/src/core/altium/AltiumSchematicSheetBoundsNormalizer.mjs +287 -0
- package/src/parser.mjs +5 -0
- package/src/renderers.mjs +2 -0
- package/src/scene3d.mjs +1 -0
- package/src/ui/AltiumPcbBottomViewMirror.mjs +394 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +300 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +364 -22
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +568 -4
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +149 -23
- package/src/ui/PcbFootprintPadAxisNormalizer.mjs +609 -0
- package/src/ui/PcbScene3dBuilder.mjs +313 -15
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +34 -2
|
@@ -8,12 +8,14 @@ import { PcbScene3dDrillCutoutBuilder } from './PcbScene3dDrillCutoutBuilder.mjs
|
|
|
8
8
|
import { AltiumScene3dExternalPlacementAdapter } from './AltiumScene3dExternalPlacementAdapter.mjs'
|
|
9
9
|
import { AltiumScene3dBottomPadRotationAdapter } from './AltiumScene3dBottomPadRotationAdapter.mjs'
|
|
10
10
|
import { AltiumScene3dComponentBodyAdapter } from './AltiumScene3dComponentBodyAdapter.mjs'
|
|
11
|
+
import { AltiumScene3dAuthoredBodyAnchorAdapter } from './AltiumScene3dAuthoredBodyAnchorAdapter.mjs'
|
|
11
12
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
12
13
|
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
13
14
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
14
15
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
15
16
|
import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
|
|
16
17
|
import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
|
|
18
|
+
import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Builds deterministic 3D scene data from the normalized PCB model.
|
|
@@ -24,10 +26,16 @@ export class PcbScene3dBuilder {
|
|
|
24
26
|
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
25
27
|
static #DENSE_OVERLAY_KNOCKOUT_COLOR = 0x2f6a2c
|
|
26
28
|
static #PRECISE_BODY_MATCH_TOLERANCE_MIL = 20
|
|
29
|
+
static #EXACT_BODY_MISMATCH_TOLERANCE_MIL = 1
|
|
30
|
+
static #NEAR_PACKAGE_AFFINITY_DISTANCE_MIL = 100
|
|
27
31
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
28
32
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
29
33
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
30
34
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
35
|
+
static #AUTHORED_BODY_IDENTITY_PATTERN =
|
|
36
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|mount|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
|
|
37
|
+
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
38
|
+
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn|[a-z0-9]*qfn|bga|cap|capacitor|crystal|diode|ferrite|ind|inductor|lga|lqg[a-z0-9]*|lqw[a-z0-9]*|osc|qfp|res|resistor|sot|transistor|xtal)(?:$|[^a-z0-9])/i
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Builds a scene description for host 3D renderers.
|
|
@@ -36,7 +44,9 @@ export class PcbScene3dBuilder {
|
|
|
36
44
|
* @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 } }[], staticBodyPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[], 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 } } } }}
|
|
37
45
|
*/
|
|
38
46
|
static build(documentModel, options = {}) {
|
|
39
|
-
const
|
|
47
|
+
const sceneDocumentModel =
|
|
48
|
+
PcbFootprintPadAxisNormalizer.apply(documentModel)
|
|
49
|
+
const pcb = sceneDocumentModel?.pcb || {}
|
|
40
50
|
const appearance3d = pcb.appearance3d || {}
|
|
41
51
|
const boardOutline = pcb.boardOutline || {}
|
|
42
52
|
const primitiveLayers = Array.isArray(pcb.primitiveLayers)
|
|
@@ -77,10 +87,16 @@ export class PcbScene3dBuilder {
|
|
|
77
87
|
: []
|
|
78
88
|
}
|
|
79
89
|
const componentBodyModels = componentBodies.map((componentBody) =>
|
|
80
|
-
PcbScene3dBuilder.#
|
|
90
|
+
PcbScene3dBuilder.#shouldSuppressLayerlessBodyPlaceholder(
|
|
81
91
|
componentBody,
|
|
82
|
-
|
|
92
|
+
componentBodies,
|
|
93
|
+
board
|
|
83
94
|
)
|
|
95
|
+
? null
|
|
96
|
+
: PcbScene3dBuilder.#resolveComponentBodyModel(
|
|
97
|
+
componentBody,
|
|
98
|
+
modelRegistry
|
|
99
|
+
)
|
|
84
100
|
)
|
|
85
101
|
const bodyMatches = PcbScene3dBuilder.#resolveComponentBodyMatches(
|
|
86
102
|
componentBodies,
|
|
@@ -171,28 +187,30 @@ export class PcbScene3dBuilder {
|
|
|
171
187
|
}
|
|
172
188
|
}
|
|
173
189
|
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
return AltiumScene3dAuthoredBodyAnchorAdapter.apply(
|
|
191
|
+
AltiumScene3dBottomPadRotationAdapter.apply(
|
|
192
|
+
AltiumScene3dComponentBodyAdapter.apply(
|
|
193
|
+
AltiumScene3dExternalPlacementAdapter.apply(
|
|
194
|
+
PcbScene3dBoardOutlineRefiner.refine(
|
|
195
|
+
sceneDescription,
|
|
196
|
+
sceneDocumentModel
|
|
197
|
+
),
|
|
198
|
+
sceneDocumentModel
|
|
180
199
|
),
|
|
181
|
-
|
|
182
|
-
)
|
|
183
|
-
documentModel
|
|
200
|
+
sceneDocumentModel
|
|
201
|
+
)
|
|
184
202
|
)
|
|
185
203
|
)
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
/**
|
|
189
207
|
* Builds one procedural component scene entry.
|
|
190
|
-
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, modelPath?: string }} component
|
|
208
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, description?: string, parameters?: Record<string, unknown>, modelPath?: string }} component
|
|
191
209
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
192
210
|
* @param {{ centerX: number, centerY: number }} board
|
|
193
211
|
* @param {number} thicknessMil
|
|
194
212
|
* @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
|
|
195
|
-
* @returns {{ 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 }}
|
|
213
|
+
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, description: string, parameters: Record<string, unknown>, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }}
|
|
196
214
|
*/
|
|
197
215
|
static #buildComponent(
|
|
198
216
|
component,
|
|
@@ -227,6 +245,13 @@ export class PcbScene3dBuilder {
|
|
|
227
245
|
},
|
|
228
246
|
pattern: String(component.pattern || ''),
|
|
229
247
|
source: String(component.source || ''),
|
|
248
|
+
description: String(component.description || ''),
|
|
249
|
+
parameters:
|
|
250
|
+
component.parameters &&
|
|
251
|
+
typeof component.parameters === 'object' &&
|
|
252
|
+
!Array.isArray(component.parameters)
|
|
253
|
+
? { ...component.parameters }
|
|
254
|
+
: {},
|
|
230
255
|
body,
|
|
231
256
|
externalModel: modelRegistry
|
|
232
257
|
? modelRegistry.resolveComponentModel(component)
|
|
@@ -269,6 +294,16 @@ export class PcbScene3dBuilder {
|
|
|
269
294
|
return null
|
|
270
295
|
}
|
|
271
296
|
|
|
297
|
+
if (
|
|
298
|
+
!matchedComponent &&
|
|
299
|
+
PcbScene3dBuilder.#shouldDropUnmatchedPackageBody(
|
|
300
|
+
componentBody,
|
|
301
|
+
components
|
|
302
|
+
)
|
|
303
|
+
) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
272
307
|
if (
|
|
273
308
|
!matchedComponent &&
|
|
274
309
|
!PcbScene3dBuilder.#isBodyPositionNearBoard(componentBody, board)
|
|
@@ -534,6 +569,12 @@ export class PcbScene3dBuilder {
|
|
|
534
569
|
bodyIndex,
|
|
535
570
|
componentIndex,
|
|
536
571
|
affinityScore,
|
|
572
|
+
nearPackageAffinityScore:
|
|
573
|
+
PcbScene3dBuilder.#nearPackageAffinityScore(
|
|
574
|
+
componentBody,
|
|
575
|
+
affinityScore,
|
|
576
|
+
distance
|
|
577
|
+
),
|
|
537
578
|
preciseOwnerScore:
|
|
538
579
|
precise && (sideCompatible || affinityScore > 0)
|
|
539
580
|
? 1
|
|
@@ -549,6 +590,8 @@ export class PcbScene3dBuilder {
|
|
|
549
590
|
closeCandidates
|
|
550
591
|
.sort(
|
|
551
592
|
(left, right) =>
|
|
593
|
+
right.nearPackageAffinityScore -
|
|
594
|
+
left.nearPackageAffinityScore ||
|
|
552
595
|
right.preciseOwnerScore - left.preciseOwnerScore ||
|
|
553
596
|
right.sideAffinityScore - left.sideAffinityScore ||
|
|
554
597
|
right.affinityScore - left.affinityScore ||
|
|
@@ -798,7 +841,11 @@ export class PcbScene3dBuilder {
|
|
|
798
841
|
distanceMil
|
|
799
842
|
) {
|
|
800
843
|
if (PcbScene3dBuilder.#isPreciseBodyComponentDistance(distanceMil)) {
|
|
801
|
-
return
|
|
844
|
+
return !PcbScene3dBuilder.#isIncompatiblePackageBodyMatch(
|
|
845
|
+
componentBody,
|
|
846
|
+
component,
|
|
847
|
+
distanceMil
|
|
848
|
+
)
|
|
802
849
|
}
|
|
803
850
|
|
|
804
851
|
if (
|
|
@@ -819,6 +866,242 @@ export class PcbScene3dBuilder {
|
|
|
819
866
|
return bodyCount > 0 && bodyCount <= candidateCount
|
|
820
867
|
}
|
|
821
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Scores short-range package metadata matches above no-affinity anchors.
|
|
871
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
872
|
+
* @param {number} affinityScore Shared body/component identity score.
|
|
873
|
+
* @param {number} distanceMil Body/component anchor distance.
|
|
874
|
+
* @returns {number}
|
|
875
|
+
*/
|
|
876
|
+
static #nearPackageAffinityScore(
|
|
877
|
+
componentBody,
|
|
878
|
+
affinityScore,
|
|
879
|
+
distanceMil
|
|
880
|
+
) {
|
|
881
|
+
return PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
882
|
+
Number(affinityScore || 0) > 0 &&
|
|
883
|
+
Number(distanceMil || 0) <=
|
|
884
|
+
PcbScene3dBuilder.#NEAR_PACKAGE_AFFINITY_DISTANCE_MIL
|
|
885
|
+
? 1
|
|
886
|
+
: 0
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Checks whether an unmatched package-like body should be suppressed
|
|
891
|
+
* because it sits on an exact but incompatible component anchor.
|
|
892
|
+
* @param {{ name?: string, identifier?: string, positionMil?: { x?: number, y?: number } }} componentBody Component-body record.
|
|
893
|
+
* @param {{ x: number, y: number, pattern?: string, source?: string, modelPath?: string }[]} components PCB components.
|
|
894
|
+
* @returns {boolean}
|
|
895
|
+
*/
|
|
896
|
+
static #shouldDropUnmatchedPackageBody(componentBody, components) {
|
|
897
|
+
if (
|
|
898
|
+
!PcbScene3dBuilder.#isComponentPackageBody(componentBody) ||
|
|
899
|
+
PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody)
|
|
900
|
+
) {
|
|
901
|
+
return false
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const exactComponents = (
|
|
905
|
+
Array.isArray(components) ? components : []
|
|
906
|
+
).filter(
|
|
907
|
+
(component) =>
|
|
908
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
909
|
+
componentBody,
|
|
910
|
+
component
|
|
911
|
+
) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
return (
|
|
915
|
+
exactComponents.length > 0 &&
|
|
916
|
+
exactComponents.every(
|
|
917
|
+
(component) =>
|
|
918
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
919
|
+
componentBody,
|
|
920
|
+
component
|
|
921
|
+
) <= 0
|
|
922
|
+
)
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Checks whether a precise body/component pair is a package-family mismatch.
|
|
928
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
929
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string }} component PCB component.
|
|
930
|
+
* @param {number} distanceMil Body/component anchor distance.
|
|
931
|
+
* @returns {boolean}
|
|
932
|
+
*/
|
|
933
|
+
static #isIncompatiblePackageBodyMatch(
|
|
934
|
+
componentBody,
|
|
935
|
+
component,
|
|
936
|
+
distanceMil
|
|
937
|
+
) {
|
|
938
|
+
return (
|
|
939
|
+
Number(distanceMil || 0) <=
|
|
940
|
+
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL &&
|
|
941
|
+
PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
942
|
+
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
943
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
944
|
+
componentBody,
|
|
945
|
+
component
|
|
946
|
+
) <= 0
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Checks whether a component body names a package model rather than an
|
|
952
|
+
* authored board mechanical.
|
|
953
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
954
|
+
* @returns {boolean}
|
|
955
|
+
*/
|
|
956
|
+
static #isComponentPackageBody(componentBody) {
|
|
957
|
+
return PcbScene3dBuilder.#COMPONENT_PACKAGE_BODY_PATTERN.test(
|
|
958
|
+
PcbScene3dBuilder.#componentBodyIdentityText(componentBody)
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Checks whether a component body identity describes an authored hardware
|
|
964
|
+
* or connector anchor that can legitimately sit away from a footprint
|
|
965
|
+
* center.
|
|
966
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
967
|
+
* @returns {boolean}
|
|
968
|
+
*/
|
|
969
|
+
static #isAuthoredBodyIdentity(componentBody) {
|
|
970
|
+
return PcbScene3dBuilder.#AUTHORED_BODY_IDENTITY_PATTERN.test(
|
|
971
|
+
PcbScene3dBuilder.#componentBodyIdentityText(componentBody)
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Checks whether a layerless component body is a footprint-library
|
|
977
|
+
* placeholder rather than a board-side placement.
|
|
978
|
+
* @param {{ layer?: string, name?: string, identifier?: string, modelId?: string, checksum?: number | null, positionMil?: { x?: number, y?: number } }} componentBody Component body record.
|
|
979
|
+
* @param {object[]} componentBodies All component body records.
|
|
980
|
+
* @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} board Board envelope.
|
|
981
|
+
* @returns {boolean}
|
|
982
|
+
*/
|
|
983
|
+
static #shouldSuppressLayerlessBodyPlaceholder(
|
|
984
|
+
componentBody,
|
|
985
|
+
componentBodies,
|
|
986
|
+
board
|
|
987
|
+
) {
|
|
988
|
+
if (
|
|
989
|
+
!PcbScene3dBuilder.#isLayerlessComponentBody(componentBody) ||
|
|
990
|
+
PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody)
|
|
991
|
+
) {
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return (
|
|
996
|
+
!PcbScene3dBuilder.#isBodyPositionNearBoard(componentBody, board) ||
|
|
997
|
+
PcbScene3dBuilder.#hasLayeredEquivalentPackageBody(
|
|
998
|
+
componentBody,
|
|
999
|
+
componentBodies
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Checks whether a body lacks an authored mechanical layer.
|
|
1006
|
+
* @param {{ layer?: string }} componentBody Component body record.
|
|
1007
|
+
* @returns {boolean}
|
|
1008
|
+
*/
|
|
1009
|
+
static #isLayerlessComponentBody(componentBody) {
|
|
1010
|
+
return String(componentBody?.layer || '').trim() === ''
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Checks whether a layerless body duplicates a real board-side body.
|
|
1015
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Candidate layerless body.
|
|
1016
|
+
* @param {object[]} componentBodies All component body records.
|
|
1017
|
+
* @returns {boolean}
|
|
1018
|
+
*/
|
|
1019
|
+
static #hasLayeredEquivalentPackageBody(componentBody, componentBodies) {
|
|
1020
|
+
return (Array.isArray(componentBodies) ? componentBodies : []).some(
|
|
1021
|
+
(candidate) =>
|
|
1022
|
+
candidate !== componentBody &&
|
|
1023
|
+
!PcbScene3dBuilder.#isLayerlessComponentBody(candidate) &&
|
|
1024
|
+
PcbScene3dBuilder.#componentBodiesShareModelIdentity(
|
|
1025
|
+
componentBody,
|
|
1026
|
+
candidate
|
|
1027
|
+
) &&
|
|
1028
|
+
PcbScene3dBuilder.#distanceBetweenBodies(
|
|
1029
|
+
componentBody,
|
|
1030
|
+
candidate
|
|
1031
|
+
) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Checks whether two component bodies refer to the same external package.
|
|
1037
|
+
* @param {{ name?: string, identifier?: string, modelId?: string, checksum?: number | null }} left First body.
|
|
1038
|
+
* @param {{ name?: string, identifier?: string, modelId?: string, checksum?: number | null }} right Second body.
|
|
1039
|
+
* @returns {boolean}
|
|
1040
|
+
*/
|
|
1041
|
+
static #componentBodiesShareModelIdentity(left, right) {
|
|
1042
|
+
const leftModelId = PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1043
|
+
left?.modelId
|
|
1044
|
+
)
|
|
1045
|
+
const rightModelId = PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1046
|
+
right?.modelId
|
|
1047
|
+
)
|
|
1048
|
+
if (leftModelId && rightModelId && leftModelId === rightModelId) {
|
|
1049
|
+
return true
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const leftChecksum = Number(left?.checksum)
|
|
1053
|
+
const rightChecksum = Number(right?.checksum)
|
|
1054
|
+
if (
|
|
1055
|
+
Number.isFinite(leftChecksum) &&
|
|
1056
|
+
Number.isFinite(rightChecksum) &&
|
|
1057
|
+
leftChecksum === rightChecksum
|
|
1058
|
+
) {
|
|
1059
|
+
return true
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const leftName = PcbScene3dBuilder.#normalizeBodyNameToken(left)
|
|
1063
|
+
const rightName = PcbScene3dBuilder.#normalizeBodyNameToken(right)
|
|
1064
|
+
|
|
1065
|
+
return Boolean(leftName && rightName && leftName === rightName)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Normalizes a body model name or identifier for duplicate detection.
|
|
1070
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component body.
|
|
1071
|
+
* @returns {string}
|
|
1072
|
+
*/
|
|
1073
|
+
static #normalizeBodyNameToken(componentBody) {
|
|
1074
|
+
return PcbScene3dBuilder.#normalizeIdentityToken(
|
|
1075
|
+
componentBody?.name || componentBody?.identifier
|
|
1076
|
+
).replace(/\.[a-z0-9]+$/i, '')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Normalizes an identity token for case-insensitive comparisons.
|
|
1081
|
+
* @param {unknown} value Raw token.
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
static #normalizeIdentityToken(value) {
|
|
1085
|
+
return String(value || '')
|
|
1086
|
+
.trim()
|
|
1087
|
+
.toLowerCase()
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Builds searchable identity text for one component body.
|
|
1092
|
+
* @param {{ name?: string, identifier?: string, modelId?: string }} componentBody Component-body record.
|
|
1093
|
+
* @returns {string}
|
|
1094
|
+
*/
|
|
1095
|
+
static #componentBodyIdentityText(componentBody) {
|
|
1096
|
+
return [
|
|
1097
|
+
componentBody?.identifier,
|
|
1098
|
+
componentBody?.name,
|
|
1099
|
+
componentBody?.modelId
|
|
1100
|
+
]
|
|
1101
|
+
.map((value) => String(value || ''))
|
|
1102
|
+
.join(' ')
|
|
1103
|
+
}
|
|
1104
|
+
|
|
822
1105
|
/**
|
|
823
1106
|
* Pairs one unresolved repeated body group with a repeated component group
|
|
824
1107
|
* by preserving the dominant ordering axis and choosing the pairing that
|
|
@@ -1535,6 +1818,21 @@ export class PcbScene3dBuilder {
|
|
|
1535
1818
|
)
|
|
1536
1819
|
}
|
|
1537
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* Returns the euclidean distance between two component body anchors.
|
|
1823
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} left First body.
|
|
1824
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} right Second body.
|
|
1825
|
+
* @returns {number}
|
|
1826
|
+
*/
|
|
1827
|
+
static #distanceBetweenBodies(left, right) {
|
|
1828
|
+
return Math.hypot(
|
|
1829
|
+
Number(left?.positionMil?.x || 0) -
|
|
1830
|
+
Number(right?.positionMil?.x || 0),
|
|
1831
|
+
Number(left?.positionMil?.y || 0) -
|
|
1832
|
+
Number(right?.positionMil?.y || 0)
|
|
1833
|
+
)
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1538
1836
|
/**
|
|
1539
1837
|
* Returns true when one unresolved body anchor still lies close enough to
|
|
1540
1838
|
* the board envelope to be renderable without a component match.
|
|
@@ -81,7 +81,7 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
81
81
|
* Scores how strongly one component record appears to belong to one body
|
|
82
82
|
* record based on shared model/footprint tokens.
|
|
83
83
|
* @param {{ name?: string, identifier?: string }} componentBody
|
|
84
|
-
* @param {{ pattern?: string, source?: string, modelPath?: string }} component
|
|
84
|
+
* @param {{ pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }} component
|
|
85
85
|
* @returns {number}
|
|
86
86
|
*/
|
|
87
87
|
static scoreBodyComponentAffinity(componentBody, component) {
|
|
@@ -94,7 +94,11 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
94
94
|
PcbScene3dPlacementSideResolver.#collectMeaningfulTokens([
|
|
95
95
|
component?.pattern,
|
|
96
96
|
component?.source,
|
|
97
|
-
component?.modelPath
|
|
97
|
+
component?.modelPath,
|
|
98
|
+
component?.description,
|
|
99
|
+
...PcbScene3dPlacementSideResolver.#componentPackageMetadata(
|
|
100
|
+
component
|
|
101
|
+
)
|
|
98
102
|
])
|
|
99
103
|
let score = 0
|
|
100
104
|
|
|
@@ -107,6 +111,34 @@ export class PcbScene3dPlacementSideResolver {
|
|
|
107
111
|
return score
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Resolves package-related component metadata that can identify generic
|
|
116
|
+
* embedded 3D bodies when pattern/source only name the electrical part.
|
|
117
|
+
* @param {{ parameters?: object, provenance?: object } | null | undefined} component Component record.
|
|
118
|
+
* @returns {string[]}
|
|
119
|
+
*/
|
|
120
|
+
static #componentPackageMetadata(component) {
|
|
121
|
+
const parameters =
|
|
122
|
+
component?.parameters && typeof component.parameters === 'object'
|
|
123
|
+
? component.parameters
|
|
124
|
+
: {}
|
|
125
|
+
const provenance =
|
|
126
|
+
component?.provenance && typeof component.provenance === 'object'
|
|
127
|
+
? component.provenance
|
|
128
|
+
: {}
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
parameters['Package / Case'],
|
|
132
|
+
parameters['Supplier Device Package'],
|
|
133
|
+
parameters['Part Description'],
|
|
134
|
+
parameters.Package,
|
|
135
|
+
provenance.footprintDescription,
|
|
136
|
+
provenance.sourceLibReference,
|
|
137
|
+
provenance.sourceFootprintLibrary,
|
|
138
|
+
provenance.sourceFootprintLibraryName
|
|
139
|
+
].map((value) => String(value || ''))
|
|
140
|
+
}
|
|
141
|
+
|
|
110
142
|
/**
|
|
111
143
|
* Resolves side from the normalized component layer.
|
|
112
144
|
* @param {string | undefined} layer
|