pcb-scene3d-viewer 1.1.21 → 1.1.22

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.
@@ -0,0 +1,105 @@
1
+ import { PcbScene3dBodyColor } from './PcbScene3dBodyColor.mjs'
2
+ import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
3
+ import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
4
+
5
+ /**
6
+ * Builds procedural fallback body render roots for PCB components.
7
+ */
8
+ export class PcbScene3dFallbackBodyFactory {
9
+ /**
10
+ * Builds one procedural fallback body root.
11
+ * @param {any} THREE Three.js namespace.
12
+ * @param {{ designator?: string, positionMil: { x: number, y: number, z: number }, rotationDeg: number, mountSide: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } } }} component Component scene entry.
13
+ * @param {{ companionBase?: boolean }} [options] Rendering options.
14
+ * @returns {{ rootGroup: any, adjustmentGroup: any }}
15
+ */
16
+ static build(THREE, component, options = {}) {
17
+ const family = component.body.family
18
+ const size = component.body.sizeMil
19
+ const material = new THREE.MeshStandardMaterial({
20
+ color: PcbScene3dFallbackBodyFactory.#resolveColor(family, options),
21
+ roughness: 0.72,
22
+ metalness: family === 'chip' ? 0.12 : 0.08
23
+ })
24
+ const mesh = PcbScene3dFallbackBodyFactory.#buildMesh(
25
+ THREE,
26
+ family,
27
+ size,
28
+ material
29
+ )
30
+ const mountRig = PcbScene3dMountRig.create(THREE, component)
31
+ const adjustmentGroup =
32
+ PcbScene3dFallbackBodyFactory.#buildAdjustmentGroup(
33
+ THREE,
34
+ component
35
+ )
36
+
37
+ mountRig.rootGroup.userData.scene3dSelection = {
38
+ designator: String(component?.designator || 'component'),
39
+ sourceType: 'component'
40
+ }
41
+ adjustmentGroup.add(mesh)
42
+ mountRig.faceGroup.add(adjustmentGroup)
43
+
44
+ return {
45
+ rootGroup: mountRig.rootGroup,
46
+ adjustmentGroup
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolves the fallback body color.
52
+ * @param {string} family Package family.
53
+ * @param {{ companionBase?: boolean }} options Rendering options.
54
+ * @returns {number}
55
+ */
56
+ static #resolveColor(family, options) {
57
+ return options?.companionBase
58
+ ? 0x808080
59
+ : PcbScene3dBodyColor.resolve(family)
60
+ }
61
+
62
+ /**
63
+ * Builds the body mesh for one package family.
64
+ * @param {any} THREE Three.js namespace.
65
+ * @param {string} family Package family.
66
+ * @param {{ width: number, depth: number, height: number }} size Body size.
67
+ * @param {any} material Mesh material.
68
+ * @returns {any}
69
+ */
70
+ static #buildMesh(THREE, family, size, material) {
71
+ if (family === 'radial-capacitor' || family === 'test-point') {
72
+ return new THREE.Mesh(
73
+ new THREE.CylinderGeometry(
74
+ size.width / 2,
75
+ size.width / 2,
76
+ size.height,
77
+ 28
78
+ ),
79
+ material
80
+ )
81
+ }
82
+
83
+ return new THREE.Mesh(
84
+ new THREE.BoxGeometry(size.width, size.depth, size.height),
85
+ material
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Builds the transform node used by live component adjustments.
91
+ * @param {any} THREE Three.js namespace.
92
+ * @param {{ designator?: string }} component Component scene entry.
93
+ * @returns {any}
94
+ */
95
+ static #buildAdjustmentGroup(THREE, component) {
96
+ const adjustmentGroup = new THREE.Group()
97
+ adjustmentGroup.userData.scene3dAdjustmentTarget = true
98
+ adjustmentGroup.userData.scene3dAdjustmentDesignator = String(
99
+ component?.designator || 'component'
100
+ )
101
+ adjustmentGroup.userData.scene3dAdjustmentBaseline =
102
+ PcbScene3dComponentAdjustment.neutral()
103
+ return adjustmentGroup
104
+ }
105
+ }
@@ -74,9 +74,66 @@ export class PcbScene3dFallbackVisibility {
74
74
  return
75
75
  }
