pcb-scene3d-viewer 1.3.3 → 1.3.4
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/docs/api.md +15 -0
- package/docs/model-format.md +5 -0
- package/package.json +2 -2
- package/src/PcbAssemblyFillGeometryResolver.mjs +16 -0
- package/src/PcbScene3dCopperOcclusionClipper.mjs +4 -5
- package/src/PcbScene3dCopperOcclusionGeometry.mjs +182 -0
- package/src/PcbScene3dCopperOcclusionPlanes.mjs +145 -0
- package/src/PcbScene3dCopperTextFactory.mjs +8 -10
- package/src/PcbScene3dCutoutGeometryFilter.mjs +40 -8
- package/src/PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.mjs +78 -32
- package/src/PcbScene3dGeneratedGeometryBuilder.mjs +87 -0
- package/src/PcbScene3dGeometryTransfer.mjs +398 -0
- package/src/PcbScene3dGeometryWorker.mjs +45 -0
- package/src/PcbScene3dGeometryWorkerClient.mjs +182 -0
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +17 -27
- package/src/PcbScene3dRuntime.mjs +33 -54
- package/src/PcbScene3dSilkscreenChunkedFactory.mjs +15 -3
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +35 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +58 -22
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +4 -3
- package/src/PcbScene3dWorkerClient.mjs +12 -6
|
@@ -41,6 +41,10 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
41
41
|
sceneDescription,
|
|
42
42
|
placement
|
|
43
43
|
)
|
|
44
|
+
if (siblingPlacements.length <= 1) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
const packageRecords =
|
|
45
49
|
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#packageRecords(
|
|
46
50
|
sceneDescription,
|
|
@@ -117,11 +121,7 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
117
121
|
String(placement?.projection?.source || '').toLowerCase() ===
|
|
118
122
|
'model-bounds' &&
|
|
119
123
|
String(placement?.externalModel?.origin || '').toLowerCase() ===
|
|
120
|
-
'embedded'
|
|
121
|
-
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#siblingPlacements(
|
|
122
|
-
sceneDescription,
|
|
123
|
-
placement
|
|
124
|
-
).length > 1
|
|
124
|
+
'embedded'
|
|
125
125
|
)
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -191,18 +191,29 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
191
191
|
* @returns {{ placement: object, padCenter: { x: number, y: number }, anchorOffset: { x: number, y: number } }[]}
|
|
192
192
|
*/
|
|
193
193
|
static #packageRecords(sceneDescription, placements) {
|
|
194
|
+
// Reuse ownership indexes across the sibling group, while rebuilding
|
|
195
|
+
// them per application so edits to scene rows remain observable.
|
|
196
|
+
const componentsByDesignator =
|
|
197
|
+
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#componentsByDesignator(
|
|
198
|
+
sceneDescription
|
|
199
|
+
)
|
|
200
|
+
const padsByComponent =
|
|
201
|
+
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#padsByComponent(
|
|
202
|
+
sceneDescription
|
|
203
|
+
)
|
|
194
204
|
return (Array.isArray(placements) ? placements : [])
|
|
195
205
|
.map((placement) => {
|
|
196
206
|
const component =
|
|
197
207
|
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#resolveComponent(
|
|
198
|
-
|
|
208
|
+
componentsByDesignator,
|
|
199
209
|
placement
|
|
200
210
|
)
|
|
201
211
|
const padCenter =
|
|
202
212
|
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#ownedPackagePadCenter(
|
|
203
213
|
sceneDescription,
|
|
204
214
|
component,
|
|
205
|
-
placement
|
|
215
|
+
placement,
|
|
216
|
+
padsByComponent
|
|
206
217
|
)
|
|
207
218
|
if (!padCenter) {
|
|
208
219
|
return null
|
|
@@ -225,9 +236,15 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
225
236
|
* @param {object | null | undefined} sceneDescription Scene description.
|
|
226
237
|
* @param {object | null} component Scene component.
|
|
227
238
|
* @param {object | null | undefined} placement External placement.
|
|
239
|
+
* @param {Map<number, object[]>} padsByComponent Pads indexed by owner.
|
|
228
240
|
* @returns {{ x: number, y: number } | null}
|
|
229
241
|
*/
|
|
230
|
-
static #ownedPackagePadCenter(
|
|
242
|
+
static #ownedPackagePadCenter(
|
|
243
|
+
sceneDescription,
|
|
244
|
+
component,
|
|
245
|
+
placement,
|
|
246
|
+
padsByComponent
|
|
247
|
+
) {
|
|
231
248
|
if (
|
|
232
249
|
!PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isPackageComponent(
|
|
233
250
|
component
|
|
@@ -247,18 +264,12 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
247
264
|
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isBottomPlacement(
|
|
248
265
|
placement
|
|
249
266
|
)
|
|
250
|
-
const points = (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
(pad) =>
|
|
257
|
-
Number(pad?.componentIndex) === componentIndex &&
|
|
258
|
-
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isSurfacePad(
|
|
259
|
-
pad,
|
|
260
|
-
isBottom
|
|
261
|
-
)
|
|
267
|
+
const points = (padsByComponent.get(componentIndex) || [])
|
|
268
|
+
.filter((pad) =>
|
|
269
|
+
PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.#isSurfacePad(
|
|
270
|
+
pad,
|
|
271
|
+
isBottom
|
|
272
|
+
)
|
|
262
273
|
)
|
|
263
274
|
.map((pad) => ({
|
|
264
275
|
x: Number(pad?.x || 0) - centerX,
|
|
@@ -482,23 +493,58 @@ export class PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair {
|
|
|
482
493
|
}
|
|
483
494
|
|
|
484
495
|
/**
|
|
485
|
-
*
|
|
496
|
+
* Indexes scene pads once for all owners in a repeated package group.
|
|
497
|
+
* @param {object | null | undefined} sceneDescription Scene description.
|
|
498
|
+
* @returns {Map<number, object[]>}
|
|
499
|
+
*/
|
|
500
|
+
static #padsByComponent(sceneDescription) {
|
|
501
|
+
const index = new Map()
|
|
502
|
+
const pads = Array.isArray(sceneDescription?.detail?.pads)
|
|
503
|
+
? sceneDescription.detail.pads
|
|
504
|
+
: []
|
|
505
|
+
for (const pad of pads) {
|
|
506
|
+
const componentIndex = Number(pad?.componentIndex)
|
|
507
|
+
if (!Number.isFinite(componentIndex)) {
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
let ownedPads = index.get(componentIndex)
|
|
511
|
+
if (!ownedPads) {
|
|
512
|
+
ownedPads = []
|
|
513
|
+
index.set(componentIndex, ownedPads)
|
|
514
|
+
}
|
|
515
|
+
ownedPads.push(pad)
|
|
516
|
+
}
|
|
517
|
+
return index
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Indexes the first component for each normalized owner designator.
|
|
486
522
|
* @param {object | null | undefined} sceneDescription Scene description.
|
|
523
|
+
* @returns {Map<string, object>}
|
|
524
|
+
*/
|
|
525
|
+
static #componentsByDesignator(sceneDescription) {
|
|
526
|
+
const index = new Map()
|
|
527
|
+
const components = Array.isArray(sceneDescription?.components)
|
|
528
|
+
? sceneDescription.components
|
|
529
|
+
: []
|
|
530
|
+
for (const component of components) {
|
|
531
|
+
const designator = String(component?.designator || '').trim()
|
|
532
|
+
if (designator && !index.has(designator)) {
|
|
533
|
+
index.set(designator, component)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return index
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Resolves the scene component for one placement.
|
|
541
|
+
* @param {Map<string, object>} componentsByDesignator Components indexed by owner.
|
|
487
542
|
* @param {object | null | undefined} placement External placement.
|
|
488
543
|
* @returns {object | null}
|
|
489
544
|
*/
|
|
490
|
-
static #resolveComponent(
|
|
545
|
+
static #resolveComponent(componentsByDesignator, placement) {
|
|
491
546
|
const designator = String(placement?.designator || '').trim()
|
|
492
|
-
|
|
493
|
-
return null
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return (
|
|
497
|
-
sceneDescription.components.find(
|
|
498
|
-
(component) =>
|
|
499
|
-
String(component?.designator || '').trim() === designator
|
|
500
|
-
) || null
|
|
501
|
-
)
|
|
547
|
+
return componentsByDesignator.get(designator) || null
|
|
502
548
|
}
|
|
503
549
|
|
|
504
550
|
/**
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFactory.mjs'
|
|
2
|
+
import { PcbScene3dCopperDetailGroupBuilder } from './PcbScene3dCopperDetailGroupBuilder.mjs'
|
|
3
|
+
import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
|
|
4
|
+
import { PcbScene3dDrillVoidFactory } from './PcbScene3dDrillVoidFactory.mjs'
|
|
5
|
+
import { PcbScene3dRuntimeBoardMeshes } from './PcbScene3dRuntimeBoardMeshes.mjs'
|
|
6
|
+
|
|
7
|
+
/** Runs the same exact generated-geometry factories on workers or the caller. */
|
|
8
|
+
export class PcbScene3dGeneratedGeometryBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Builds one independent generated runtime stage.
|
|
11
|
+
* @param {any} THREE Three.js namespace.
|
|
12
|
+
* @param {'board' | 'copper'} kind Geometry stage.
|
|
13
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
14
|
+
* @returns {any}
|
|
15
|
+
*/
|
|
16
|
+
static build(THREE, kind, sceneDescription) {
|
|
17
|
+
const normalizePoint =
|
|
18
|
+
PcbScene3dDetailCoordinateNormalizer.create(sceneDescription)
|
|
19
|
+
const board = sceneDescription.board
|
|
20
|
+
if (kind === 'copper') {
|
|
21
|
+
return PcbScene3dCopperDetailGroupBuilder.build(
|
|
22
|
+
THREE,
|
|
23
|
+
sceneDescription,
|
|
24
|
+
board.thicknessMil / 2 + 0.05,
|
|
25
|
+
normalizePoint
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
if (kind !== 'board')
|
|
29
|
+
throw new Error('Unknown generated geometry stage: ' + kind)
|
|
30
|
+
const group = new THREE.Group()
|
|
31
|
+
group.add(
|
|
32
|
+
PcbScene3dRuntimeBoardMeshes.buildBoardMesh(
|
|
33
|
+
THREE,
|
|
34
|
+
sceneDescription,
|
|
35
|
+
normalizePoint
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
group.add(
|
|
39
|
+
PcbScene3dBoardSolderMaskFactory.buildGroup(
|
|
40
|
+
THREE,
|
|
41
|
+
sceneDescription,
|
|
42
|
+
normalizePoint
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
group.add(
|
|
46
|
+
PcbScene3dDrillVoidFactory.buildGroup(
|
|
47
|
+
THREE,
|
|
48
|
+
sceneDescription.detail,
|
|
49
|
+
board.thicknessMil / 2,
|
|
50
|
+
-board.thicknessMil / 2,
|
|
51
|
+
normalizePoint,
|
|
52
|
+
{
|
|
53
|
+
enabled: true,
|
|
54
|
+
board,
|
|
55
|
+
hasBoardAssemblyModel: Boolean(
|
|
56
|
+
sceneDescription.boardAssemblyModel
|
|
57
|
+
),
|
|
58
|
+
sourceFormat: sceneDescription.sourceFormat
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
group.add(
|
|
63
|
+
PcbScene3dRuntimeBoardMeshes.buildBoardOutline(
|
|
64
|
+
THREE,
|
|
65
|
+
sceneDescription,
|
|
66
|
+
normalizePoint
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return group
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keeps component model payloads outside geometry-worker messages.
|
|
74
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
75
|
+
* @returns {object}
|
|
76
|
+
*/
|
|
77
|
+
static workerInput(sceneDescription) {
|
|
78
|
+
return {
|
|
79
|
+
board: sceneDescription.board,
|
|
80
|
+
detail: sceneDescription.detail,
|
|
81
|
+
texts: sceneDescription.texts,
|
|
82
|
+
sourceFormat: sceneDescription.sourceFormat,
|
|
83
|
+
coordinateSystem: sceneDescription.coordinateSystem,
|
|
84
|
+
boardAssemblyModel: Boolean(sceneDescription.boardAssemblyModel)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/** Transfers generated scene geometry without JSON expansion or retriangulation. */
|
|
2
|
+
export class PcbScene3dGeometryTransfer {
|
|
3
|
+
/**
|
|
4
|
+
* Extracts an owned object tree and its transferable vertex buffers.
|
|
5
|
+
* @param {any} root Generated Three.js object.
|
|
6
|
+
* @returns {{ payload: object, transferables: ArrayBuffer[] }}
|
|
7
|
+
*/
|
|
8
|
+
static serialize(root) {
|
|
9
|
+
const context = {
|
|
10
|
+
geometries: [],
|
|
11
|
+
materials: [],
|
|
12
|
+
interleaved: [],
|
|
13
|
+
geometryIds: new Map(),
|
|
14
|
+
materialIds: new Map(),
|
|
15
|
+
interleavedIds: new Map(),
|
|
16
|
+
buffers: new Set()
|
|
17
|
+
}
|
|
18
|
+
const object = this.#serializeObject(root, context)
|
|
19
|
+
return {
|
|
20
|
+
payload: {
|
|
21
|
+
object,
|
|
22
|
+
geometries: context.geometries,
|
|
23
|
+
materials: context.materials,
|
|
24
|
+
interleaved: context.interleaved
|
|
25
|
+
},
|
|
26
|
+
transferables: [...context.buffers]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Restores lightweight Three.js wrappers around received buffers.
|
|
32
|
+
* @param {any} THREE Three.js namespace.
|
|
33
|
+
* @param {object} payload Serialized generated tree.
|
|
34
|
+
* @returns {any}
|
|
35
|
+
*/
|
|
36
|
+
static deserialize(THREE, payload) {
|
|
37
|
+
const geometries = []
|
|
38
|
+
const materials = []
|
|
39
|
+
try {
|
|
40
|
+
const interleaved = payload.interleaved.map((entry) => {
|
|
41
|
+
const buffer = new THREE.InterleavedBuffer(
|
|
42
|
+
entry.array,
|
|
43
|
+
entry.stride
|
|
44
|
+
)
|
|
45
|
+
buffer.setUsage(entry.usage)
|
|
46
|
+
return buffer
|
|
47
|
+
})
|
|
48
|
+
for (const entry of payload.geometries) {
|
|
49
|
+
const geometry = new THREE.BufferGeometry()
|
|
50
|
+
geometries.push(geometry)
|
|
51
|
+
geometry.name = entry.name
|
|
52
|
+
geometry.userData = entry.userData
|
|
53
|
+
for (const [name, attribute] of Object.entries(
|
|
54
|
+
entry.attributes
|
|
55
|
+
)) {
|
|
56
|
+
geometry.setAttribute(
|
|
57
|
+
name,
|
|
58
|
+
this.#deserializeAttribute(
|
|
59
|
+
THREE,
|
|
60
|
+
attribute,
|
|
61
|
+
interleaved
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
if (entry.index)
|
|
66
|
+
geometry.setIndex(
|
|
67
|
+
this.#deserializeAttribute(
|
|
68
|
+
THREE,
|
|
69
|
+
entry.index,
|
|
70
|
+
interleaved
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
for (const [name, attributes] of Object.entries(
|
|
74
|
+
entry.morphAttributes
|
|
75
|
+
)) {
|
|
76
|
+
geometry.morphAttributes[name] = attributes.map(
|
|
77
|
+
(attribute) =>
|
|
78
|
+
this.#deserializeAttribute(
|
|
79
|
+
THREE,
|
|
80
|
+
attribute,
|
|
81
|
+
interleaved
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
geometry.morphTargetsRelative = entry.morphTargetsRelative
|
|
86
|
+
geometry.groups = entry.groups
|
|
87
|
+
geometry.setDrawRange(
|
|
88
|
+
entry.drawRange.start,
|
|
89
|
+
entry.drawRange.count
|
|
90
|
+
)
|
|
91
|
+
if (entry.boundingBox)
|
|
92
|
+
geometry.boundingBox = new THREE.Box3(
|
|
93
|
+
new THREE.Vector3().fromArray(entry.boundingBox.min),
|
|
94
|
+
new THREE.Vector3().fromArray(entry.boundingBox.max)
|
|
95
|
+
)
|
|
96
|
+
if (entry.boundingSphere)
|
|
97
|
+
geometry.boundingSphere = new THREE.Sphere(
|
|
98
|
+
new THREE.Vector3().fromArray(
|
|
99
|
+
entry.boundingSphere.center
|
|
100
|
+
),
|
|
101
|
+
entry.boundingSphere.radius
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
const loader = new THREE.MaterialLoader()
|
|
105
|
+
for (const entry of payload.materials) {
|
|
106
|
+
const material = loader.parse(entry.json)
|
|
107
|
+
materials.push(material)
|
|
108
|
+
for (const [name, color] of Object.entries(entry.colors)) {
|
|
109
|
+
material[name].fromArray(color)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return this.#deserializeObject(
|
|
113
|
+
THREE,
|
|
114
|
+
payload.object,
|
|
115
|
+
geometries,
|
|
116
|
+
materials
|
|
117
|
+
)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
geometries.forEach((geometry) => geometry.dispose())
|
|
120
|
+
materials.forEach((material) => material.dispose())
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Releases shared generated geometry/material resources once each.
|
|
127
|
+
* @param {any} root Generated object tree, including a detached tree.
|
|
128
|
+
* @returns {void}
|
|
129
|
+
*/
|
|
130
|
+
static dispose(root) {
|
|
131
|
+
const resources = new Set()
|
|
132
|
+
const pending = root ? [root] : []
|
|
133
|
+
while (pending.length) {
|
|
134
|
+
const object = pending.pop()
|
|
135
|
+
if (object.geometry) resources.add(object.geometry)
|
|
136
|
+
for (const material of Array.isArray(object.material)
|
|
137
|
+
? object.material
|
|
138
|
+
: [object.material]) {
|
|
139
|
+
if (material) resources.add(material)
|
|
140
|
+
}
|
|
141
|
+
pending.push(...(object.children || []))
|
|
142
|
+
}
|
|
143
|
+
resources.forEach((resource) => resource.dispose?.())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Serializes scene hierarchy while deduplicating GPU resources.
|
|
148
|
+
* @param {any} object Three.js object.
|
|
149
|
+
* @param {object} context Resource tables.
|
|
150
|
+
* @returns {object}
|
|
151
|
+
*/
|
|
152
|
+
static #serializeObject(object, context) {
|
|
153
|
+
const types = [
|
|
154
|
+
'Group',
|
|
155
|
+
'Mesh',
|
|
156
|
+
'Line',
|
|
157
|
+
'LineLoop',
|
|
158
|
+
'LineSegments',
|
|
159
|
+
'Object3D'
|
|
160
|
+
]
|
|
161
|
+
if (!types.includes(object.type))
|
|
162
|
+
throw new Error('Unsupported generated object: ' + object.type)
|
|
163
|
+
if (object.matrixAutoUpdate) object.updateMatrix()
|
|
164
|
+
return {
|
|
165
|
+
type: object.type,
|
|
166
|
+
name: object.name,
|
|
167
|
+
userData: object.userData,
|
|
168
|
+
position: object.position.toArray(),
|
|
169
|
+
quaternion: object.quaternion.toArray(),
|
|
170
|
+
rotationOrder: object.rotation.order,
|
|
171
|
+
scale: object.scale.toArray(),
|
|
172
|
+
matrix: object.matrix.toArray(),
|
|
173
|
+
matrixAutoUpdate: object.matrixAutoUpdate,
|
|
174
|
+
matrixWorldAutoUpdate: object.matrixWorldAutoUpdate,
|
|
175
|
+
visible: object.visible,
|
|
176
|
+
castShadow: object.castShadow,
|
|
177
|
+
receiveShadow: object.receiveShadow,
|
|
178
|
+
frustumCulled: object.frustumCulled,
|
|
179
|
+
renderOrder: object.renderOrder,
|
|
180
|
+
layers: object.layers.mask,
|
|
181
|
+
geometry: object.geometry
|
|
182
|
+
? this.#resourceId(
|
|
183
|
+
object.geometry,
|
|
184
|
+
context.geometryIds,
|
|
185
|
+
context.geometries,
|
|
186
|
+
() => this.#serializeGeometry(object.geometry, context)
|
|
187
|
+
)
|
|
188
|
+
: null,
|
|
189
|
+
material: Array.isArray(object.material)
|
|
190
|
+
? object.material.map((material) =>
|
|
191
|
+
this.#materialId(material, context)
|
|
192
|
+
)
|
|
193
|
+
: object.material
|
|
194
|
+
? this.#materialId(object.material, context)
|
|
195
|
+
: null,
|
|
196
|
+
children: object.children.map((child) =>
|
|
197
|
+
this.#serializeObject(child, context)
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recreates render objects without invoking any geometry factory.
|
|
204
|
+
* @param {any} THREE Three.js namespace.
|
|
205
|
+
* @param {object} entry Object descriptor.
|
|
206
|
+
* @param {any[]} geometries Geometry table.
|
|
207
|
+
* @param {any[]} materials Material table.
|
|
208
|
+
* @returns {any}
|
|
209
|
+
*/
|
|
210
|
+
static #deserializeObject(THREE, entry, geometries, materials) {
|
|
211
|
+
const material = Array.isArray(entry.material)
|
|
212
|
+
? entry.material.map((id) => materials[id])
|
|
213
|
+
: materials[entry.material]
|
|
214
|
+
const object =
|
|
215
|
+
entry.geometry == null
|
|
216
|
+
? new THREE[entry.type]()
|
|
217
|
+
: new THREE[entry.type](geometries[entry.geometry], material)
|
|
218
|
+
for (const field of [
|
|
219
|
+
'name',
|
|
220
|
+
'userData',
|
|
221
|
+
'visible',
|
|
222
|
+
'castShadow',
|
|
223
|
+
'receiveShadow',
|
|
224
|
+
'frustumCulled',
|
|
225
|
+
'renderOrder',
|
|
226
|
+
'matrixAutoUpdate',
|
|
227
|
+
'matrixWorldAutoUpdate'
|
|
228
|
+
]) {
|
|
229
|
+
object[field] = entry[field]
|
|
230
|
+
}
|
|
231
|
+
object.rotation.order = entry.rotationOrder
|
|
232
|
+
object.position.fromArray(entry.position)
|
|
233
|
+
object.quaternion.fromArray(entry.quaternion)
|
|
234
|
+
object.scale.fromArray(entry.scale)
|
|
235
|
+
object.matrix.fromArray(entry.matrix)
|
|
236
|
+
object.matrixWorldNeedsUpdate = true
|
|
237
|
+
object.layers.mask = entry.layers
|
|
238
|
+
entry.children.forEach((child) =>
|
|
239
|
+
object.add(
|
|
240
|
+
this.#deserializeObject(THREE, child, geometries, materials)
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return object
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Stores one shared resource and returns its stable local index.
|
|
248
|
+
* @param {any} resource Source resource.
|
|
249
|
+
* @param {Map} ids Identity map.
|
|
250
|
+
* @param {any[]} entries Serialized resources.
|
|
251
|
+
* @param {() => object} serialize Serializer.
|
|
252
|
+
* @returns {number}
|
|
253
|
+
*/
|
|
254
|
+
static #resourceId(resource, ids, entries, serialize) {
|
|
255
|
+
if (!ids.has(resource)) {
|
|
256
|
+
ids.set(resource, entries.length)
|
|
257
|
+
entries.push(serialize())
|
|
258
|
+
}
|
|
259
|
+
return ids.get(resource)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Stores built-in material properties with unquantized linear colors.
|
|
264
|
+
* @param {any} material Three.js material.
|
|
265
|
+
* @param {object} context Resource tables.
|
|
266
|
+
* @returns {number}
|
|
267
|
+
*/
|
|
268
|
+
static #materialId(material, context) {
|
|
269
|
+
return this.#resourceId(
|
|
270
|
+
material,
|
|
271
|
+
context.materialIds,
|
|
272
|
+
context.materials,
|
|
273
|
+
() => {
|
|
274
|
+
const colors = {}
|
|
275
|
+
for (const [name, value] of Object.entries(material)) {
|
|
276
|
+
if (value?.isTexture)
|
|
277
|
+
throw new Error(
|
|
278
|
+
'Generated geometry transfer does not support textures.'
|
|
279
|
+
)
|
|
280
|
+
if (value?.isColor) colors[name] = value.toArray()
|
|
281
|
+
}
|
|
282
|
+
return { json: material.toJSON(), colors }
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Stores geometry data directly as typed arrays, retaining draw groups.
|
|
289
|
+
* @param {any} geometry Generated geometry.
|
|
290
|
+
* @param {object} context Resource tables.
|
|
291
|
+
* @returns {object}
|
|
292
|
+
*/
|
|
293
|
+
static #serializeGeometry(geometry, context) {
|
|
294
|
+
if (!geometry.boundingBox) geometry.computeBoundingBox()
|
|
295
|
+
if (!geometry.boundingSphere) geometry.computeBoundingSphere()
|
|
296
|
+
return {
|
|
297
|
+
name: geometry.name,
|
|
298
|
+
userData: geometry.userData,
|
|
299
|
+
attributes: Object.fromEntries(
|
|
300
|
+
Object.entries(geometry.attributes).map(([name, attribute]) => [
|
|
301
|
+
name,
|
|
302
|
+
this.#serializeAttribute(attribute, context)
|
|
303
|
+
])
|
|
304
|
+
),
|
|
305
|
+
index: geometry.index
|
|
306
|
+
? this.#serializeAttribute(geometry.index, context)
|
|
307
|
+
: null,
|
|
308
|
+
morphAttributes: Object.fromEntries(
|
|
309
|
+
Object.entries(geometry.morphAttributes).map(
|
|
310
|
+
([name, attributes]) => [
|
|
311
|
+
name,
|
|
312
|
+
attributes.map((attribute) =>
|
|
313
|
+
this.#serializeAttribute(attribute, context)
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
)
|
|
317
|
+
),
|
|
318
|
+
morphTargetsRelative: geometry.morphTargetsRelative,
|
|
319
|
+
groups: geometry.groups,
|
|
320
|
+
drawRange: geometry.drawRange,
|
|
321
|
+
boundingBox: geometry.boundingBox
|
|
322
|
+
? {
|
|
323
|
+
min: geometry.boundingBox.min.toArray(),
|
|
324
|
+
max: geometry.boundingBox.max.toArray()
|
|
325
|
+
}
|
|
326
|
+
: null,
|
|
327
|
+
boundingSphere: geometry.boundingSphere
|
|
328
|
+
? {
|
|
329
|
+
center: geometry.boundingSphere.center.toArray(),
|
|
330
|
+
radius: geometry.boundingSphere.radius
|
|
331
|
+
}
|
|
332
|
+
: null
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Stores normal or interleaved attributes without duplicating buffers.
|
|
338
|
+
* @param {any} attribute Buffer attribute.
|
|
339
|
+
* @param {object} context Resource tables.
|
|
340
|
+
* @returns {object}
|
|
341
|
+
*/
|
|
342
|
+
static #serializeAttribute(attribute, context) {
|
|
343
|
+
const data = attribute.isInterleavedBufferAttribute
|
|
344
|
+
? attribute.data
|
|
345
|
+
: attribute
|
|
346
|
+
context.buffers.add(data.array.buffer)
|
|
347
|
+
return {
|
|
348
|
+
name: attribute.name,
|
|
349
|
+
itemSize: attribute.itemSize,
|
|
350
|
+
normalized: attribute.normalized,
|
|
351
|
+
usage: data.usage,
|
|
352
|
+
gpuType: attribute.gpuType,
|
|
353
|
+
array: attribute.isInterleavedBufferAttribute
|
|
354
|
+
? null
|
|
355
|
+
: attribute.array,
|
|
356
|
+
interleaved: attribute.isInterleavedBufferAttribute
|
|
357
|
+
? this.#resourceId(
|
|
358
|
+
data,
|
|
359
|
+
context.interleavedIds,
|
|
360
|
+
context.interleaved,
|
|
361
|
+
() => ({
|
|
362
|
+
array: data.array,
|
|
363
|
+
stride: data.stride,
|
|
364
|
+
usage: data.usage
|
|
365
|
+
})
|
|
366
|
+
)
|
|
367
|
+
: null,
|
|
368
|
+
offset: attribute.offset
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Wraps a received attribute buffer in the corresponding Three.js type.
|
|
374
|
+
* @param {any} THREE Three.js namespace.
|
|
375
|
+
* @param {object} entry Attribute descriptor.
|
|
376
|
+
* @param {any[]} interleaved Interleaved buffer table.
|
|
377
|
+
* @returns {any}
|
|
378
|
+
*/
|
|
379
|
+
static #deserializeAttribute(THREE, entry, interleaved) {
|
|
380
|
+
const attribute =
|
|
381
|
+
entry.interleaved == null
|
|
382
|
+
? new THREE.BufferAttribute(
|
|
383
|
+
entry.array,
|
|
384
|
+
entry.itemSize,
|
|
385
|
+
entry.normalized
|
|
386
|
+
)
|
|
387
|
+
: new THREE.InterleavedBufferAttribute(
|
|
388
|
+
interleaved[entry.interleaved],
|
|
389
|
+
entry.itemSize,
|
|
390
|
+
entry.offset,
|
|
391
|
+
entry.normalized
|
|
392
|
+
)
|
|
393
|
+
attribute.name = entry.name
|
|
394
|
+
if (entry.gpuType !== undefined) attribute.gpuType = entry.gpuType
|
|
395
|
+
attribute.setUsage?.(entry.usage)
|
|
396
|
+
return attribute
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PcbScene3dGeneratedGeometryBuilder } from './PcbScene3dGeneratedGeometryBuilder.mjs'
|
|
2
|
+
import { PcbScene3dGeometryTransfer } from './PcbScene3dGeometryTransfer.mjs'
|
|
3
|
+
|
|
4
|
+
/** Dedicated worker boundary for exact generated board and copper geometry. */
|
|
5
|
+
export class PcbScene3dGeometryWorker {
|
|
6
|
+
/**
|
|
7
|
+
* Builds and transfers one stage, returning serializable failures.
|
|
8
|
+
* @param {object} payload Worker request.
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
static async handle(payload) {
|
|
12
|
+
if (payload?.type !== 'scene3d:geometry-build') return
|
|
13
|
+
let root = null
|
|
14
|
+
try {
|
|
15
|
+
const THREE = await import(payload.threeModuleUrl)
|
|
16
|
+
root = PcbScene3dGeneratedGeometryBuilder.build(
|
|
17
|
+
THREE,
|
|
18
|
+
payload.kind,
|
|
19
|
+
payload.sceneDescription
|
|
20
|
+
)
|
|
21
|
+
const { payload: geometry, transferables } =
|
|
22
|
+
PcbScene3dGeometryTransfer.serialize(root)
|
|
23
|
+
globalThis.postMessage(
|
|
24
|
+
{
|
|
25
|
+
type: 'scene3d:geometry-success',
|
|
26
|
+
requestId: payload.requestId,
|
|
27
|
+
geometry
|
|
28
|
+
},
|
|
29
|
+
transferables
|
|
30
|
+
)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
globalThis.postMessage({
|
|
33
|
+
type: 'scene3d:geometry-error',
|
|
34
|
+
requestId: payload.requestId,
|
|
35
|
+
message: String(error?.message || error)
|
|
36
|
+
})
|
|
37
|
+
} finally {
|
|
38
|
+
PcbScene3dGeometryTransfer.dispose(root)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
globalThis.addEventListener('message', (event) => {
|
|
44
|
+
PcbScene3dGeometryWorker.handle(event.data)
|
|
45
|
+
})
|