pcb-scene3d-viewer 1.1.49 → 1.2.0
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 +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import earcut from 'earcut'
|
|
2
|
-
import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
|
|
3
2
|
import { PcbScene3dCopperFillAreaClipper } from './PcbScene3dCopperFillAreaClipper.mjs'
|
|
3
|
+
import { PcbScene3dCopperFillCoverageContext } from './PcbScene3dCopperFillCoverageContext.mjs'
|
|
4
|
+
import { PcbScene3dCopperFillLoopSetResolver } from './PcbScene3dCopperFillLoopSetResolver.mjs'
|
|
4
5
|
import { PcbScene3dCopperFillPolygonBoolean } from './PcbScene3dCopperFillPolygonBoolean.mjs'
|
|
5
6
|
import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
|
|
6
7
|
|
|
@@ -8,8 +9,6 @@ import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionCli
|
|
|
8
9
|
* Builds saved copper fill meshes for the interactive 3D scene.
|
|
9
10
|
*/
|
|
10
11
|
export class PcbScene3dCopperFillMeshBuilder {
|
|
11
|
-
static #AREA_EPSILON = 0.001
|
|
12
|
-
|
|
13
12
|
/**
|
|
14
13
|
* Builds one combined copper fill mesh.
|
|
15
14
|
* @param {any} THREE Three.js namespace.
|
|
@@ -20,7 +19,7 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
20
19
|
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
21
20
|
* @param {any} material Copper material.
|
|
22
21
|
* @param {{ x: number, y: number }[][]} [cutouts] Optional normalized overlay cutouts.
|
|
23
|
-
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} [options] Optional mesh options.
|
|
22
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean, loopSets?: { outer: number[][], holes: number[][][], bounds: object }[], coverageContext?: object }} [options] Optional mesh options.
|
|
24
23
|
* @returns {any | null}
|
|
25
24
|
*/
|
|
26
25
|
static build(
|
|
@@ -38,6 +37,13 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
38
37
|
const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
|
|
39
38
|
const bottomZ = Number(z || 0) - halfThickness
|
|
40
39
|
const topZ = Number(z || 0) + halfThickness
|
|
40
|
+
const loopSets = Array.isArray(options?.loopSets)
|
|
41
|
+
? options.loopSets
|
|
42
|
+
: PcbScene3dCopperFillLoopSetResolver.resolve(
|
|
43
|
+
fills,
|
|
44
|
+
normalizeBoardPoint,
|
|
45
|
+
mirrorY
|
|
46
|
+
)
|
|
41
47
|
|
|
42
48
|
if (
|
|
43
49
|
PcbScene3dCopperFillMeshBuilder.#shouldClipContainedFillOverlaps(
|
|
@@ -47,22 +53,18 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
47
53
|
PcbScene3dCopperFillMeshBuilder.#appendClippedFillPositions(
|
|
48
54
|
THREE,
|
|
49
55
|
positions,
|
|
50
|
-
|
|
56
|
+
loopSets,
|
|
51
57
|
bottomZ,
|
|
52
58
|
topZ,
|
|
53
|
-
normalizeBoardPoint,
|
|
54
|
-
mirrorY,
|
|
55
59
|
options
|
|
56
60
|
)
|
|
57
61
|
} else {
|
|
58
|
-
for (const
|
|
59
|
-
PcbScene3dCopperFillMeshBuilder.#
|
|
62
|
+
for (const loopSet of loopSets) {
|
|
63
|
+
PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
|
|
60
64
|
positions,
|
|
61
|
-
|
|
65
|
+
loopSet,
|
|
62
66
|
bottomZ,
|
|
63
67
|
topZ,
|
|
64
|
-
normalizeBoardPoint,
|
|
65
|
-
mirrorY,
|
|
66
68
|
options
|
|
67
69
|
)
|
|
68
70
|
}
|
|
@@ -107,70 +109,34 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
107
109
|
return PcbScene3dCopperOcclusionClipper.filter(THREE, geometry, cutouts)
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
/**
|
|
111
|
-
* Appends one filled primitive to a shared position buffer.
|
|
112
|
-
* @param {number[]} positions Position buffer.
|
|
113
|
-
* @param {object} fill Filled copper primitive.
|
|
114
|
-
* @param {number} bottomZ Lower Z.
|
|
115
|
-
* @param {number} topZ Upper Z.
|
|
116
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
117
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
118
|
-
* @param {{ surfaceOnly?: boolean }} options Mesh options.
|
|
119
|
-
* @returns {void}
|
|
120
|
-
*/
|
|
121
|
-
static #appendFillPositions(
|
|
122
|
-
positions,
|
|
123
|
-
fill,
|
|
124
|
-
bottomZ,
|
|
125
|
-
topZ,
|
|
126
|
-
normalizeBoardPoint,
|
|
127
|
-
mirrorY,
|
|
128
|
-
options
|
|
129
|
-
) {
|
|
130
|
-
for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(fill)) {
|
|
131
|
-
PcbScene3dCopperFillMeshBuilder.#appendLoopSetPositions(
|
|
132
|
-
positions,
|
|
133
|
-
loops,
|
|
134
|
-
bottomZ,
|
|
135
|
-
topZ,
|
|
136
|
-
normalizeBoardPoint,
|
|
137
|
-
mirrorY,
|
|
138
|
-
options
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
112
|
/**
|
|
144
113
|
* Appends fill positions while removing duplicate overlap surfaces.
|
|
145
114
|
* @param {any} THREE Three.js namespace.
|
|
146
115
|
* @param {number[]} positions Position buffer.
|
|
147
|
-
* @param {object[]}
|
|
116
|
+
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets Normalized fill islands.
|
|
148
117
|
* @param {number} bottomZ Lower Z.
|
|
149
118
|
* @param {number} topZ Upper Z.
|
|
150
|
-
* @param {
|
|
151
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
152
|
-
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
|
|
119
|
+
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean, coverageContext?: object }} options Mesh options.
|
|
153
120
|
* @returns {void}
|
|
154
121
|
*/
|
|
155
122
|
static #appendClippedFillPositions(
|
|
156
123
|
THREE,
|
|
157
124
|
positions,
|
|
158
|
-
|
|
125
|
+
loopSets,
|
|
159
126
|
bottomZ,
|
|
160
127
|
topZ,
|
|
161
|
-
normalizeBoardPoint,
|
|
162
|
-
mirrorY,
|
|
163
128
|
options
|
|
164
129
|
) {
|
|
165
|
-
const loopSets = PcbScene3dCopperFillMeshBuilder.#resolveLoopSets(
|
|
166
|
-
fills,
|
|
167
|
-
normalizeBoardPoint,
|
|
168
|
-
mirrorY
|
|
169
|
-
)
|
|
170
130
|
const emittedPolygons = []
|
|
171
131
|
const emittedLoopSets = []
|
|
132
|
+
let coverageContext = null
|
|
172
133
|
|
|
173
|
-
for (
|
|
134
|
+
for (
|
|
135
|
+
let loopSetIndex = 0;
|
|
136
|
+
loopSetIndex < loopSets.length;
|
|
137
|
+
loopSetIndex += 1
|
|
138
|
+
) {
|
|
139
|
+
const loopSet = loopSets[loopSetIndex]
|
|
174
140
|
const remainingLoopSets =
|
|
175
141
|
PcbScene3dCopperFillPolygonBoolean.resolveRemainingLoopSets(
|
|
176
142
|
loopSet,
|
|
@@ -188,11 +154,23 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
188
154
|
)
|
|
189
155
|
}
|
|
190
156
|
} else {
|
|
157
|
+
if (!coverageContext) {
|
|
158
|
+
coverageContext =
|
|
159
|
+
options?.coverageContext?.matchesLoopSets?.(
|
|
160
|
+
loopSets
|
|
161
|
+
) === true
|
|
162
|
+
? options.coverageContext
|
|
163
|
+
: PcbScene3dCopperFillCoverageContext.fromLoopSets(
|
|
164
|
+
loopSets
|
|
165
|
+
)
|
|
166
|
+
}
|
|
191
167
|
PcbScene3dCopperFillMeshBuilder.#appendTriangleClippedLoopSet(
|
|
192
168
|
THREE,
|
|
193
169
|
positions,
|
|
194
170
|
loopSet,
|
|
195
171
|
emittedLoopSets,
|
|
172
|
+
loopSetIndex,
|
|
173
|
+
coverageContext,
|
|
196
174
|
bottomZ,
|
|
197
175
|
topZ,
|
|
198
176
|
options
|
|
@@ -214,6 +192,8 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
214
192
|
* @param {number[]} positions Position buffer.
|
|
215
193
|
* @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Candidate loop set.
|
|
216
194
|
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} emittedLoopSets Already emitted loop sets.
|
|
195
|
+
* @param {number} loopSetIndex Flattened candidate source index.
|
|
196
|
+
* @param {object} coverageContext Prepared ordered fill coverage.
|
|
217
197
|
* @param {number} bottomZ Lower Z.
|
|
218
198
|
* @param {number} topZ Upper Z.
|
|
219
199
|
* @param {{ surfaceOnly?: boolean, clipContainedFillOverlaps?: boolean }} options Mesh options.
|
|
@@ -224,6 +204,8 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
224
204
|
positions,
|
|
225
205
|
loopSet,
|
|
226
206
|
emittedLoopSets,
|
|
207
|
+
loopSetIndex,
|
|
208
|
+
coverageContext,
|
|
227
209
|
bottomZ,
|
|
228
210
|
topZ,
|
|
229
211
|
options
|
|
@@ -249,10 +231,12 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
249
231
|
)
|
|
250
232
|
PcbScene3dCopperFillMeshBuilder.#appendPositions(
|
|
251
233
|
positions,
|
|
252
|
-
PcbScene3dCopperFillMeshBuilder.#
|
|
234
|
+
PcbScene3dCopperFillMeshBuilder.#clipPositionsAgainstCoverage(
|
|
253
235
|
THREE,
|
|
254
236
|
loopPositions,
|
|
255
|
-
|
|
237
|
+
coverageContext,
|
|
238
|
+
loopSetIndex,
|
|
239
|
+
PcbScene3dCopperFillMeshBuilder.#resolveClipSourceIndexes(
|
|
256
240
|
loopSet,
|
|
257
241
|
emittedLoopSets
|
|
258
242
|
)
|
|
@@ -261,14 +245,21 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
261
245
|
}
|
|
262
246
|
|
|
263
247
|
/**
|
|
264
|
-
* Resolves earlier loop sets that can
|
|
248
|
+
* Resolves indexes of earlier loop sets that can overlap a candidate.
|
|
265
249
|
* @param {{ outer: number[][], holes: number[][][], bounds: object }} candidate Candidate loop set.
|
|
266
250
|
* @param {{ outer: number[][], holes: number[][][], bounds: object }[]} loopSets Emitted loop sets.
|
|
267
|
-
* @returns {
|
|
251
|
+
* @returns {Set<number>}
|
|
268
252
|
*/
|
|
269
|
-
static #
|
|
270
|
-
|
|
271
|
-
|
|
253
|
+
static #resolveClipSourceIndexes(candidate, loopSets) {
|
|
254
|
+
const sourceIndexes = new Set()
|
|
255
|
+
|
|
256
|
+
for (
|
|
257
|
+
let sourceIndex = 0;
|
|
258
|
+
sourceIndex < loopSets.length;
|
|
259
|
+
sourceIndex += 1
|
|
260
|
+
) {
|
|
261
|
+
const loopSet = loopSets[sourceIndex]
|
|
262
|
+
if (
|
|
272
263
|
PcbScene3dCopperFillMeshBuilder.#boundsOverlap(
|
|
273
264
|
candidate.bounds,
|
|
274
265
|
loopSet.bounds
|
|
@@ -277,7 +268,12 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
277
268
|
candidate,
|
|
278
269
|
loopSet
|
|
279
270
|
)
|
|
280
|
-
|
|
271
|
+
) {
|
|
272
|
+
sourceIndexes.add(sourceIndex)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return sourceIndexes
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
/**
|
|
@@ -293,14 +289,22 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
293
289
|
}
|
|
294
290
|
|
|
295
291
|
/**
|
|
296
|
-
* Clips position triangles against
|
|
292
|
+
* Clips position triangles against eligible prepared fill areas.
|
|
297
293
|
* @param {any} THREE Three.js namespace.
|
|
298
294
|
* @param {number[]} positions Candidate triangle positions.
|
|
299
|
-
* @param {
|
|
295
|
+
* @param {object} coverageContext Prepared ordered fill coverage.
|
|
296
|
+
* @param {number} beforeSourceIndex Exclusive source-order prefix.
|
|
297
|
+
* @param {Set<number>} allowedSourceIndexes Eligible earlier sources.
|
|
300
298
|
* @returns {number[]}
|
|
301
299
|
*/
|
|
302
|
-
static #
|
|
303
|
-
|
|
300
|
+
static #clipPositionsAgainstCoverage(
|
|
301
|
+
THREE,
|
|
302
|
+
positions,
|
|
303
|
+
coverageContext,
|
|
304
|
+
beforeSourceIndex,
|
|
305
|
+
allowedSourceIndexes
|
|
306
|
+
) {
|
|
307
|
+
if (!positions.length || !allowedSourceIndexes.size) {
|
|
304
308
|
return positions
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -310,12 +314,11 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
310
314
|
new THREE.Float32BufferAttribute(positions, 3)
|
|
311
315
|
)
|
|
312
316
|
const mesh = new THREE.Mesh(geometry)
|
|
313
|
-
const clippedMesh = PcbScene3dCopperFillAreaClipper.
|
|
317
|
+
const clippedMesh = PcbScene3dCopperFillAreaClipper.filterPrepared(
|
|
314
318
|
THREE,
|
|
315
319
|
mesh,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
false
|
|
320
|
+
coverageContext,
|
|
321
|
+
{ beforeSourceIndex, allowedSourceIndexes }
|
|
319
322
|
)
|
|
320
323
|
if (!clippedMesh) {
|
|
321
324
|
return []
|
|
@@ -323,77 +326,6 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
323
326
|
|
|
324
327
|
return Array.from(clippedMesh.geometry.attributes.position.array)
|
|
325
328
|
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Converts normalized loop sets back to fill primitives for shared clipping.
|
|
329
|
-
* @param {{ outer: number[][], holes: number[][][] }[]} loopSets Loop sets.
|
|
330
|
-
* @returns {object[]}
|
|
331
|
-
*/
|
|
332
|
-
static #loopSetsToFills(loopSets) {
|
|
333
|
-
return loopSets.map((loopSet) => ({
|
|
334
|
-
points: loopSet.outer.map((point) => ({
|
|
335
|
-
x: point[0],
|
|
336
|
-
y: point[1]
|
|
337
|
-
})),
|
|
338
|
-
holes: (loopSet.holes || []).map((hole) =>
|
|
339
|
-
hole.map((point) => ({
|
|
340
|
-
x: point[0],
|
|
341
|
-
y: point[1]
|
|
342
|
-
}))
|
|
343
|
-
)
|
|
344
|
-
}))
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Appends one fill island loop set to a shared position buffer.
|
|
349
|
-
* @param {number[]} positions Position buffer.
|
|
350
|
-
* @param {{ outer: number[][], holes: number[][][] }} loops Fill loop set.
|
|
351
|
-
* @param {number} bottomZ Lower Z.
|
|
352
|
-
* @param {number} topZ Upper Z.
|
|
353
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
354
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
355
|
-
* @param {{ surfaceOnly?: boolean }} options Mesh options.
|
|
356
|
-
* @returns {void}
|
|
357
|
-
*/
|
|
358
|
-
static #appendLoopSetPositions(
|
|
359
|
-
positions,
|
|
360
|
-
loops,
|
|
361
|
-
bottomZ,
|
|
362
|
-
topZ,
|
|
363
|
-
normalizeBoardPoint,
|
|
364
|
-
mirrorY,
|
|
365
|
-
options
|
|
366
|
-
) {
|
|
367
|
-
const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
368
|
-
loops.outer,
|
|
369
|
-
normalizeBoardPoint,
|
|
370
|
-
mirrorY
|
|
371
|
-
)
|
|
372
|
-
const holes = (loops.holes || [])
|
|
373
|
-
.map((loop) =>
|
|
374
|
-
PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
375
|
-
loop,
|
|
376
|
-
normalizeBoardPoint,
|
|
377
|
-
mirrorY
|
|
378
|
-
)
|
|
379
|
-
)
|
|
380
|
-
.filter((loop) =>
|
|
381
|
-
PcbScene3dCopperFillMeshBuilder.#isValidLoop(loop)
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
if (!PcbScene3dCopperFillMeshBuilder.#isValidLoop(outer)) {
|
|
385
|
-
return
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
PcbScene3dCopperFillMeshBuilder.#appendNormalizedLoopSetPositions(
|
|
389
|
-
positions,
|
|
390
|
-
{ outer, holes },
|
|
391
|
-
bottomZ,
|
|
392
|
-
topZ,
|
|
393
|
-
options
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
329
|
/**
|
|
398
330
|
* Appends one already-normalized fill island loop set.
|
|
399
331
|
* @param {number[]} positions Position buffer.
|
|
@@ -453,63 +385,6 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
453
385
|
}
|
|
454
386
|
}
|
|
455
387
|
|
|
456
|
-
/**
|
|
457
|
-
* Resolves normalized fill loop sets.
|
|
458
|
-
* @param {object[]} fills Filled copper primitives.
|
|
459
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
460
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
461
|
-
* @returns {{ outer: number[][], holes: number[][][], bounds: object }[]}
|
|
462
|
-
*/
|
|
463
|
-
static #resolveLoopSets(fills, normalizeBoardPoint, mirrorY) {
|
|
464
|
-
return (fills || []).flatMap((fill) =>
|
|
465
|
-
PcbAssemblyFillGeometryResolver.resolveAll(fill)
|
|
466
|
-
.map((loops) =>
|
|
467
|
-
PcbScene3dCopperFillMeshBuilder.#normalizeLoopSet(
|
|
468
|
-
loops,
|
|
469
|
-
normalizeBoardPoint,
|
|
470
|
-
mirrorY
|
|
471
|
-
)
|
|
472
|
-
)
|
|
473
|
-
.filter(Boolean)
|
|
474
|
-
)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Normalizes one fill loop set.
|
|
479
|
-
* @param {{ outer: number[][], holes: number[][][] }} loops Source loops.
|
|
480
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
481
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
482
|
-
* @returns {{ outer: number[][], holes: number[][][], bounds: object } | null}
|
|
483
|
-
*/
|
|
484
|
-
static #normalizeLoopSet(loops, normalizeBoardPoint, mirrorY) {
|
|
485
|
-
const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
486
|
-
loops.outer,
|
|
487
|
-
normalizeBoardPoint,
|
|
488
|
-
mirrorY
|
|
489
|
-
)
|
|
490
|
-
if (!PcbScene3dCopperFillMeshBuilder.#isValidLoop(outer)) {
|
|
491
|
-
return null
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const holes = (loops.holes || [])
|
|
495
|
-
.map((loop) =>
|
|
496
|
-
PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
497
|
-
loop,
|
|
498
|
-
normalizeBoardPoint,
|
|
499
|
-
mirrorY
|
|
500
|
-
)
|
|
501
|
-
)
|
|
502
|
-
.filter((loop) =>
|
|
503
|
-
PcbScene3dCopperFillMeshBuilder.#isValidLoop(loop)
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
outer,
|
|
508
|
-
holes,
|
|
509
|
-
bounds: PcbScene3dCopperFillMeshBuilder.#loopBounds(outer)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
388
|
/**
|
|
514
389
|
* Resolves later fill loops fully contained by an earlier loop set.
|
|
515
390
|
* @param {{ outer: number[][], holes: number[][][], bounds: object }} loopSet Current loop set.
|
|
@@ -579,31 +454,6 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
579
454
|
return options?.clipContainedFillOverlaps === true
|
|
580
455
|
}
|
|
581
456
|
|
|
582
|
-
/**
|
|
583
|
-
* Normalizes one loop into local copper-side coordinates.
|
|
584
|
-
* @param {number[][]} loop Source loop.
|
|
585
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
586
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
587
|
-
* @returns {number[][]}
|
|
588
|
-
*/
|
|
589
|
-
static #normalizeLoop(loop, normalizeBoardPoint, mirrorY) {
|
|
590
|
-
const points = []
|
|
591
|
-
for (const point of loop || []) {
|
|
592
|
-
const normalized = normalizeBoardPoint(
|
|
593
|
-
Number(point?.[0]),
|
|
594
|
-
Number(point?.[1])
|
|
595
|
-
)
|
|
596
|
-
const nextPoint = [
|
|
597
|
-
Number(normalized?.x),
|
|
598
|
-
mirrorY ? -Number(normalized?.y) : Number(normalized?.y)
|
|
599
|
-
]
|
|
600
|
-
if (nextPoint.every(Number.isFinite)) {
|
|
601
|
-
points.push(nextPoint)
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
return PcbScene3dCopperFillMeshBuilder.#cleanLoop(points)
|
|
605
|
-
}
|
|
606
|
-
|
|
607
457
|
/**
|
|
608
458
|
* Flattens loops for triangulation.
|
|
609
459
|
* @param {number[][]} outer Outer loop.
|
|
@@ -711,59 +561,6 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
711
561
|
positions.push(point[0], point[1], z)
|
|
712
562
|
}
|
|
713
563
|
|
|
714
|
-
/**
|
|
715
|
-
* Removes invalid and duplicate loop points.
|
|
716
|
-
* @param {number[][]} points Candidate points.
|
|
717
|
-
* @returns {number[][]}
|
|
718
|
-
*/
|
|
719
|
-
static #cleanLoop(points) {
|
|
720
|
-
const loop = []
|
|
721
|
-
for (const point of points || []) {
|
|
722
|
-
const x = Number(point?.[0])
|
|
723
|
-
const y = Number(point?.[1])
|
|
724
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
725
|
-
continue
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const previous = loop[loop.length - 1]
|
|
729
|
-
if (
|
|
730
|
-
previous &&
|
|
731
|
-
Math.abs(previous[0] - x) < 0.001 &&
|
|
732
|
-
Math.abs(previous[1] - y) < 0.001
|
|
733
|
-
) {
|
|
734
|
-
continue
|
|
735
|
-
}
|
|
736
|
-
loop.push([x, y])
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const first = loop[0]
|
|
740
|
-
const last = loop[loop.length - 1]
|
|
741
|
-
if (
|
|
742
|
-
first &&
|
|
743
|
-
last &&
|
|
744
|
-
Math.abs(first[0] - last[0]) < 0.001 &&
|
|
745
|
-
Math.abs(first[1] - last[1]) < 0.001
|
|
746
|
-
) {
|
|
747
|
-
loop.pop()
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
return loop
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/**
|
|
754
|
-
* Checks whether one loop has enough non-collinear area.
|
|
755
|
-
* @param {number[][]} loop Candidate loop.
|
|
756
|
-
* @returns {boolean}
|
|
757
|
-
*/
|
|
758
|
-
static #isValidLoop(loop) {
|
|
759
|
-
return (
|
|
760
|
-
Array.isArray(loop) &&
|
|
761
|
-
loop.length >= 3 &&
|
|
762
|
-
Math.abs(PcbScene3dCopperFillMeshBuilder.#signedArea(loop)) >
|
|
763
|
-
PcbScene3dCopperFillMeshBuilder.#AREA_EPSILON
|
|
764
|
-
)
|
|
765
|
-
}
|
|
766
|
-
|
|
767
564
|
/**
|
|
768
565
|
* Computes axis-aligned bounds for one loop.
|
|
769
566
|
* @param {number[][]} loop Candidate loop.
|
|
@@ -897,19 +694,4 @@ export class PcbScene3dCopperFillMeshBuilder {
|
|
|
897
694
|
|
|
898
695
|
return inside
|
|
899
696
|
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Computes signed loop area.
|
|
903
|
-
* @param {number[][]} loop Candidate loop.
|
|
904
|
-
* @returns {number}
|
|
905
|
-
*/
|
|
906
|
-
static #signedArea(loop) {
|
|
907
|
-
let area = 0
|
|
908
|
-
for (let index = 0; index < loop.length; index += 1) {
|
|
909
|
-
const current = loop[index]
|
|
910
|
-
const next = loop[(index + 1) % loop.length]
|
|
911
|
-
area += current[0] * next[1] - next[0] * current[1]
|
|
912
|
-
}
|
|
913
|
-
return area / 2
|
|
914
|
-
}
|
|
915
697
|
}
|
|
@@ -17,7 +17,7 @@ export class PcbScene3dCopperTextFactory {
|
|
|
17
17
|
* @param {any[]} texts
|
|
18
18
|
* @param {number} z
|
|
19
19
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
20
|
-
* @param {{ side?: 'top' | 'bottom', mirrorY?: boolean, materialColor?: number, materialProperties?: { materialKind?: 'basic' | 'standard', roughness?: number, metalness?: number, transparent?: boolean, opacity?: number, toneMapped?: boolean, fog?: boolean }, filterSide?: boolean, glyphYUp?: boolean, drillCutouts?: { x: number, y: number }[][] }} [options]
|
|
20
|
+
* @param {{ side?: 'top' | 'bottom', mirrorY?: boolean, materialColor?: number, materialProperties?: { materialKind?: 'basic' | 'standard', roughness?: number, metalness?: number, transparent?: boolean, opacity?: number, toneMapped?: boolean, fog?: boolean }, filterSide?: boolean, glyphYUp?: boolean, drillCutouts?: { x: number, y: number }[][], preparedPolygonCache?: Map }} [options]
|
|
21
21
|
* @returns {any}
|
|
22
22
|
*/
|
|
23
23
|
static buildGroup(THREE, texts, z, normalizeBoardPoint, options = {}) {
|
|
@@ -59,7 +59,8 @@ export class PcbScene3dCopperTextFactory {
|
|
|
59
59
|
PcbScene3dCopperTextFactory.#filterDrillCutouts(
|
|
60
60
|
THREE,
|
|
61
61
|
geometry,
|
|
62
|
-
options?.drillCutouts
|
|
62
|
+
options?.drillCutouts,
|
|
63
|
+
options?.preparedPolygonCache
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
if (
|
|
@@ -88,14 +89,21 @@ export class PcbScene3dCopperTextFactory {
|
|
|
88
89
|
* @param {any} THREE
|
|
89
90
|
* @param {any} geometry
|
|
90
91
|
* @param {{ x: number, y: number }[][] | undefined} drillCutouts
|
|
92
|
+
* @param {Map | undefined} preparedPolygonCache Request-scoped prepared cache.
|
|
91
93
|
* @returns {any}
|
|
92
94
|
*/
|
|
93
|
-
static #filterDrillCutouts(
|
|
95
|
+
static #filterDrillCutouts(
|
|
96
|
+
THREE,
|
|
97
|
+
geometry,
|
|
98
|
+
drillCutouts,
|
|
99
|
+
preparedPolygonCache
|
|
100
|
+
) {
|
|
94
101
|
return Array.isArray(drillCutouts) && drillCutouts.length
|
|
95
102
|
? PcbScene3dCutoutGeometryFilter.filter(
|
|
96
103
|
THREE,
|
|
97
104
|
geometry,
|
|
98
|
-
drillCutouts
|
|
105
|
+
drillCutouts,
|
|
106
|
+
{ preparedPolygonCache }
|
|
99
107
|
)
|
|
100
108
|
: geometry
|
|
101
109
|
}
|
|
@@ -21,14 +21,27 @@ export class PcbScene3dCutoutCircleDetector {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const center = PcbScene3dCutoutCircleDetector.#resolveCentroid(points)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
let radiusSum = 0
|
|
25
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
26
|
+
if (!(index in points)) {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const point = points[index]
|
|
31
|
+
radiusSum += Math.hypot(point.x - center.x, point.y - center.y)
|
|
32
|
+
}
|
|
33
|
+
const radius = radiusSum / points.length
|
|
34
|
+
let maxError = -Infinity
|
|
35
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
36
|
+
if (!(index in points)) {
|
|
37
|
+
maxError = Math.max(maxError, undefined)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const point = points[index]
|
|
42
|
+
const value = Math.hypot(point.x - center.x, point.y - center.y)
|
|
43
|
+
maxError = Math.max(maxError, Math.abs(value - radius))
|
|
44
|
+
}
|
|
32
45
|
const tolerance = Math.max(
|
|
33
46
|
Number(epsilon || 0),
|
|
34
47
|
radius * PcbScene3dCutoutCircleDetector.#MAX_RELATIVE_ERROR
|
|
@@ -68,17 +81,21 @@ export class PcbScene3dCutoutCircleDetector {
|
|
|
68
81
|
* @returns {{ x: number, y: number }}
|
|
69
82
|
*/
|
|
70
83
|
static #resolveCentroid(points) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
let totalX = 0
|
|
85
|
+
let totalY = 0
|
|
86
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
87
|
+
if (!(index in points)) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const point = points[index]
|
|
92
|
+
totalX += Number(point.x || 0)
|
|
93
|
+
totalY += Number(point.y || 0)
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
return {
|
|
80
|
-
x:
|
|
81
|
-
y:
|
|
97
|
+
x: totalX / points.length,
|
|
98
|
+
y: totalY / points.length
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
}
|