76
76
 
77
+ const stitchedCompanion =
78
+ PcbScene3dFallbackVisibility.shouldKeepExternalCompanion(
79
+ rootObject,
80
+ toggles
81
+ )
77
82
  rootObject.visible =
78
- showFallbackBodies && !hideForLoadedExternal
83
+ stitchedCompanion ||
84
+ (showFallbackBodies && !hideForLoadedExternal)
79
85
  })
80
86
  })
81
87
  }
88
+
89
+ /**
90
+ * Checks whether any fallback root should keep the fallback group visible
91
+ * as part of a stitched external-model package.
92
+ * @param {Map<string, Set<any>>} fallbackRoots Fallback root registry.
93
+ * @param {{ 'external-models'?: boolean }} toggles Detail toggles.
94
+ * @returns {boolean}
95
+ */
96
+ static hasVisibleExternalCompanion(fallbackRoots, toggles) {
97
+ if (
98
+ !(fallbackRoots instanceof Map) ||
99
+ !Boolean(toggles?.['external-models'])
100
+ ) {
101
+ return false
102
+ }
103
+
104
+ let hasCompanion = false
105
+ fallbackRoots.forEach((roots) => {
106
+ roots?.forEach?.((rootObject) => {
107
+ hasCompanion =
108
+ hasCompanion ||
109
+ PcbScene3dFallbackVisibility.#isExternalCompanion(
110
+ rootObject
111
+ )
112
+ })
113
+ })
114
+ return hasCompanion
115
+ }
116
+
117
+ /**
118
+ * Checks whether one fallback body should stay visible as a stitched
119
+ * external-model companion under the active toggles.
120
+ * @param {any} rootObject Fallback root object.
121
+ * @param {{ 'external-models'?: boolean }} toggles Detail toggles.
122
+ * @returns {boolean}
123
+ */
124
+ static shouldKeepExternalCompanion(rootObject, toggles) {
125
+ return (
126
+ Boolean(toggles?.['external-models']) &&
127
+ PcbScene3dFallbackVisibility.#isExternalCompanion(rootObject)
128
+ )
129
+ }
130
+
131
+ /**
132
+ * Checks whether a fallback body is part of a stitched external model.
133
+ * @param {any} rootObject Fallback root object.
134
+ * @returns {boolean}
135
+ */
136
+ static #isExternalCompanion(rootObject) {
137
+ return Boolean(rootObject?.userData?.scene3dFallbackExternalCompanion)
138
+ }
82
139
  }
@@ -37,9 +37,16 @@ export class PcbScene3dRenderGroupVisibility {
37
37
  state?.groups?.get('copper'),
38
38
  boardAssemblyActive
39
39
  )
40
+ const showFallbackBodies =
41
+ !boardAssemblyActive &&
42
+ (Boolean(state?.toggles?.['fallback-bodies']) ||
43
+ PcbScene3dFallbackVisibility.hasVisibleExternalCompanion(
44
+ state?.fallbackBodyRoots,
45
+ state?.toggles
46
+ ))
40
47
  PcbScene3dRenderGroupVisibility.#setVisible(
41
48
  state?.groups?.get('fallback-bodies'),
42
- Boolean(state?.toggles?.['fallback-bodies']) && !boardAssemblyActive
49
+ showFallbackBodies
43
50
  )
44
51
 
