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
|
@@ -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
|
/**
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { PcbScene3dBoardSolderMaskFactory } from './PcbScene3dBoardSolderMaskFactory.mjs'
|
|
2
1
|
import { PcbScene3dCameraRig } from './PcbScene3dCameraRig.mjs'
|
|
3
2
|
import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs'
|
|
4
3
|
import { PcbScene3dComponentVisibility } from './PcbScene3dComponentVisibility.mjs'
|
|
5
4
|
import { PcbScene3dComponentAdjustment } from './PcbScene3dComponentAdjustment.mjs'
|
|
6
5
|
import { PcbScene3dComponentAdjustmentRegistry } from './PcbScene3dComponentAdjustmentRegistry.mjs'
|
|
7
6
|
import { PcbScene3dCompanionBasePlacementAdjuster } from './PcbScene3dCompanionBasePlacementAdjuster.mjs'
|
|
8
|
-
import { PcbScene3dCopperDetailGroupBuilder } from './PcbScene3dCopperDetailGroupBuilder.mjs'
|
|
9
7
|
import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
|
|
10
8
|
import { PcbScene3dDetailCoordinateNormalizer } from './PcbScene3dDetailCoordinateNormalizer.mjs'
|
|
11
9
|
import { PcbScene3dDeferredModelFinalizer } from './PcbScene3dDeferredModelFinalizer.mjs'
|
|
12
|
-
import {
|
|
10
|
+
import { PcbScene3dGeometryTransfer } from './PcbScene3dGeometryTransfer.mjs'
|
|
11
|
+
import { PcbScene3dGeometryWorkerClient } from './PcbScene3dGeometryWorkerClient.mjs'
|
|
13
12
|
import { PcbScene3dExternalCompanionFallback } from './PcbScene3dExternalCompanionFallback.mjs'
|
|
14
13
|
import { PcbScene3dExternalModels } from './PcbScene3dExternalModels.mjs'
|
|
15
14
|
import { PcbScene3dExternalPlacementDefaults } from './PcbScene3dExternalPlacementDefaults.mjs'
|
|
@@ -75,10 +74,11 @@ export class PcbScene3dRuntime {
|
|
|
75
74
|
#hasSettledReady
|
|
76
75
|
#renderScheduler
|
|
77
76
|
#visibilityGraph
|
|
77
|
+
#geometryWorker
|
|
78
78
|
/**
|
|
79
79
|
* @param {HTMLElement} viewportNode
|
|
80
80
|
* @param {any} sceneDescription Scene description or CircuitJSON model.
|
|
81
|
-
* @param {{ setDiagnostics?: (messages: string[]) => void, setSelection?: (selection: any | null) => void, loadRuntimeModules?: () => Promise<{ THREE: any, OrbitControls: any }>, translate?: ((key: string) => string) | null, modelLoaderOptions?: object }} [hooks]
|
|
81
|
+
* @param {{ setDiagnostics?: (messages: string[]) => void, setSelection?: (selection: any | null) => void, loadRuntimeModules?: () => Promise<{ THREE: any, OrbitControls: any }>, translate?: ((key: string) => string) | null, modelLoaderOptions?: object, geometryWorkerOptions?: object }} [hooks]
|
|
82
82
|
*/
|
|
83
83
|
constructor(viewportNode, sceneDescription, hooks = {}) {
|
|
84
84
|
const renderModel =
|
|
@@ -129,6 +129,9 @@ export class PcbScene3dRuntime {
|
|
|
129
129
|
}
|
|
130
130
|
})
|
|
131
131
|
this.#visibilityGraph = new PcbScene3dVisibilityGraph()
|
|
132
|
+
this.#geometryWorker = new PcbScene3dGeometryWorkerClient(
|
|
133
|
+
hooks.geometryWorkerOptions
|
|
134
|
+
)
|
|
132
135
|
this.#resolveReadyPromise = null
|
|
133
136
|
this.#readyPromise = new Promise((resolve) => {
|
|
134
137
|
this.#resolveReadyPromise = resolve
|
|
@@ -218,6 +221,10 @@ export class PcbScene3dRuntime {
|
|
|
218
221
|
/** @returns {void} */
|
|
219
222
|
dispose() {
|
|
220
223
|
this.#isDisposed = true
|
|
224
|
+
this.#geometryWorker.dispose()
|
|
225
|
+
;['board', 'copper'].forEach((kind) =>
|
|
226
|
+
PcbScene3dGeometryTransfer.dispose(this.#groups.get(kind))
|
|
227
|
+
)
|
|
221
228
|
this.#renderScheduler.cancel()
|
|
222
229
|
this.#listeners.forEach(({ node, type, listener }) => {
|
|
223
230
|
node.removeEventListener?.(type, listener)
|
|
@@ -283,9 +290,13 @@ export class PcbScene3dRuntime {
|
|
|
283
290
|
this.#bindSelectionInteraction()
|
|
284
291
|
this.#applyToggleVisibility()
|
|
285
292
|
this.#render()
|
|
293
|
+
await this.#loadGeneratedGeometry('board')
|
|
294
|
+
if (this.#isDisposed) return
|
|
295
|
+
this.#render()
|
|
286
296
|
await this.#loadDeferredDetail()
|
|
287
297
|
this.#settleReady()
|
|
288
298
|
} catch (error) {
|
|
299
|
+
if (this.#isDisposed) return
|
|
289
300
|
this.#hooks.setDiagnostics?.([
|
|
290
301
|
'3D preview could not start: ' +
|
|
291
302
|
String(error?.message || error || 'Unknown error.')
|
|
@@ -342,44 +353,6 @@ export class PcbScene3dRuntime {
|
|
|
342
353
|
.multiplyScalar(this.#initialRadius * 0.8)
|
|
343
354
|
this.#scene.add(ambientLight, keyLight, fillLight)
|
|
344
355
|
const boardGroup = new THREE.Group()
|
|
345
|
-
boardGroup.add(
|
|
346
|
-
PcbScene3dRuntimeBoardMeshes.buildBoardMesh(
|
|
347
|
-
THREE,
|
|
348
|
-
this.#sceneDescription,
|
|
349
|
-
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
350
|
-
)
|
|
351
|
-
)
|
|
352
|
-
boardGroup.add(
|
|
353
|
-
PcbScene3dBoardSolderMaskFactory.buildGroup(
|
|
354
|
-
THREE,
|
|
355
|
-
this.#sceneDescription,
|
|
356
|
-
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
357
|
-
)
|
|
358
|
-
)
|
|
359
|
-
boardGroup.add(
|
|
360
|
-
PcbScene3dDrillVoidFactory.buildGroup(
|
|
361
|
-
THREE,
|
|
362
|
-
this.#sceneDescription.detail,
|
|
363
|
-
board.thicknessMil / 2,
|
|
364
|
-
-board.thicknessMil / 2,
|
|
365
|
-
(x, y) => this.#normalizeDetailPoint(x, y),
|
|
366
|
-
{
|
|
367
|
-
enabled: true,
|
|
368
|
-
board,
|
|
369
|
-
hasBoardAssemblyModel: Boolean(
|
|
370
|
-
this.#sceneDescription.boardAssemblyModel
|
|
371
|
-
),
|
|
372
|
-
sourceFormat: this.#sceneDescription.sourceFormat
|
|
373
|
-
}
|
|
374
|
-
)
|
|
375
|
-
)
|
|
376
|
-
boardGroup.add(
|
|
377
|
-
PcbScene3dRuntimeBoardMeshes.buildBoardOutline(
|
|
378
|
-
THREE,
|
|
379
|
-
this.#sceneDescription,
|
|
380
|
-
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
381
|
-
)
|
|
382
|
-
)
|
|
383
356
|
this.#groups.set('board', boardGroup)
|
|
384
357
|
PcbScene3dRuntimeBoardMeshes.applyBoardFaceSide(
|
|
385
358
|
this.#three,
|
|
@@ -536,7 +509,8 @@ export class PcbScene3dRuntime {
|
|
|
536
509
|
if (this.#isDisposed) {
|
|
537
510
|
return
|
|
538
511
|
}
|
|
539
|
-
this.#
|
|
512
|
+
await this.#loadGeneratedGeometry('copper')
|
|
513
|
+
if (this.#isDisposed) return
|
|
540
514
|
this.#applyToggleVisibility()
|
|
541
515
|
this.#render()
|
|
542
516
|
this.#settleReady()
|
|
@@ -596,24 +570,29 @@ export class PcbScene3dRuntime {
|
|
|
596
570
|
}
|
|
597
571
|
|
|
598
572
|
/**
|
|
599
|
-
*
|
|
600
|
-
* @
|
|
573
|
+
* Attaches exact worker-built geometry only while its runtime is alive.
|
|
574
|
+
* @param {'board' | 'copper'} kind Generated detail stage.
|
|
575
|
+
* @returns {Promise<void>}
|
|
601
576
|
*/
|
|
602
|
-
#
|
|
603
|
-
const
|
|
604
|
-
if (!
|
|
577
|
+
async #loadGeneratedGeometry(kind) {
|
|
578
|
+
const group = this.#groups.get(kind)
|
|
579
|
+
if (!group || group.children.length || this.#isDisposed) {
|
|
605
580
|
return
|
|
606
581
|
}
|
|
607
|
-
const
|
|
608
|
-
const detailGroup = PcbScene3dCopperDetailGroupBuilder.build(
|
|
582
|
+
const detailGroup = await this.#geometryWorker.build(
|
|
609
583
|
this.#three,
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
(x, y) => this.#normalizeDetailPoint(x, y)
|
|
584
|
+
kind,
|
|
585
|
+
this.#sceneDescription
|
|
613
586
|
)
|
|
587
|
+
if (this.#isDisposed) {
|
|
588
|
+
PcbScene3dGeometryTransfer.dispose(detailGroup)
|
|
589
|
+
return
|
|
590
|
+
}
|
|
614
591
|
if (detailGroup.children.length) {
|
|
615
|
-
|
|
592
|
+
if (kind === 'board') group.add(...[...detailGroup.children])
|
|
593
|
+
else group.add(detailGroup)
|
|
616
594
|
this.#applyViewScale(this.#presetState.get())
|
|
595
|
+
this.#applyToggleVisibility()
|
|
617
596
|
}
|
|
618
597
|
}
|
|
619
598
|
/**
|
|
@@ -41,6 +41,7 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const group = new THREE.Group()
|
|
44
|
+
const cutoutContexts = {}
|
|
44
45
|
for (const side of ['top', 'bottom']) {
|
|
45
46
|
for (const recordKey of PcbScene3dSilkscreenChunkedFactory
|
|
46
47
|
.#RECORD_KEYS) {
|
|
@@ -53,7 +54,8 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
53
54
|
topZ,
|
|
54
55
|
bottomZ,
|
|
55
56
|
normalizeBoardPoint,
|
|
56
|
-
options
|
|
57
|
+
options,
|
|
58
|
+
cutoutContexts
|
|
57
59
|
)
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -72,6 +74,7 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
72
74
|
* @param {number} bottomZ
|
|
73
75
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
74
76
|
* @param {{ yieldToMain?: () => Promise<void> | void, shouldContinue?: () => boolean }} options
|
|
77
|
+
* @param {object} cutoutContexts Build-scoped side preparations.
|
|
75
78
|
* @returns {Promise<void>}
|
|
76
79
|
*/
|
|
77
80
|
static async #appendRecordChunks(
|
|
@@ -83,7 +86,8 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
83
86
|
topZ,
|
|
84
87
|
bottomZ,
|
|
85
88
|
normalizeBoardPoint,
|
|
86
|
-
options
|
|
89
|
+
options,
|
|
90
|
+
cutoutContexts
|
|
87
91
|
) {
|
|
88
92
|
const sideSilkscreen = silkscreen?.[side] || {}
|
|
89
93
|
const records = Array.isArray(sideSilkscreen[recordKey])
|
|
@@ -98,6 +102,13 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
98
102
|
PcbScene3dSilkscreenChunkedFactory.#shouldContinue(options);
|
|
99
103
|
index += batchSize
|
|
100
104
|
) {
|
|
105
|
+
// Prepare lazily so cancelled or empty sides incur no cutout work.
|
|
106
|
+
cutoutContexts[side] ||=
|
|
107
|
+
PcbScene3dSilkscreenFactory.prepareCutoutContext(
|
|
108
|
+
sideSilkscreen,
|
|
109
|
+
normalizeBoardPoint,
|
|
110
|
+
side === 'bottom'
|
|
111
|
+
)
|
|
101
112
|
const chunkGroup = PcbScene3dSilkscreenFactory.buildGroup(
|
|
102
113
|
THREE,
|
|
103
114
|
PcbScene3dSilkscreenChunkedFactory.#buildChunkSilkscreen(
|
|
@@ -108,7 +119,8 @@ export class PcbScene3dSilkscreenChunkedFactory {
|
|
|
108
119
|
),
|
|
109
120
|
topZ,
|
|
110
121
|
bottomZ,
|
|
111
|
-
normalizeBoardPoint
|
|
122
|
+
normalizeBoardPoint,
|
|
123
|
+
cutoutContexts
|
|
112
124
|
)
|
|
113
125
|
if (chunkGroup.children.length) {
|
|
114
126
|
group.add(chunkGroup)
|
|
@@ -66,6 +66,41 @@ export class PcbScene3dSilkscreenCutoutContext {
|
|
|
66
66
|
this.#prepareNormalizedCutout(cutout)
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
/** @type {{ x: number, y: number }[][]} */
|
|
70
|
+
#surfaceCutouts
|
|
71
|
+
|
|
72
|
+
/** @type {WeakMap<any[], object>} */
|
|
73
|
+
#preparedCutoutCache = new WeakMap()
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Owns detached normalized cutouts for one side of a build. Callers must
|
|
77
|
+
* keep these coordinates unchanged for the lifetime of this context.
|
|
78
|
+
* @param {{ x: number, y: number }[][]} [surfaceCutouts] Normalized cutouts.
|
|
79
|
+
*/
|
|
80
|
+
constructor(surfaceCutouts = []) {
|
|
81
|
+
this.#surfaceCutouts = surfaceCutouts
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the normalized surface cutouts shared by every side batch.
|
|
86
|
+
* @returns {{ x: number, y: number }[][]}
|
|
87
|
+
*/
|
|
88
|
+
get surfaceCutouts() {
|
|
89
|
+
return this.#surfaceCutouts
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns build-scoped caches for exact clipping consumers. Weak keys
|
|
94
|
+
* allow temporary fill-specific cutout lists to be released after use.
|
|
95
|
+
* @returns {{ preparedPolygonCache: Map, preparedCutoutCache: WeakMap }}
|
|
96
|
+
*/
|
|
97
|
+
get geometryFilterOptions() {
|
|
98
|
+
return {
|
|
99
|
+
preparedPolygonCache: this.#preparedPolygonCache,
|
|
100
|
+
preparedCutoutCache: this.#preparedCutoutCache
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
/**
|
|
70
105
|
* Returns the lazy cache shared by exact geometry consumers.
|
|
71
106
|
* @returns {Map<any, PcbScene3dPreparedPolygon>}
|
|
@@ -31,23 +31,33 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
31
31
|
* @param {number} topZ
|
|
32
32
|
* @param {number} bottomZ
|
|
33
33
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
34
|
+
* @param {{ top?: PcbScene3dSilkscreenCutoutContext, bottom?: PcbScene3dSilkscreenCutoutContext }} [cutoutContexts] Internal build-scoped side preparations.
|
|
34
35
|
* @returns {any}
|
|
35
36
|
*/
|
|
36
|
-
static buildGroup(
|
|
37
|
+
static buildGroup(
|
|
38
|
+
THREE,
|
|
39
|
+
silkscreen,
|
|
40
|
+
topZ,
|
|
41
|
+
bottomZ,
|
|
42
|
+
normalizeBoardPoint,
|
|
43
|
+
cutoutContexts = {}
|
|
44
|
+
) {
|
|
37
45
|
const group = new THREE.Group()
|
|
38
46
|
const topGroup = PcbScene3dSilkscreenFactory.#buildSideGroup(
|
|
39
47
|
THREE,
|
|
40
48
|
silkscreen?.top,
|
|
41
49
|
Math.abs(Number(topZ || 0)),
|
|
42
50
|
normalizeBoardPoint,
|
|
43
|
-
false
|
|
51
|
+
false,
|
|
52
|
+
cutoutContexts.top
|
|
44
53
|
)
|
|
45
54
|
const bottomGroup = PcbScene3dSilkscreenFactory.#buildSideGroup(
|
|
46
55
|
THREE,
|
|
47
56
|
silkscreen?.bottom,
|
|
48
57
|
Math.abs(Number(bottomZ || 0)),
|
|
49
58
|
normalizeBoardPoint,
|
|
50
|
-
true
|
|
59
|
+
true,
|
|
60
|
+
cutoutContexts.bottom
|
|
51
61
|
)
|
|
52
62
|
|
|
53
63
|
if (topGroup.children.length) {
|
|
@@ -60,6 +70,30 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
60
70
|
return group
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Normalizes detached cutouts once for all batches on one board side.
|
|
75
|
+
* The returned context must not be reused across separate scene builds.
|
|
76
|
+
* @param {{ drillCutouts?: object[][], copperCutouts?: object[][] } | undefined} silkscreen Side artwork.
|
|
77
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
78
|
+
* @param {boolean} mirrorY Whether the side is mirrored.
|
|
79
|
+
* @returns {PcbScene3dSilkscreenCutoutContext}
|
|
80
|
+
*/
|
|
81
|
+
static prepareCutoutContext(silkscreen, normalizeBoardPoint, mirrorY) {
|
|
82
|
+
const drillCutouts = PcbScene3dSilkscreenFactory.#normalizeCutouts(
|
|
83
|
+
silkscreen?.drillCutouts,
|
|
84
|
+
normalizeBoardPoint,
|
|
85
|
+
mirrorY
|
|
86
|
+
)
|
|
87
|
+
const copperCutouts = PcbScene3dSilkscreenFactory.#normalizeCutouts(
|
|
88
|
+
silkscreen?.copperCutouts,
|
|
89
|
+
normalizeBoardPoint,
|
|
90
|
+
mirrorY
|
|
91
|
+
)
|
|
92
|
+
return new PcbScene3dSilkscreenCutoutContext(
|
|
93
|
+
drillCutouts.concat(copperCutouts)
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
63
97
|
/**
|
|
64
98
|
* Builds one side-specific silkscreen group.
|
|
65
99
|
* @param {any} THREE
|
|
@@ -67,9 +101,17 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
67
101
|
* @param {number} z
|
|
68
102
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
69
103
|
* @param {boolean} mirrorY
|
|
104
|
+
* @param {PcbScene3dSilkscreenCutoutContext} [preparedCutoutContext] Shared side preparation.
|
|
70
105
|
* @returns {any}
|
|
71
106
|
*/
|
|
72
|
-
static #buildSideGroup(
|
|
107
|
+
static #buildSideGroup(
|
|
108
|
+
THREE,
|
|
109
|
+
silkscreen,
|
|
110
|
+
z,
|
|
111
|
+
normalizeBoardPoint,
|
|
112
|
+
mirrorY,
|
|
113
|
+
preparedCutoutContext
|
|
114
|
+
) {
|
|
73
115
|
const group = new THREE.Group()
|
|
74
116
|
const strokeColor = PcbScene3dSilkscreenFactory.#resolveMaterialColor(
|
|
75
117
|
silkscreen?.strokeColor
|
|
@@ -89,18 +131,14 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
89
131
|
? fillColor
|
|
90
132
|
: textMaterialColor
|
|
91
133
|
const strokeZ = z + PcbScene3dSilkscreenFactory.#STROKE_Z_OFFSET
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
mirrorY
|
|
101
|
-
)
|
|
102
|
-
const surfaceCutouts = drillCutouts.concat(copperCutouts)
|
|
103
|
-
const cutoutContext = new PcbScene3dSilkscreenCutoutContext()
|
|
134
|
+
const cutoutContext =
|
|
135
|
+
preparedCutoutContext ||
|
|
136
|
+
PcbScene3dSilkscreenFactory.prepareCutoutContext(
|
|
137
|
+
silkscreen,
|
|
138
|
+
normalizeBoardPoint,
|
|
139
|
+
mirrorY
|
|
140
|
+
)
|
|
141
|
+
const surfaceCutouts = cutoutContext.surfaceCutouts
|
|
104
142
|
const strokeMaterial = PcbScene3dSilkscreenFactory.#buildMaterial(
|
|
105
143
|
THREE,
|
|
106
144
|
strokeColor
|
|
@@ -147,9 +185,7 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
147
185
|
mirrorY,
|
|
148
186
|
fillMaterial,
|
|
149
187
|
surfaceCutouts,
|
|
150
|
-
|
|
151
|
-
preparedPolygonCache: cutoutContext.preparedPolygonCache
|
|
152
|
-
}
|
|
188
|
+
cutoutContext.geometryFilterOptions
|
|
153
189
|
)
|
|
154
190
|
const texts = Array.isArray(silkscreen?.texts) ? silkscreen.texts : []
|
|
155
191
|
const renderableTexts = texts.filter(
|
|
@@ -179,7 +215,7 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
179
215
|
fog: false
|
|
180
216
|
},
|
|
181
217
|
mirrorY,
|
|
182
|
-
|
|
218
|
+
...cutoutContext.geometryFilterOptions,
|
|
183
219
|
side: mirrorY ? 'bottom' : 'top'
|
|
184
220
|
}
|
|
185
221
|
)
|
|
@@ -534,7 +570,7 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
534
570
|
maxDepth: 12,
|
|
535
571
|
maxEdgeLength:
|
|
536
572
|
PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH,
|
|
537
|
-
|
|
573
|
+
...cutoutContext.geometryFilterOptions
|
|
538
574
|
}
|
|
539
575
|
)
|
|
540
576
|
const mesh = new THREE.Mesh(geometry, material)
|
|
@@ -823,7 +859,7 @@ export class PcbScene3dSilkscreenFactory {
|
|
|
823
859
|
maxDepth: 12,
|
|
824
860
|
maxEdgeLength:
|
|
825
861
|
PcbScene3dSilkscreenFactory.#CUTOUT_MAX_EDGE_LENGTH,
|
|
826
|
-
|
|
862
|
+
...cutoutContext?.geometryFilterOptions
|
|
827
863
|
}
|
|
828
864
|
),
|
|
829
865
|
material
|
|
@@ -22,7 +22,7 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
|
|
|
22
22
|
* @param {boolean} mirrorY Whether the side is mirrored.
|
|
23
23
|
* @param {any} material Shared fill material.
|
|
24
24
|
* @param {{ x: number, y: number }[][]} [cutouts] Cutouts that should remain uncovered.
|
|
25
|
-
* @param {{ preparedPolygonCache?: Map }} [options] Request-scoped options.
|
|
25
|
+
* @param {{ preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} [options] Request-scoped options.
|
|
26
26
|
* @returns {any[]}
|
|
27
27
|
*/
|
|
28
28
|
static buildMeshes(
|
|
@@ -117,7 +117,7 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
|
|
|
117
117
|
* @param {number[]} positions Position buffer.
|
|
118
118
|
* @param {any} material Shared material.
|
|
119
119
|
* @param {{ x: number, y: number }[][]} cutouts Cutout polygons.
|
|
120
|
-
* @param {{ preparedPolygonCache?: Map }} options Request-scoped options.
|
|
120
|
+
* @param {{ preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} options Request-scoped options.
|
|
121
121
|
* @returns {void}
|
|
122
122
|
*/
|
|
123
123
|
static #appendMesh(meshes, THREE, positions, material, cutouts, options) {
|
|
@@ -139,7 +139,8 @@ export class PcbScene3dSilkscreenFillSeamBuilder {
|
|
|
139
139
|
PcbScene3dCutoutGeometryFilter.filter(THREE, geometry, cutouts, {
|
|
140
140
|
maxDepth: 12,
|
|
141
141
|
maxEdgeLength: 2,
|
|
142
|
-
preparedPolygonCache: options?.preparedPolygonCache
|
|
142
|
+
preparedPolygonCache: options?.preparedPolygonCache,
|
|
143
|
+
preparedCutoutCache: options?.preparedCutoutCache
|
|
143
144
|
}),
|
|
144
145
|
material
|
|
145
146
|
)
|