pcb-scene3d-viewer 1.1.21 → 1.1.31
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 +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +3 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds flat board-surface markers for the currently selected component.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dSelectionMarkerFactory {
|
|
5
|
+
static #MARKER_PADDING_MIL = 8
|
|
6
|
+
static #MARKER_Z_OFFSET_MIL = 8
|
|
7
|
+
static #MIN_MARKER_SPAN_MIL = 24
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds one selected-component outline marker.
|
|
11
|
+
* @param {any} THREE Three.js namespace.
|
|
12
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
13
|
+
* @param {string} designator Selected component designator.
|
|
14
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint Board/detail coordinate normalizer.
|
|
15
|
+
* @param {{ color?: number }} [options] Marker options.
|
|
16
|
+
* @returns {any | null}
|
|
17
|
+
*/
|
|
18
|
+
static build(
|
|
19
|
+
THREE,
|
|
20
|
+
sceneDescription,
|
|
21
|
+
designator,
|
|
22
|
+
normalizePoint,
|
|
23
|
+
options = {}
|
|
24
|
+
) {
|
|
25
|
+
if (!THREE || typeof normalizePoint !== 'function') {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const component =
|
|
30
|
+
PcbScene3dSelectionMarkerFactory.#componentByDesignator(
|
|
31
|
+
sceneDescription,
|
|
32
|
+
designator
|
|
33
|
+
)
|
|
34
|
+
if (!component) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const bounds =
|
|
39
|
+
PcbScene3dSelectionMarkerFactory.#boundsFromPads(
|
|
40
|
+
PcbScene3dSelectionMarkerFactory.#componentPads(
|
|
41
|
+
sceneDescription,
|
|
42
|
+
component
|
|
43
|
+
)
|
|
44
|
+
) ||
|
|
45
|
+
PcbScene3dSelectionMarkerFactory.#boundsFromComponentBody(
|
|
46
|
+
component,
|
|
47
|
+
sceneDescription?.board
|
|
48
|
+
)
|
|
49
|
+
const paddedBounds =
|
|
50
|
+
PcbScene3dSelectionMarkerFactory.#expandBounds(bounds)
|
|
51
|
+
if (!paddedBounds) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const z = PcbScene3dSelectionMarkerFactory.#markerZ(
|
|
56
|
+
sceneDescription?.board,
|
|
57
|
+
component
|
|
58
|
+
)
|
|
59
|
+
const points = PcbScene3dSelectionMarkerFactory.#markerPoints(
|
|
60
|
+
paddedBounds,
|
|
61
|
+
z,
|
|
62
|
+
normalizePoint
|
|
63
|
+
)
|
|
64
|
+
const geometry = new THREE.BufferGeometry()
|
|
65
|
+
geometry.setAttribute(
|
|
66
|
+
'position',
|
|
67
|
+
new THREE.Float32BufferAttribute(points.flat(), 3)
|
|
68
|
+
)
|
|
69
|
+
const material = new THREE.LineBasicMaterial({
|
|
70
|
+
color: Number(options?.color || 0x14c5e6),
|
|
71
|
+
transparent: true,
|
|
72
|
+
opacity: 1,
|
|
73
|
+
depthTest: false,
|
|
74
|
+
depthWrite: false
|
|
75
|
+
})
|
|
76
|
+
const line = new THREE.LineLoop(geometry, material)
|
|
77
|
+
line.renderOrder = 1000
|
|
78
|
+
|
|
79
|
+
const root = new THREE.Group()
|
|
80
|
+
root.userData.scene3dSelectionMarker = {
|
|
81
|
+
designator: String(component.designator || designator)
|
|
82
|
+
}
|
|
83
|
+
root.add(line)
|
|
84
|
+
return root
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Finds one component by designator.
|
|
89
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
90
|
+
* @param {string} designator Component designator.
|
|
91
|
+
* @returns {object | null}
|
|
92
|
+
*/
|
|
93
|
+
static #componentByDesignator(sceneDescription, designator) {
|
|
94
|
+
const selectedDesignator = String(designator || '').trim()
|
|
95
|
+
if (!selectedDesignator) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
(Array.isArray(sceneDescription?.components)
|
|
101
|
+
? sceneDescription.components
|
|
102
|
+
: []
|
|
103
|
+
).find(
|
|
104
|
+
(component) =>
|
|
105
|
+
String(component?.designator || '').trim() ===
|
|
106
|
+
selectedDesignator
|
|
107
|
+
) || null
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolves pads owned by one scene component.
|
|
113
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
114
|
+
* @param {{ componentIndex?: number | null }} component Scene component.
|
|
115
|
+
* @returns {object[]}
|
|
116
|
+
*/
|
|
117
|
+
static #componentPads(sceneDescription, component) {
|
|
118
|
+
const componentIndex = Number(component?.componentIndex)
|
|
119
|
+
if (!Number.isFinite(componentIndex)) {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
Array.isArray(sceneDescription?.detail?.pads)
|
|
125
|
+
? sceneDescription.detail.pads
|
|
126
|
+
: []
|
|
127
|
+
).filter((pad) => Number(pad?.componentIndex) === componentIndex)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolves an axis-aligned board-space bounds box from owned pad surfaces.
|
|
132
|
+
* @param {object[]} pads Pad records.
|
|
133
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
134
|
+
*/
|
|
135
|
+
static #boundsFromPads(pads) {
|
|
136
|
+
const points = []
|
|
137
|
+
;(pads || []).forEach((pad) => {
|
|
138
|
+
points.push(...PcbScene3dSelectionMarkerFactory.#padCorners(pad))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return PcbScene3dSelectionMarkerFactory.#boundsFromPoints(points)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves conservative board-space corners for one pad.
|
|
146
|
+
* @param {object} pad Pad record.
|
|
147
|
+
* @returns {{ x: number, y: number }[]}
|
|
148
|
+
*/
|
|
149
|
+
static #padCorners(pad) {
|
|
150
|
+
const x = Number(pad?.x)
|
|
151
|
+
const y = Number(pad?.y)
|
|
152
|
+
const width = PcbScene3dSelectionMarkerFactory.#maxPositive([
|
|
153
|
+
pad?.sizeTopX,
|
|
154
|
+
pad?.sizeMidX,
|
|
155
|
+
pad?.sizeBottomX,
|
|
156
|
+
pad?.holeDiameter
|
|
157
|
+
])
|
|
158
|
+
const height = PcbScene3dSelectionMarkerFactory.#maxPositive([
|
|
159
|
+
pad?.sizeTopY,
|
|
160
|
+
pad?.sizeMidY,
|
|
161
|
+
pad?.sizeBottomY,
|
|
162
|
+
pad?.holeDiameter
|
|
163
|
+
])
|
|
164
|
+
if (
|
|
165
|
+
!Number.isFinite(x) ||
|
|
166
|
+
!Number.isFinite(y) ||
|
|
167
|
+
!(width > 0) ||
|
|
168
|
+
!(height > 0)
|
|
169
|
+
) {
|
|
170
|
+
return []
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const offsetX = Number(pad?.offsetTopX ?? pad?.offsetBottomX ?? 0) || 0
|
|
174
|
+
const offsetY = Number(pad?.offsetTopY ?? pad?.offsetBottomY ?? 0) || 0
|
|
175
|
+
const angle = (Number(pad?.rotation || 0) * Math.PI) / 180
|
|
176
|
+
const cos = Math.cos(angle)
|
|
177
|
+
const sin = Math.sin(angle)
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
{ x: -width / 2 + offsetX, y: -height / 2 + offsetY },
|
|
181
|
+
{ x: width / 2 + offsetX, y: -height / 2 + offsetY },
|
|
182
|
+
{ x: width / 2 + offsetX, y: height / 2 + offsetY },
|
|
183
|
+
{ x: -width / 2 + offsetX, y: height / 2 + offsetY }
|
|
184
|
+
].map((corner) => ({
|
|
185
|
+
x: x + corner.x * cos - corner.y * sin,
|
|
186
|
+
y: y + corner.x * sin + corner.y * cos
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves an axis-aligned board-space bounds box from component body data.
|
|
192
|
+
* @param {object} component Scene component.
|
|
193
|
+
* @param {object | undefined} board Board metadata.
|
|
194
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
195
|
+
*/
|
|
196
|
+
static #boundsFromComponentBody(component, board) {
|
|
197
|
+
const centerX =
|
|
198
|
+
Number(component?.boardPositionMil?.x) ||
|
|
199
|
+
Number(component?.positionMil?.x || 0) + Number(board?.centerX || 0)
|
|
200
|
+
const centerY =
|
|
201
|
+
Number(component?.boardPositionMil?.y) ||
|
|
202
|
+
Number(component?.positionMil?.y || 0) + Number(board?.centerY || 0)
|
|
203
|
+
const width = Number(component?.body?.sizeMil?.width || 0)
|
|
204
|
+
const depth = Number(component?.body?.sizeMil?.depth || 0)
|
|
205
|
+
if (
|
|
206
|
+
!Number.isFinite(centerX) ||
|
|
207
|
+
!Number.isFinite(centerY) ||
|
|
208
|
+
!(width > 0) ||
|
|
209
|
+
!(depth > 0)
|
|
210
|
+
) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const angle = (Number(component?.rotationDeg || 0) * Math.PI) / 180
|
|
215
|
+
const cos = Math.cos(angle)
|
|
216
|
+
const sin = Math.sin(angle)
|
|
217
|
+
return PcbScene3dSelectionMarkerFactory.#boundsFromPoints(
|
|
218
|
+
[
|
|
219
|
+
{ x: -width / 2, y: -depth / 2 },
|
|
220
|
+
{ x: width / 2, y: -depth / 2 },
|
|
221
|
+
{ x: width / 2, y: depth / 2 },
|
|
222
|
+
{ x: -width / 2, y: depth / 2 }
|
|
223
|
+
].map((corner) => ({
|
|
224
|
+
x: centerX + corner.x * cos - corner.y * sin,
|
|
225
|
+
y: centerY + corner.x * sin + corner.y * cos
|
|
226
|
+
}))
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolves a bounds box from finite points.
|
|
232
|
+
* @param {{ x: number, y: number }[]} points Candidate points.
|
|
233
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
234
|
+
*/
|
|
235
|
+
static #boundsFromPoints(points) {
|
|
236
|
+
const finitePoints = (points || []).filter(
|
|
237
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
238
|
+
)
|
|
239
|
+
if (!finitePoints.length) {
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const xs = finitePoints.map((point) => point.x)
|
|
244
|
+
const ys = finitePoints.map((point) => point.y)
|
|
245
|
+
return {
|
|
246
|
+
minX: Math.min(...xs),
|
|
247
|
+
minY: Math.min(...ys),
|
|
248
|
+
maxX: Math.max(...xs),
|
|
249
|
+
maxY: Math.max(...ys)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Adds marker padding and a minimum clickable/visible span.
|
|
255
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} bounds Bounds.
|
|
256
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
257
|
+
*/
|
|
258
|
+
static #expandBounds(bounds) {
|
|
259
|
+
if (!bounds) {
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
264
|
+
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
265
|
+
const width = Math.max(
|
|
266
|
+
bounds.maxX -
|
|
267
|
+
bounds.minX +
|
|
268
|
+
PcbScene3dSelectionMarkerFactory.#MARKER_PADDING_MIL * 2,
|
|
269
|
+
PcbScene3dSelectionMarkerFactory.#MIN_MARKER_SPAN_MIL
|
|
270
|
+
)
|
|
271
|
+
const height = Math.max(
|
|
272
|
+
bounds.maxY -
|
|
273
|
+
bounds.minY +
|
|
274
|
+
PcbScene3dSelectionMarkerFactory.#MARKER_PADDING_MIL * 2,
|
|
275
|
+
PcbScene3dSelectionMarkerFactory.#MIN_MARKER_SPAN_MIL
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
minX: centerX - width / 2,
|
|
280
|
+
minY: centerY - height / 2,
|
|
281
|
+
maxX: centerX + width / 2,
|
|
282
|
+
maxY: centerY + height / 2
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Builds normalized marker line points.
|
|
288
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Board-space bounds.
|
|
289
|
+
* @param {number} z Marker z position.
|
|
290
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint Coordinate normalizer.
|
|
291
|
+
* @returns {number[][]}
|
|
292
|
+
*/
|
|
293
|
+
static #markerPoints(bounds, z, normalizePoint) {
|
|
294
|
+
return [
|
|
295
|
+
[bounds.minX, bounds.minY],
|
|
296
|
+
[bounds.maxX, bounds.minY],
|
|
297
|
+
[bounds.maxX, bounds.maxY],
|
|
298
|
+
[bounds.minX, bounds.maxY]
|
|
299
|
+
].map(([x, y]) => {
|
|
300
|
+
const point = normalizePoint(x, y)
|
|
301
|
+
return [point.x, point.y, z]
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Resolves marker z above the mounted board face.
|
|
307
|
+
* @param {{ thicknessMil?: number } | undefined} board Board metadata.
|
|
308
|
+
* @param {{ mountSide?: string }} component Scene component.
|
|
309
|
+
* @returns {number}
|
|
310
|
+
*/
|
|
311
|
+
static #markerZ(board, component) {
|
|
312
|
+
const topZ =
|
|
313
|
+
Number(board?.thicknessMil || 0) / 2 +
|
|
314
|
+
PcbScene3dSelectionMarkerFactory.#MARKER_Z_OFFSET_MIL
|
|
315
|
+
return String(component?.mountSide || 'top').toLowerCase() === 'bottom'
|
|
316
|
+
? -topZ
|
|
317
|
+
: topZ
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Resolves the maximum positive finite value from a candidate list.
|
|
322
|
+
* @param {unknown[]} values Candidate values.
|
|
323
|
+
* @returns {number}
|
|
324
|
+
*/
|
|
325
|
+
static #maxPositive(values) {
|
|
326
|
+
return Math.max(
|
|
327
|
+
0,
|
|
328
|
+
...(values || [])
|
|
329
|
+
.map((value) => Number(value))
|
|
330
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PcbScene3dSelectionMarkerFactory } from './PcbScene3dSelectionMarkerFactory.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Owns the render group used for the current selected-component marker.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dSelectionMarkerOverlay {
|
|
7
|
+
#markerGroup
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {any} markerGroup Three group that hosts selection marker meshes.
|
|
11
|
+
*/
|
|
12
|
+
constructor(markerGroup) {
|
|
13
|
+
this.#markerGroup = markerGroup
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clears and rebuilds the selected-component marker.
|
|
18
|
+
* @param {any} THREE Three.js namespace.
|
|
19
|
+
* @param {object | null} sceneDescription Normalized scene description.
|
|
20
|
+
* @param {string} selectedDesignator Selected component designator.
|
|
21
|
+
* @param {boolean} selectedComponentHidden Whether the component is hidden.
|
|
22
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizePoint Board/detail coordinate normalizer.
|
|
23
|
+
* @param {{ color?: number }} [options] Marker options.
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
update(
|
|
27
|
+
THREE,
|
|
28
|
+
sceneDescription,
|
|
29
|
+
selectedDesignator,
|
|
30
|
+
selectedComponentHidden,
|
|
31
|
+
normalizePoint,
|
|
32
|
+
options = {}
|
|
33
|
+
) {
|
|
34
|
+
if (!this.#markerGroup) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
PcbScene3dSelectionMarkerOverlay.#clearGroup(this.#markerGroup)
|
|
39
|
+
if (!THREE || !selectedDesignator || selectedComponentHidden) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const marker = PcbScene3dSelectionMarkerFactory.build(
|
|
44
|
+
THREE,
|
|
45
|
+
sceneDescription,
|
|
46
|
+
selectedDesignator,
|
|
47
|
+
normalizePoint,
|
|
48
|
+
options
|
|
49
|
+
)
|
|
50
|
+
if (marker) {
|
|
51
|
+
this.#markerGroup.add(marker)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Clears a Three group while keeping tests compatible with tiny fakes.
|
|
57
|
+
* @param {any} group Render group.
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
static #clearGroup(group) {
|
|
61
|
+
if (typeof group?.clear === 'function') {
|
|
62
|
+
group.clear()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(group?.children)) {
|
|
67
|
+
group.children.length = 0
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -138,7 +138,12 @@ export class PcbScene3dSelectionStyler {
|
|
|
138
138
|
if (PcbScene3dSelectionStyler.#canWriteColor(material.color)) {
|
|
139
139
|
PcbScene3dSelectionStyler.#writeColor(
|
|
140
140
|
material.color,
|
|
141
|
-
highlighted
|
|
141
|
+
highlighted
|
|
142
|
+
? PcbScene3dSelectionStyler.#highlightedColorHex(
|
|
143
|
+
material,
|
|
144
|
+
baseState
|
|
145
|
+
)
|
|
146
|
+
: baseState.colorHex
|
|
142
147
|
)
|
|
143
148
|
}
|
|
144
149
|
PcbScene3dSelectionStyler.#writeColor(
|
|
@@ -146,7 +151,9 @@ export class PcbScene3dSelectionStyler {
|
|
|
146
151
|
highlighted ? highlightColor : baseState.emissiveHex
|
|
147
152
|
)
|
|
148
153
|
material.emissiveIntensity = highlighted
|
|
149
|
-
?
|
|
154
|
+
? PcbScene3dSelectionStyler.#highlightedEmissiveIntensity(
|
|
155
|
+
material
|
|
156
|
+
)
|
|
150
157
|
: baseState.emissiveIntensity
|
|
151
158
|
material.needsUpdate = true
|
|
152
159
|
return
|
|
@@ -184,6 +191,49 @@ export class PcbScene3dSelectionStyler {
|
|
|
184
191
|
return material.userData.scene3dHighlightBase
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Resolves the selected-state base color for one material.
|
|
196
|
+
* @param {any} material Material to highlight.
|
|
197
|
+
* @param {{ colorHex: number | null }} baseState Stored base state.
|
|
198
|
+
* @returns {number | null}
|
|
199
|
+
*/
|
|
200
|
+
static #highlightedColorHex(material, baseState) {
|
|
201
|
+
if (PcbScene3dSelectionStyler.#isTransparentMaterial(material)) {
|
|
202
|
+
return baseState.colorHex
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return 0x000000
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Returns true when selection should preserve the source color for stacking.
|
|
210
|
+
* @param {any} material Material to inspect.
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
static #isTransparentMaterial(material) {
|
|
214
|
+
return (
|
|
215
|
+
material?.transparent === true && Number(material?.opacity ?? 1) < 1
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolves the selected-state emissive strength for one material.
|
|
221
|
+
* @param {any} material Material to highlight.
|
|
222
|
+
* @returns {number}
|
|
223
|
+
*/
|
|
224
|
+
static #highlightedEmissiveIntensity(material) {
|
|
225
|
+
if (!PcbScene3dSelectionStyler.#isTransparentMaterial(material)) {
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const opacity = Number(material?.opacity)
|
|
230
|
+
if (!Number.isFinite(opacity)) {
|
|
231
|
+
return 1
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Math.max(0, Math.min(1, opacity))
|
|
235
|
+
}
|
|
236
|
+
|
|
187
237
|
/**
|
|
188
238
|
* Normalizes a Three material field into an array.
|
|
189
239
|
* @param {any} materials
|