45
52
  PcbScene3dFallbackVisibility.applyVisibility(
@@ -1,18 +1,19 @@
1
1
  import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFactory.mjs'
2
- import { PcbScene3dBodyColor } from './PcbScene3dBodyColor.mjs'
3
2
  import { PcbScene3dCameraRig } from './PcbScene3dCameraRig.mjs'
4
3
  import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
5
4
  import { PcbScene3dComponentVisibility } from './PcbScene3dComponentVisibility.mjs'
6
5
  import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
7
6
  import { PcbScene3dComponentAdjustmentRegistry } from './PcbScene3dComponentAdjustmentRegistry.mjs'
7
+ import { PcbScene3dCompanionBasePlacementAdjuster } from './PcbScene3dCompanionBasePlacementAdjuster.mjs'
8
8
  import { PcbScene3dCopperDetailGroupBuilder } from './PcbScene3dCopperDetailGroupBuilder.mjs'
9
9
  import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
10
10
  import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
11
11
  import { PcbScene3dDrillVoidFactory } from './PcbScene3dDrillVoidFactory.mjs'
12
+ import { PcbScene3dExternalCompanionFallback } from './PcbScene3dExternalCompanionFallback.mjs'
12
13
  import { PcbScene3dExternalModels } from './PcbScene3dExternalModels.mjs'
14
+ import { PcbScene3dFallbackBodyFactory } from './PcbScene3dFallbackBodyFactory.mjs'
13
15
  import { PcbScene3dFallbackVisibility } from './PcbScene3dFallbackVisibility.mjs'
14
16
  import { PcbScene3dInteractionHints } from './PcbScene3dInteractionHints.mjs'
15
- import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
16
17
  import { PcbScene3dModelSearchPlacement } from './PcbScene3dModelSearchPlacement.mjs'
17
18
  import { PcbScene3dPresetState } from './PcbScene3dPresetState.mjs'
18
19
  import { PcbScene3dRenderGroupVisibility } from './PcbScene3dRenderGroupVisibility.mjs'
@@ -22,6 +23,7 @@ import { PcbScene3dSilkscreenChunkedFactory } from './PcbScene3dSilkscreenChunke
22
23
  import { PcbScene3dTrueTypeTextFactory } from './PcbScene3dTrueTypeTextFactory.mjs'
23
24
  import { PcbScene3dSelectionResolver } from './PcbScene3dSelectionResolver.mjs'
24
25
  import { PcbScene3dSelectionStyler } from './PcbScene3dSelectionStyler.mjs'
26
+ import { PcbScene3dStaticBodyFactory } from './PcbScene3dStaticBodyFactory.mjs'
25
27
  import { PcbScene3dViewportResize } from './PcbScene3dViewportResize.mjs'
26
28
  import { PcbScene3dViewScale } from './PcbScene3dViewScale.mjs'
27
29
  const SILKSCREEN_COPPER_CLEARANCE_MIL = 0.12
@@ -37,6 +39,7 @@ const Z_MIL = {
37
39
  export class PcbScene3dRuntime {
38
40
  #viewportNode
39
41
  #sceneDescription
42
+ #placementSceneDescription
40
43
  #hooks
41
44
  #toggles
42
45
  #groups
@@ -78,6 +81,8 @@ export class PcbScene3dRuntime {
78
81
  PcbScene3dRuntime.#normalizeSceneDescription(sceneDescription)
79
82
  this.#viewportNode = viewportNode
80
83
  this.#sceneDescription = renderModel
84
+ this.#placementSceneDescription =
85
+ PcbScene3dCompanionBasePlacementAdjuster.adjust(renderModel)
81
86
  this.#hooks = hooks
82
87
  this.#toggles = {
83
88
  'external-models': true,
@@ -385,19 +390,60 @@ export class PcbScene3dRuntime {
385
390
  this.#groups.set('copper', copperGroup)
386
391
  this.#rootGroup.add(copperGroup)
387
392
  const fallbackBodiesGroup = new THREE.Group()
393
+ const staticBodiesGroup = new THREE.Group()
388
394
  const externalModelsGroup = new THREE.Group()
389
395
  this.#sceneDescription.components.forEach((component) => {
390
- const fallbackBody = this.#buildFallbackBody(component)
391
- fallbackBodiesGroup.add(fallbackBody)
396
+ if (component?.renderFallbackBody === false) {
397
+ return
398
+ }
399
+
400
+ const isCompanionBase =
401
+ PcbScene3dExternalCompanionFallback.shouldKeepFallback(
402
+ this.#sceneDescription,
403
+ component
404
+ )
405
+ const fallbackBody = PcbScene3dFallbackBodyFactory.build(
406
+ THREE,
407
+ component,
408
+ { companionBase: isCompanionBase }
409
+ )
410
+ if (isCompanionBase) {
411
+ fallbackBody.rootGroup.userData.scene3dFallbackExternalCompanion = true
412
+ }
413
+ fallbackBodiesGroup.add(fallbackBody.rootGroup)
392
414
  PcbScene3dFallbackVisibility.registerFallbackRoot(
393
415
  this.#fallbackBodyRoots,
394
416
  component?.designator,
395
- fallbackBody
417
+ fallbackBody.rootGroup
418
+ )
419
+ this.#registerSelectionRoot(
420
+ component?.designator,
421
+ fallbackBody.rootGroup
422
+ )
423
+ this.#componentAdjustmentRegistry.register(
424
+ component?.designator,
425
+ fallbackBody.adjustmentGroup
426
+ )
427
+ })
428
+ PcbScene3dStaticBodyFactory.buildMany(
429
+ THREE,
430
+ this.#placementSceneDescription.staticBodyPlacements
431
+ ).forEach((staticBody) => {
432
+ staticBodiesGroup.add(staticBody.rootGroup)
433
+ this.#registerSelectionRoot(
434
+ staticBody.placement?.designator,
435
+ staticBody.rootGroup
436
+ )
437
+ this.#componentAdjustmentRegistry.register(
438
+ staticBody.placement?.designator,
439
+ staticBody.adjustmentGroup
396
440
  )
397
441
  })
