pcb-scene3d-viewer 1.3.2 → 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/README.md +6 -0
- package/docs/api.md +15 -0
- package/docs/model-format.md +12 -0
- package/docs/release-notes-v1.3.3.md +28 -0
- package/package.json +3 -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/PcbScene3dExternalModelSourceOriginPolicy.mjs +3 -2
- package/src/PcbScene3dExternalModels.mjs +77 -1
- 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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { PcbScene3dGeneratedGeometryBuilder } from './PcbScene3dGeneratedGeometryBuilder.mjs'
|
|
2
|
+
import { PcbScene3dGeometryTransfer } from './PcbScene3dGeometryTransfer.mjs'
|
|
3
|
+
import { PcbScene3dRuntimeHelpers } from './PcbScene3dRuntimeHelpers.mjs'
|
|
4
|
+
|
|
5
|
+
/** Owns cancellable worker geometry stages with an exact compatibility fallback. */
|
|
6
|
+
export class PcbScene3dGeometryWorkerClient {
|
|
7
|
+
#worker = null
|
|
8
|
+
#options
|
|
9
|
+
#requests = new Map()
|
|
10
|
+
#sequence = 0
|
|
11
|
+
#unavailable = false
|
|
12
|
+
#disposed = false
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ workerFactory?: (() => Worker) | null, threeModuleUrl?: string, requestTimeoutMs?: number }} [options] Worker deployment options; a null factory disables workers.
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.#options = options
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Builds a stage away from the UI thread and restores received buffers.
|
|
23
|
+
* @param {any} THREE Runtime Three.js namespace.
|
|
24
|
+
* @param {'board' | 'copper'} kind Generated stage.
|
|
25
|
+
* @param {object} sceneDescription Normalized scene description.
|
|
26
|
+
* @returns {Promise<any>}
|
|
27
|
+
*/
|
|
28
|
+
async build(THREE, kind, sceneDescription) {
|
|
29
|
+
this.#assertActive()
|
|
30
|
+
const worker = this.#ensureWorker()
|
|
31
|
+
if (worker) {
|
|
32
|
+
try {
|
|
33
|
+
const data = await this.#request(worker, kind, sceneDescription)
|
|
34
|
+
this.#assertActive()
|
|
35
|
+
return PcbScene3dGeometryTransfer.deserialize(THREE, data)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.#assertActive()
|
|
38
|
+
this.#fail(error)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// A worker may be forbidden by the host CSP; retain exact geometry there.
|
|
42
|
+
await PcbScene3dRuntimeHelpers.yieldToNextFrame(globalThis)
|
|
43
|
+
this.#assertActive()
|
|
44
|
+
return PcbScene3dGeneratedGeometryBuilder.build(
|
|
45
|
+
THREE,
|
|
46
|
+
kind,
|
|
47
|
+
sceneDescription
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Terminates active work instead of leaving detached scene builds running. @returns {void} */
|
|
52
|
+
dispose() {
|
|
53
|
+
this.#disposed = true
|
|
54
|
+
this.#fail(this.#abortError())
|
|
55
|
+
this.#options = {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Creates one reusable worker, isolating startup failures. @returns {Worker | null} */
|
|
59
|
+
#ensureWorker() {
|
|
60
|
+
if (this.#unavailable || this.#worker) return this.#worker
|
|
61
|
+
try {
|
|
62
|
+
if (this.#options.workerFactory === null) {
|
|
63
|
+
this.#unavailable = true
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
if (this.#options.workerFactory)
|
|
67
|
+
this.#worker = this.#options.workerFactory()
|
|
68
|
+
else if (typeof globalThis.Worker === 'function') {
|
|
69
|
+
const url = new URL(
|
|
70
|
+
'./PcbScene3dGeometryWorker.mjs',
|
|
71
|
+
import.meta.url
|
|
72
|
+
)
|
|
73
|
+
url.search = new URL(import.meta.url).search
|
|
74
|
+
this.#worker = new globalThis.Worker(url, {
|
|
75
|
+
type: 'module',
|
|
76
|
+
name: 'pcb-generated-geometry'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
if (!this.#worker) {
|
|
80
|
+
this.#unavailable = true
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
this.#worker.addEventListener('message', (event) =>
|
|
84
|
+
this.#receive(event.data)
|
|
85
|
+
)
|
|
86
|
+
this.#worker.addEventListener('error', (event) =>
|
|
87
|
+
this.#fail(
|
|
88
|
+
new Error(event?.message || 'Geometry worker failed.')
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
this.#worker.addEventListener('messageerror', () =>
|
|
92
|
+
this.#fail(
|
|
93
|
+
new Error('Geometry worker response could not be read.')
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
this.#fail(error)
|
|
98
|
+
}
|
|
99
|
+
return this.#worker
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sends a cloneable scene subset without transferring caller-owned arrays.
|
|
104
|
+
* @param {Worker} worker Active worker.
|
|
105
|
+
* @param {string} kind Geometry stage.
|
|
106
|
+
* @param {object} sceneDescription Normalized scene.
|
|
107
|
+
* @returns {Promise<object>}
|
|
108
|
+
*/
|
|
109
|
+
#request(worker, kind, sceneDescription) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const requestId = 'geometry-' + ++this.#sequence
|
|
112
|
+
const timeout = setTimeout(
|
|
113
|
+
() => this.#fail(new Error('Geometry worker timed out.')),
|
|
114
|
+
this.#options.requestTimeoutMs ?? 120000
|
|
115
|
+
)
|
|
116
|
+
this.#requests.set(requestId, { resolve, reject, timeout })
|
|
117
|
+
try {
|
|
118
|
+
const version = new URL(import.meta.url).search
|
|
119
|
+
worker.postMessage({
|
|
120
|
+
type: 'scene3d:geometry-build',
|
|
121
|
+
requestId,
|
|
122
|
+
kind,
|
|
123
|
+
threeModuleUrl:
|
|
124
|
+
this.#options.threeModuleUrl ||
|
|
125
|
+
'/node_modules/three/build/three.module.js' + version,
|
|
126
|
+
sceneDescription:
|
|
127
|
+
PcbScene3dGeneratedGeometryBuilder.workerInput(
|
|
128
|
+
sceneDescription
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.#fail(error)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Settles a pending request and ignores late replies after cancellation.
|
|
139
|
+
* @param {object} payload Worker reply.
|
|
140
|
+
* @returns {void}
|
|
141
|
+
*/
|
|
142
|
+
#receive(payload) {
|
|
143
|
+
const pending = this.#requests.get(payload?.requestId)
|
|
144
|
+
if (!pending) return
|
|
145
|
+
this.#requests.delete(payload.requestId)
|
|
146
|
+
clearTimeout(pending.timeout)
|
|
147
|
+
if (payload.type === 'scene3d:geometry-success')
|
|
148
|
+
pending.resolve(payload.geometry)
|
|
149
|
+
else
|
|
150
|
+
pending.reject(
|
|
151
|
+
new Error(payload.message || 'Geometry worker failed.')
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Rejects every outstanding stage and permanently stops the failed worker.
|
|
157
|
+
* @param {Error} error Failure or cancellation.
|
|
158
|
+
* @returns {void}
|
|
159
|
+
*/
|
|
160
|
+
#fail(error) {
|
|
161
|
+
this.#unavailable = true
|
|
162
|
+
for (const { reject, timeout } of this.#requests.values()) {
|
|
163
|
+
clearTimeout(timeout)
|
|
164
|
+
reject(error)
|
|
165
|
+
}
|
|
166
|
+
this.#requests.clear()
|
|
167
|
+
this.#worker?.terminate()
|
|
168
|
+
this.#worker = null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Rejects calls on a disposed client. @returns {void} */
|
|
172
|
+
#assertActive() {
|
|
173
|
+
if (this.#disposed) throw this.#abortError()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Creates a portable abort error, including non-DOM hosts. @returns {Error} */
|
|
177
|
+
#abortError() {
|
|
178
|
+
const error = new Error('Generated geometry build was cancelled.')
|
|
179
|
+
error.name = 'AbortError'
|
|
180
|
+
return error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -52,7 +52,9 @@ export class PcbScene3dMaskCoveredCopperSurfaceFilter {
|
|
|
52
52
|
options
|
|
53
53
|
)
|
|
54
54
|
) {
|
|
55
|
-
|
|
55
|
+
for (let offset = 0; offset < 9; offset += 1) {
|
|
56
|
+
filtered.push(source[index + offset])
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -83,32 +85,20 @@ export class PcbScene3dMaskCoveredCopperSurfaceFilter {
|
|
|
83
85
|
* @returns {boolean}
|
|
84
86
|
*/
|
|
85
87
|
static #keepsTriangle(source, index, zBounds, options) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (hasTop
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Checks whether a Z value matches one target plane.
|
|
103
|
-
* @param {number} value Candidate Z.
|
|
104
|
-
* @param {number} target Target Z.
|
|
105
|
-
* @returns {boolean}
|
|
106
|
-
*/
|
|
107
|
-
static #matchesZ(value, target) {
|
|
108
|
-
return (
|
|
109
|
-
Math.abs(Number(value) - Number(target)) <=
|
|
110
|
-
PcbScene3dMaskCoveredCopperSurfaceFilter.#Z_EPSILON
|
|
111
|
-
)
|
|
88
|
+
const a = Number(source[index + 2])
|
|
89
|
+
const b = Number(source[index + 5])
|
|
90
|
+
const c = Number(source[index + 8])
|
|
91
|
+
const epsilon = PcbScene3dMaskCoveredCopperSurfaceFilter.#Z_EPSILON
|
|
92
|
+
const hasTop =
|
|
93
|
+
Math.abs(a - zBounds.maxZ) <= epsilon ||
|
|
94
|
+
Math.abs(b - zBounds.maxZ) <= epsilon ||
|
|
95
|
+
Math.abs(c - zBounds.maxZ) <= epsilon
|
|
96
|
+
if (!hasTop || options?.keepSideWalls === true) return hasTop
|
|
97
|
+
const hasBottom =
|
|
98
|
+
Math.abs(a - zBounds.minZ) <= epsilon ||
|
|
99
|
+
Math.abs(b - zBounds.minZ) <= epsilon ||
|
|
100
|
+
Math.abs(c - zBounds.minZ) <= epsilon
|
|
101
|
+
return !hasBottom
|
|
112
102
|
}
|
|
113
103
|
|
|
114
104
|
/**
|