398
442
  this.#groups.set('fallback-bodies', fallbackBodiesGroup)
443
+ this.#groups.set('static-bodies', staticBodiesGroup)
399
444
  this.#groups.set('external-models', externalModelsGroup)
400
445
  this.#rootGroup.add(fallbackBodiesGroup)
446
+ this.#rootGroup.add(staticBodiesGroup)
401
447
  this.#rootGroup.add(externalModelsGroup)
402
448
  const boardSpan = Math.max(board.widthMil, board.heightMil, 1)
403
449
  this.#scene.fog = new THREE.Fog(
@@ -536,63 +582,6 @@ export class PcbScene3dRuntime {
536
582
  }
537
583
  }
538
584
 
539
- /**
540
- * Builds one procedural fallback body mesh.
541
- * @param {{ positionMil: { x: number, y: number, z: number }, rotationDeg: number, mountSide: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } } }} component
542
- * @returns {any}
543
- */
544
- #buildFallbackBody(component) {
545
- const THREE = this.#three
546
- const family = component.body.family
547
- const size = component.body.sizeMil
548
- const material = new THREE.MeshStandardMaterial({
549
- color: PcbScene3dBodyColor.resolve(family),
550
- roughness: 0.72,
551
- metalness: family === 'chip' ? 0.12 : 0.08
552
- })
553
-
554
- let mesh
555
- if (family === 'radial-capacitor' || family === 'test-point') {
556
- mesh = new THREE.Mesh(
557
- new THREE.CylinderGeometry(
558
- size.width / 2,
559
- size.width / 2,
560
- size.height,
561
- 28
562
- ),
563
- material
564
- )
565
- } else {
566
- mesh = new THREE.Mesh(
567
- new THREE.BoxGeometry(size.width, size.depth, size.height),
568
- material
569
- )
570
- }
571
-
572
- const mountRig = PcbScene3dMountRig.create(THREE, component)
573
- const rootGroup = mountRig.rootGroup
574
- const adjustmentGroup = new THREE.Group()
575
-
576
- rootGroup.userData.scene3dSelection = {
577
- designator: String(component?.designator || 'component'),
578
- sourceType: 'component'
579
- }
580
- adjustmentGroup.userData.scene3dAdjustmentTarget = true
581
- adjustmentGroup.userData.scene3dAdjustmentDesignator = String(
582
- component?.designator || 'component'
583
- )
584
- adjustmentGroup.userData.scene3dAdjustmentBaseline =
585
- PcbScene3dComponentAdjustment.neutral()
586
- this.#registerSelectionRoot(component?.designator, rootGroup)
587
- this.#componentAdjustmentRegistry.register(
588
- component?.designator,
589
- adjustmentGroup
590
- )
591
- adjustmentGroup.add(mesh)
592
- mountRig.faceGroup.add(adjustmentGroup)
593
- return rootGroup
594
- }
595
-
596
585
  /**
597
586
  * Attempts to load any resolved external 3D models.
598
587
  * @returns {Promise<void>}
@@ -605,7 +594,7 @@ export class PcbScene3dRuntime {
605
594
 
606
595
  const diagnostics = await PcbScene3dExternalModels.loadIntoScene({
607
596
  three: this.#three,
608
- sceneDescription: this.#sceneDescription,
597
+ sceneDescription: this.#placementSceneDescription,
609
598
  externalModelsGroup,
610
599
  modelViewScale: PcbScene3dRuntime.resolveViewScale(
611
600
  this.#presetState.get(),
@@ -770,6 +759,7 @@ export class PcbScene3dRuntime {
770
759
  })
771
760
  PcbScene3dComponentVisibility.apply({
772
761
  selectionRoots: this.#selectionRoots,
762
+ selectedDesignator: this.#selectedDesignator,
773
763
  hiddenDesignators: this.#hiddenComponentDesignators,
774
764
  fallbackBodyRoots: this.#fallbackBodyRoots,
775
765
  loadedExternalModelDesignators:
@@ -891,6 +881,7 @@ export class PcbScene3dRuntime {
891
881
 
892
882
  const selectableRoots = [
893
883
  this.#groups.get('external-models'),
884
+ this.#groups.get('static-bodies'),
894
885
  this.#groups.get('fallback-bodies')
895
886
  ].filter((group) => group && group.visible !== false)
896
887
  const intersections = this.#raycaster.intersectObjects(
@@ -0,0 +1,241 @@
1
+ import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
2
+ import { PcbScene3dMountRig } from './PcbScene3dMountRig.mjs'
3
+
4
+ /**
5
+ * Builds authored static component-body render roots.
6
+ */
7
+ export class PcbScene3dStaticBodyFactory {
8
+ static #DEFAULT_COLOR = 0x808080
9
+
10
+ /**
11
+ * Builds all renderable static body roots.
12
+ * @param {any} THREE Three.js namespace.
13
+ * @param {object[]} placements Static body placements.
14
+ * @returns {{ rootGroup: any, adjustmentGroup: any, placement: object }[]}
15
+ */
16
+ static buildMany(THREE, placements) {
17
+ return (Array.isArray(placements) ? placements : [])
18
+ .map((placement) =>
19
+ PcbScene3dStaticBodyFactory.build(THREE, placement)
20
+ )
21
+ .filter(Boolean)
22
+ }
23
+
24
+ /**
25
+ * Builds one authored static body root.
26
+ * @param {any} THREE Three.js namespace.
27
+ * @param {{ designator?: string, mountSide?: string, rotationDeg?: number, positionMil?: { x?: number, y?: number, z?: number }, geometry?: object, bodyColor?: object, bodyOpacity?: number }} placement Static body placement.
28
+ * @returns {{ rootGroup: any, adjustmentGroup: any, placement: object } | null}
29
+ */
30
+ static build(THREE, placement) {
31
+ const geometry = PcbScene3dStaticBodyFactory.#buildGeometry(
32
+ THREE,
33
+ placement?.geometry
34
+ )
35
+ if (!geometry) {
36
+ return null
37
+ }
38
+
39
+ const material = new THREE.MeshStandardMaterial({
40
+ color: PcbScene3dStaticBodyFactory.#resolveColor(placement),
41
+ roughness: 0.7,
42
+ metalness: 0.08,
43
+ transparent:
44
+ PcbScene3dStaticBodyFactory.#resolveOpacity(placement) < 1,
45
+ opacity: PcbScene3dStaticBodyFactory.#resolveOpacity(placement)
46
+ })
47
+ const mesh = new THREE.Mesh(geometry, material)
48
+ const mountRig = PcbScene3dMountRig.create(THREE, placement)
49
+ const adjustmentGroup =
50
+ PcbScene3dStaticBodyFactory.#buildAdjustmentGroup(THREE, placement)
51
+
52
+ mountRig.rootGroup.userData.scene3dSelection = {
53
+ designator: String(placement?.designator || 'static-body'),
54
+ sourceType: 'static-body'
55
+ }
56
+ PcbScene3dStaticBodyFactory.#applyVariantMetadata(
57
+ mountRig.rootGroup,
58
+ placement
59
+ )
60
+ adjustmentGroup.add(mesh)
61
+ mountRig.faceGroup.add(adjustmentGroup)
62
+
63
+ return {
64
+ rootGroup: mountRig.rootGroup,
65
+ adjustmentGroup,
66
+ placement
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Builds the geometry for one supported static body.
72
+ * @param {any} THREE Three.js namespace.
73
+ * @param {{ kind?: string, verticesMil?: { x?: number, y?: number }[], heightMil?: number, radiusMil?: number }} geometry Static body geometry.
74
+ * @returns {any | null}
75
+ */
76
+ static #buildGeometry(THREE, geometry) {
77
+ const kind = String(geometry?.kind || '').toLowerCase()
78
+ if (kind === 'extruded-polygon') {
79
+ return PcbScene3dStaticBodyFactory.#buildExtrudedPolygon(
80
+ THREE,
81
+ geometry
82
+ )
83
+ }
84
+ if (kind === 'cylinder') {
85
+ return PcbScene3dStaticBodyFactory.#buildCylinder(THREE, geometry)
86
+ }
87
+ if (kind === 'cone') {
88
+ return PcbScene3dStaticBodyFactory.#buildCone(THREE, geometry)
89
+ }
90
+ if (kind === 'sphere') {
91
+ return PcbScene3dStaticBodyFactory.#buildSphere(THREE, geometry)
92
+ }
93
+
94
+ return null
95
+ }
96
+
97
+ /**
98
+ * Builds an extruded polygon geometry centered on its authored Z midpoint.
99
+ * @param {any} THREE Three.js namespace.
100
+ * @param {{ verticesMil?: { x?: number, y?: number }[], heightMil?: number }} geometry Static body geometry.
101
+ * @returns {any | null}
102
+ */
103
+ static #buildExtrudedPolygon(THREE, geometry) {
104
+ const vertices = Array.isArray(geometry?.verticesMil)
105
+ ? geometry.verticesMil
106
+ : []
107
+ const heightMil = Number(geometry?.heightMil || 0)
108
+ if (vertices.length < 3 || !(heightMil > 0)) {
109
+ return null
110
+ }
111
+
112
+ const shape = new THREE.Shape()
113
+ shape.moveTo(Number(vertices[0].x || 0), Number(vertices[0].y || 0))
114
+ vertices.slice(1).forEach((vertex) => {
115
+ shape.lineTo(Number(vertex.x || 0), Number(vertex.y || 0))
116
+ })
117
+ shape.closePath()
118
+
119
+ const extrudedGeometry = new THREE.ExtrudeGeometry(shape, {
120
+ depth: heightMil,
121
+ bevelEnabled: false
122
+ })
123
+ extrudedGeometry.translate?.(0, 0, -heightMil / 2)
124
+ return extrudedGeometry
125
+ }
126
+
127
+ /**
128
+ * Builds a cylinder body geometry.
129
+ * @param {any} THREE Three.js namespace.
130
+ * @param {{ radiusMil?: number, heightMil?: number }} geometry Static body geometry.
131
+ * @returns {any | null}
132
+ */
133
+ static #buildCylinder(THREE, geometry) {
134
+ const radiusMil = Number(geometry?.radiusMil || 0)
135
+ const heightMil = Number(geometry?.heightMil || 0)
136
+ if (!(radiusMil > 0) || !(heightMil > 0)) {
137
+ return null
138
+ }
139
+
140
+ return new THREE.CylinderGeometry(radiusMil, radiusMil, heightMil, 32)
141
+ }
142
+
143
+ /**
144
+ * Builds a cone body geometry.
145
+ * @param {any} THREE Three.js namespace.
146
+ * @param {{ radiusMil?: number, heightMil?: number }} geometry Static body geometry.
147
+ * @returns {any | null}
148
+ */
149
+ static #buildCone(THREE, geometry) {
150
+ const radiusMil = Number(geometry?.radiusMil || 0)
151
+ const heightMil = Number(geometry?.heightMil || 0)
152
+ if (!(radiusMil > 0) || !(heightMil > 0)) {
153
+ return null
154
+ }
155
+
156
+ if (typeof THREE.ConeGeometry === 'function') {
157
+ return new THREE.ConeGeometry(radiusMil, heightMil, 32)
158
+ }
159
+
160
+ return new THREE.CylinderGeometry(0, radiusMil, heightMil, 32)
161
+ }
162
+
163
+ /**
164
+ * Builds a sphere body geometry.
165
+ * @param {any} THREE Three.js namespace.
166
+ * @param {{ radiusMil?: number }} geometry Static body geometry.
167
+ * @returns {any | null}
168
+ */
169
+ static #buildSphere(THREE, geometry) {
170
+ const radiusMil = Number(geometry?.radiusMil || 0)
171
+ if (!(radiusMil > 0) || typeof THREE.SphereGeometry !== 'function') {
172
+ return null
173
+ }
174
+
175
+ return new THREE.SphereGeometry(radiusMil, 32, 16)
176
+ }
177
+
178
+ /**
179
+ * Builds the transform node used by live component adjustments.
180
+ * @param {any} THREE Three.js namespace.
181
+ * @param {{ designator?: string }} placement Static body placement.
182
+ * @returns {any}
183
+ */
184
+ static #buildAdjustmentGroup(THREE, placement) {
185
+ const adjustmentGroup = new THREE.Group()
186
+ adjustmentGroup.userData.scene3dAdjustmentTarget = true
187
+ adjustmentGroup.userData.scene3dAdjustmentDesignator = String(
188
+ placement?.designator || 'static-body'
189
+ )
190
+ adjustmentGroup.userData.scene3dAdjustmentBaseline =
191
+ PcbScene3dComponentAdjustment.neutral()
192
+ return adjustmentGroup
193
+ }
194
+
195
+ /**
196
+ * Copies authored variant grouping metadata onto the selectable root.
197
+ * @param {any} rootGroup Static body root group.
198
+ * @param {{ coLocatedVariantGroupKey?: string }} placement Static body placement.
199
+ * @returns {void}
200
+ */
201
+ static #applyVariantMetadata(rootGroup, placement) {
202
+ const groupKey = String(
203
+ placement?.coLocatedVariantGroupKey || ''
204
+ ).trim()
205
+ if (groupKey) {
206
+ rootGroup.userData.scene3dVariantGroupKey = groupKey
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Resolves static body display color.
212
+ * @param {{ bodyColor?: object, geometry?: object }} placement Static body placement.
213
+ * @returns {number}
214
+ */
215
+ static #resolveColor(placement) {
216
+ const rawColor = Number(
217
+ placement?.bodyColor?.raw ??
218
+ placement?.geometry?.bodyColor?.raw ??
219
+ placement?.geometry?.color
220
+ )
221
+ return Number.isInteger(rawColor)
222
+ ? rawColor
223
+ : PcbScene3dStaticBodyFactory.#DEFAULT_COLOR
224
+ }
225
+
226
+ /**
227
+ * Resolves static body opacity.
228
+ * @param {{ bodyOpacity?: number, geometry?: object }} placement Static body placement.
229
+ * @returns {number}
230
+ */
231
+ static #resolveOpacity(placement) {
232
+ const opacity = Number(
233
+ placement?.bodyOpacity ?? placement?.geometry?.bodyOpacity
234
+ )
235
+ if (!Number.isFinite(opacity)) {
236
+ return 1
237
+ }
238
+
239
+ return Math.min(1, Math.max(0, opacity))
240
+ }
241
+ }