pcb-scene3d-viewer 1.1.21 → 1.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +3 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import earcut from 'earcut'
|
|
2
|
+
import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
|
|
3
|
+
import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds saved copper fill meshes for the interactive 3D scene.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dCopperFillMeshBuilder {
|
|
9
|
+
static #AREA_EPSILON = 0.001
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds one combined copper fill mesh.
|
|
13
|
+
* @param {any} THREE Three.js namespace.
|
|
14
|
+
* @param {object[]} fills Filled copper primitives.
|
|
15
|
+
* @param {number} z Center Z.
|
|
16
|
+
* @param {number} thickness Visual copper thickness.
|
|
17
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
18
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
19
|
+
* @param {any} material Copper material.
|
|
20
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional normalized overlay cutouts.
|
|
21
|
+
* @returns {any | null}
|
|
22
|
+
*/
|
|
23
|
+
static build(
|
|
24
|
+
THREE,
|
|
25
|
+
fills,
|
|
26
|
+
z,
|
|
27
|
+
thickness,
|
|
28
|
+
normalizeBoardPoint,
|
|
29
|
+
mirrorY,
|
|
30
|
+
material,
|
|
31
|
+
cutouts = []
|
|
32
|
+
) {
|
|
33
|
+
const positions = []
|
|
34
|
+
const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
|
|
35
|
+
const bottomZ = Number(z || 0) - halfThickness
|
|
36
|
+
const topZ = Number(z || 0) + halfThickness
|
|
37
|
+
|
|
38
|
+
for (const fill of fills || []) {
|
|
39
|
+
PcbScene3dCopperFillMeshBuilder.#appendFillPositions(
|
|
40
|
+
positions,
|
|
41
|
+
fill,
|
|
42
|
+
bottomZ,
|
|
43
|
+
topZ,
|
|
44
|
+
normalizeBoardPoint,
|
|
45
|
+
mirrorY
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!positions.length) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const geometry = new THREE.BufferGeometry()
|
|
54
|
+
geometry.setAttribute(
|
|
55
|
+
'position',
|
|
56
|
+
new THREE.Float32BufferAttribute(positions, 3)
|
|
57
|
+
)
|
|
58
|
+
const clippedGeometry = PcbScene3dCopperFillMeshBuilder.#clipGeometry(
|
|
59
|
+
THREE,
|
|
60
|
+
geometry,
|
|
61
|
+
cutouts
|
|
62
|
+
)
|
|
63
|
+
if (!clippedGeometry) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mesh = new THREE.Mesh(clippedGeometry, material)
|
|
68
|
+
mesh.name = 'copper-fills'
|
|
69
|
+
return mesh
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clips fill geometry against optional overlay cutouts.
|
|
74
|
+
* @param {any} THREE Three.js namespace.
|
|
75
|
+
* @param {any} geometry Source fill geometry.
|
|
76
|
+
* @param {{ x: number, y: number }[][]} cutouts Normalized overlay cutouts.
|
|
77
|
+
* @returns {any | null}
|
|
78
|
+
*/
|
|
79
|
+
static #clipGeometry(THREE, geometry, cutouts) {
|
|
80
|
+
if (!Array.isArray(cutouts) || !cutouts.length) {
|
|
81
|
+
geometry.computeVertexNormals?.()
|
|
82
|
+
return geometry
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return PcbScene3dCopperOcclusionClipper.filter(THREE, geometry, cutouts)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Appends one filled primitive to a shared position buffer.
|
|
90
|
+
* @param {number[]} positions Position buffer.
|
|
91
|
+
* @param {object} fill Filled copper primitive.
|
|
92
|
+
* @param {number} bottomZ Lower Z.
|
|
93
|
+
* @param {number} topZ Upper Z.
|
|
94
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
95
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
98
|
+
static #appendFillPositions(
|
|
99
|
+
positions,
|
|
100
|
+
fill,
|
|
101
|
+
bottomZ,
|
|
102
|
+
topZ,
|
|
103
|
+
normalizeBoardPoint,
|
|
104
|
+
mirrorY
|
|
105
|
+
) {
|
|
106
|
+
for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(fill)) {
|
|
107
|
+
PcbScene3dCopperFillMeshBuilder.#appendLoopSetPositions(
|
|
108
|
+
positions,
|
|
109
|
+
loops,
|
|
110
|
+
bottomZ,
|
|
111
|
+
topZ,
|
|
112
|
+
normalizeBoardPoint,
|
|
113
|
+
mirrorY
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Appends one fill island loop set to a shared position buffer.
|
|
120
|
+
* @param {number[]} positions Position buffer.
|
|
121
|
+
* @param {{ outer: number[][], holes: number[][][] }} loops Fill loop set.
|
|
122
|
+
* @param {number} bottomZ Lower Z.
|
|
123
|
+
* @param {number} topZ Upper Z.
|
|
124
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
125
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
static #appendLoopSetPositions(
|
|
129
|
+
positions,
|
|
130
|
+
loops,
|
|
131
|
+
bottomZ,
|
|
132
|
+
topZ,
|
|
133
|
+
normalizeBoardPoint,
|
|
134
|
+
mirrorY
|
|
135
|
+
) {
|
|
136
|
+
const outer = PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
137
|
+
loops.outer,
|
|
138
|
+
normalizeBoardPoint,
|
|
139
|
+
mirrorY
|
|
140
|
+
)
|
|
141
|
+
const holes = (loops.holes || [])
|
|
142
|
+
.map((loop) =>
|
|
143
|
+
PcbScene3dCopperFillMeshBuilder.#normalizeLoop(
|
|
144
|
+
loop,
|
|
145
|
+
normalizeBoardPoint,
|
|
146
|
+
mirrorY
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
.filter((loop) =>
|
|
150
|
+
PcbScene3dCopperFillMeshBuilder.#isValidLoop(loop)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if (!PcbScene3dCopperFillMeshBuilder.#isValidLoop(outer)) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { points, flat, holeIndexes } =
|
|
158
|
+
PcbScene3dCopperFillMeshBuilder.#flattenLoops(outer, holes)
|
|
159
|
+
const triangles = earcut(flat, holeIndexes, 2)
|
|
160
|
+
if (!triangles.length) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
PcbScene3dCopperFillMeshBuilder.#appendSurfaceTriangles(
|
|
165
|
+
positions,
|
|
166
|
+
points,
|
|
167
|
+
triangles,
|
|
168
|
+
topZ,
|
|
169
|
+
false
|
|
170
|
+
)
|
|
171
|
+
PcbScene3dCopperFillMeshBuilder.#appendSurfaceTriangles(
|
|
172
|
+
positions,
|
|
173
|
+
points,
|
|
174
|
+
triangles,
|
|
175
|
+
bottomZ,
|
|
176
|
+
true
|
|
177
|
+
)
|
|
178
|
+
PcbScene3dCopperFillMeshBuilder.#appendLoopWalls(
|
|
179
|
+
positions,
|
|
180
|
+
outer,
|
|
181
|
+
bottomZ,
|
|
182
|
+
topZ
|
|
183
|
+
)
|
|
184
|
+
for (const hole of holes) {
|
|
185
|
+
PcbScene3dCopperFillMeshBuilder.#appendLoopWalls(
|
|
186
|
+
positions,
|
|
187
|
+
hole,
|
|
188
|
+
bottomZ,
|
|
189
|
+
topZ
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Normalizes one loop into local copper-side coordinates.
|
|
196
|
+
* @param {number[][]} loop Source loop.
|
|
197
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
198
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
199
|
+
* @returns {number[][]}
|
|
200
|
+
*/
|
|
201
|
+
static #normalizeLoop(loop, normalizeBoardPoint, mirrorY) {
|
|
202
|
+
const points = []
|
|
203
|
+
for (const point of loop || []) {
|
|
204
|
+
const normalized = normalizeBoardPoint(
|
|
205
|
+
Number(point?.[0]),
|
|
206
|
+
Number(point?.[1])
|
|
207
|
+
)
|
|
208
|
+
const nextPoint = [
|
|
209
|
+
Number(normalized?.x),
|
|
210
|
+
mirrorY ? -Number(normalized?.y) : Number(normalized?.y)
|
|
211
|
+
]
|
|
212
|
+
if (nextPoint.every(Number.isFinite)) {
|
|
213
|
+
points.push(nextPoint)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return PcbScene3dCopperFillMeshBuilder.#cleanLoop(points)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Flattens loops for triangulation.
|
|
221
|
+
* @param {number[][]} outer Outer loop.
|
|
222
|
+
* @param {number[][][]} holes Hole loops.
|
|
223
|
+
* @returns {{ points: number[][], flat: number[], holeIndexes: number[] }}
|
|
224
|
+
*/
|
|
225
|
+
static #flattenLoops(outer, holes) {
|
|
226
|
+
const points = []
|
|
227
|
+
const flat = []
|
|
228
|
+
const holeIndexes = []
|
|
229
|
+
|
|
230
|
+
for (const loop of [outer, ...holes]) {
|
|
231
|
+
if (points.length) {
|
|
232
|
+
holeIndexes.push(points.length)
|
|
233
|
+
}
|
|
234
|
+
for (const point of loop) {
|
|
235
|
+
points.push(point)
|
|
236
|
+
flat.push(point[0], point[1])
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { points, flat, holeIndexes }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Appends top or bottom triangulated faces.
|
|
245
|
+
* @param {number[]} positions Position buffer.
|
|
246
|
+
* @param {number[][]} points Flattened points.
|
|
247
|
+
* @param {number[]} triangles Earcut triangle indexes.
|
|
248
|
+
* @param {number} z Face Z.
|
|
249
|
+
* @param {boolean} reverse Whether to reverse winding.
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
static #appendSurfaceTriangles(positions, points, triangles, z, reverse) {
|
|
253
|
+
for (let index = 0; index + 2 < triangles.length; index += 3) {
|
|
254
|
+
const face = [
|
|
255
|
+
points[triangles[index]],
|
|
256
|
+
points[triangles[index + 1]],
|
|
257
|
+
points[triangles[index + 2]]
|
|
258
|
+
]
|
|
259
|
+
if (reverse) face.reverse()
|
|
260
|
+
PcbScene3dCopperFillMeshBuilder.#appendTriangle(positions, face, z)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Appends vertical side walls around one loop.
|
|
266
|
+
* @param {number[]} positions Position buffer.
|
|
267
|
+
* @param {number[][]} loop Loop points.
|
|
268
|
+
* @param {number} bottomZ Lower Z.
|
|
269
|
+
* @param {number} topZ Upper Z.
|
|
270
|
+
* @returns {void}
|
|
271
|
+
*/
|
|
272
|
+
static #appendLoopWalls(positions, loop, bottomZ, topZ) {
|
|
273
|
+
for (let index = 0; index < loop.length; index += 1) {
|
|
274
|
+
const current = loop[index]
|
|
275
|
+
const next = loop[(index + 1) % loop.length]
|
|
276
|
+
|
|
277
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(
|
|
278
|
+
positions,
|
|
279
|
+
current,
|
|
280
|
+
bottomZ
|
|
281
|
+
)
|
|
282
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(
|
|
283
|
+
positions,
|
|
284
|
+
next,
|
|
285
|
+
bottomZ
|
|
286
|
+
)
|
|
287
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(positions, next, topZ)
|
|
288
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(
|
|
289
|
+
positions,
|
|
290
|
+
current,
|
|
291
|
+
bottomZ
|
|
292
|
+
)
|
|
293
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(positions, next, topZ)
|
|
294
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(
|
|
295
|
+
positions,
|
|
296
|
+
current,
|
|
297
|
+
topZ
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Appends one triangle.
|
|
304
|
+
* @param {number[]} positions Position buffer.
|
|
305
|
+
* @param {number[][]} face Triangle points.
|
|
306
|
+
* @param {number} z Face Z.
|
|
307
|
+
* @returns {void}
|
|
308
|
+
*/
|
|
309
|
+
static #appendTriangle(positions, face, z) {
|
|
310
|
+
for (const point of face) {
|
|
311
|
+
PcbScene3dCopperFillMeshBuilder.#appendVertex(positions, point, z)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Appends one 3D vertex.
|
|
317
|
+
* @param {number[]} positions Position buffer.
|
|
318
|
+
* @param {number[]} point XY point.
|
|
319
|
+
* @param {number} z Z value.
|
|
320
|
+
* @returns {void}
|
|
321
|
+
*/
|
|
322
|
+
static #appendVertex(positions, point, z) {
|
|
323
|
+
positions.push(point[0], point[1], z)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Removes invalid and duplicate loop points.
|
|
328
|
+
* @param {number[][]} points Candidate points.
|
|
329
|
+
* @returns {number[][]}
|
|
330
|
+
*/
|
|
331
|
+
static #cleanLoop(points) {
|
|
332
|
+
const loop = []
|
|
333
|
+
for (const point of points || []) {
|
|
334
|
+
const x = Number(point?.[0])
|
|
335
|
+
const y = Number(point?.[1])
|
|
336
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const previous = loop[loop.length - 1]
|
|
341
|
+
if (
|
|
342
|
+
previous &&
|
|
343
|
+
Math.abs(previous[0] - x) < 0.001 &&
|
|
344
|
+
Math.abs(previous[1] - y) < 0.001
|
|
345
|
+
) {
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
loop.push([x, y])
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const first = loop[0]
|
|
352
|
+
const last = loop[loop.length - 1]
|
|
353
|
+
if (
|
|
354
|
+
first &&
|
|
355
|
+
last &&
|
|
356
|
+
Math.abs(first[0] - last[0]) < 0.001 &&
|
|
357
|
+
Math.abs(first[1] - last[1]) < 0.001
|
|
358
|
+
) {
|
|
359
|
+
loop.pop()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return loop
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Checks whether one loop has enough non-collinear area.
|
|
367
|
+
* @param {number[][]} loop Candidate loop.
|
|
368
|
+
* @returns {boolean}
|
|
369
|
+
*/
|
|
370
|
+
static #isValidLoop(loop) {
|
|
371
|
+
return (
|
|
372
|
+
Array.isArray(loop) &&
|
|
373
|
+
loop.length >= 3 &&
|
|
374
|
+
Math.abs(PcbScene3dCopperFillMeshBuilder.#signedArea(loop)) >
|
|
375
|
+
PcbScene3dCopperFillMeshBuilder.#AREA_EPSILON
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Computes signed loop area.
|
|
381
|
+
* @param {number[][]} loop Candidate loop.
|
|
382
|
+
* @returns {number}
|
|
383
|
+
*/
|
|
384
|
+
static #signedArea(loop) {
|
|
385
|
+
let area = 0
|
|
386
|
+
for (let index = 0; index < loop.length; index += 1) {
|
|
387
|
+
const current = loop[index]
|
|
388
|
+
const next = loop[(index + 1) % loop.length]
|
|
389
|
+
area += current[0] * next[1] - next[0] * current[1]
|
|
390
|
+
}
|
|
391
|
+
return area / 2
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -29,10 +29,22 @@ export class PcbScene3dCopperLayerFilter {
|
|
|
29
29
|
)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Filters one filled copper list to one outer copper face.
|
|
34
|
+
* @param {any[] | undefined} fills Filled copper primitives.
|
|
35
|
+
* @param {'top' | 'bottom'} side Board side.
|
|
36
|
+
* @returns {any[]}
|
|
37
|
+
*/
|
|
38
|
+
static fills(fills, side) {
|
|
39
|
+
return (fills || []).filter((fill) =>
|
|
40
|
+
PcbScene3dCopperLayerFilter.#matchesCopperLayer(fill, side)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/**
|
|
33
45
|
* Returns true when one primitive belongs to the requested outer copper
|
|
34
46
|
* face.
|
|
35
|
-
* @param {{ layerId?: number, layerCode?: number }} primitive Primitive.
|
|
47
|
+
* @param {{ layerId?: number, layerCode?: number, layer?: string, layerName?: string, side?: string, layerSide?: string, mountSide?: string }} primitive Primitive.
|
|
36
48
|
* @param {'top' | 'bottom'} side Board side.
|
|
37
49
|
* @returns {boolean}
|
|
38
50
|
*/
|
|
@@ -40,9 +52,26 @@ export class PcbScene3dCopperLayerFilter {
|
|
|
40
52
|
const layerId = Number(
|
|
41
53
|
primitive?.layerId ?? primitive?.layerCode ?? NaN
|
|
42
54
|
)
|
|
55
|
+
if (Number.isFinite(layerId)) {
|
|
56
|
+
return side === 'bottom'
|
|
57
|
+
? layerId ===
|
|
58
|
+
PcbScene3dCopperLayerFilter.#BOTTOM_COPPER_LAYER_ID
|
|
59
|
+
: layerId === PcbScene3dCopperLayerFilter.#TOP_COPPER_LAYER_ID
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const layerName = String(
|
|
63
|
+
primitive?.layer ||
|
|
64
|
+
primitive?.layerName ||
|
|
65
|
+
primitive?.side ||
|
|
66
|
+
primitive?.layerSide ||
|
|
67
|
+
primitive?.mountSide ||
|
|
68
|
+
''
|
|
69
|
+
)
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase()
|
|
43
72
|
|
|
44
73
|
return side === 'bottom'
|
|
45
|
-
?
|
|
46
|
-
:
|
|
74
|
+
? ['b.cu', 'bottom', 'back'].includes(layerName)
|
|
75
|
+
: ['f.cu', 'top', 'front'].includes(layerName)
|
|
47
76
|
}
|
|
48
77
|
}
|
|
@@ -92,7 +92,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
92
92
|
: PcbScene3dCutoutGeometryFilter.#DEFAULT_MAX_EDGE_LENGTH,
|
|
93
93
|
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
94
94
|
)
|
|
95
|
-
|
|
96
95
|
return {
|
|
97
96
|
maxDepth: Math.max(
|
|
98
97
|
Number.isFinite(Number(options?.maxDepth))
|
|
@@ -116,7 +115,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
116
115
|
.map((cutout) => {
|
|
117
116
|
const circularCutout =
|
|
118
117
|
PcbScene3dCutoutCircleDetector.resolve(cutout)
|
|
119
|
-
|
|
120
118
|
return {
|
|
121
119
|
points: cutout,
|
|
122
120
|
segments:
|
|
@@ -137,13 +135,11 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
137
135
|
*/
|
|
138
136
|
static #buildCutoutSegments(points) {
|
|
139
137
|
const segments = []
|
|
140
|
-
|
|
141
138
|
for (let index = 0; index < points.length; index += 1) {
|
|
142
139
|
const start = points[index]
|
|
143
140
|
const end = points[(index + 1) % points.length]
|
|
144
141
|
const dx = end.x - start.x
|
|
145
142
|
const dy = end.y - start.y
|
|
146
|
-
|
|
147
143
|
segments.push({
|
|
148
144
|
start,
|
|
149
145
|
end,
|
|
@@ -158,7 +154,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
158
154
|
}
|
|
159
155
|
})
|
|
160
156
|
}
|
|
161
|
-
|
|
162
157
|
return segments
|
|
163
158
|
}
|
|
164
159
|
/**
|
|
@@ -204,12 +199,28 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
204
199
|
overlappingCutouts.push(cutout)
|
|
205
200
|
}
|
|
206
201
|
}
|
|
207
|
-
|
|
208
202
|
if (!overlappingCutouts.length) {
|
|
209
203
|
PcbScene3dCutoutGeometryFilter.#appendTriangle(positions, triangle)
|
|
210
204
|
return
|
|
211
205
|
}
|
|
212
206
|
state.changed = true
|
|
207
|
+
if (
|
|
208
|
+
overlappingCutouts.some(
|
|
209
|
+
(cutout) =>
|
|
210
|
+
triangle.every((point) =>
|
|
211
|
+
PcbScene3dCutoutGeometryFilter.#isPointInsideOrOnCutout(
|
|
212
|
+
point,
|
|
213
|
+
cutout
|
|
214
|
+
)
|
|
215
|
+
) &&
|
|
216
|
+
!PcbScene3dCutoutGeometryFilter.#hasIntersectingEdges(
|
|
217
|
+
triangle,
|
|
218
|
+
cutout
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
213
224
|
if (
|
|
214
225
|
depth >= settings.maxDepth ||
|
|
215
226
|
PcbScene3dCutoutGeometryFilter.#maxEdgeLengthSquared(triangle) <=
|
|
@@ -250,7 +261,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
250
261
|
PcbScene3dCutoutGeometryFilter.#resolveSpatialCellSize(cutouts)
|
|
251
262
|
const cells = new Map()
|
|
252
263
|
const overflowIndexes = []
|
|
253
|
-
|
|
254
264
|
cutouts.forEach((cutout, index) => {
|
|
255
265
|
const range = PcbScene3dCutoutGeometryFilter.#resolveCellRange(
|
|
256
266
|
cutout.bounds,
|
|
@@ -258,7 +268,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
258
268
|
)
|
|
259
269
|
const cellCount =
|
|
260
270
|
(range.maxX - range.minX + 1) * (range.maxY - range.minY + 1)
|
|
261
|
-
|
|
262
271
|
if (
|
|
263
272
|
cellCount >
|
|
264
273
|
PcbScene3dCutoutGeometryFilter
|
|
@@ -267,7 +276,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
267
276
|
overflowIndexes.push(index)
|
|
268
277
|
return
|
|
269
278
|
}
|
|
270
|
-
|
|
271
279
|
for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
|
|
272
280
|
for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
|
|
273
281
|
const key = PcbScene3dCutoutGeometryFilter.#cellKey(
|
|
@@ -275,7 +283,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
275
283
|
cellY
|
|
276
284
|
)
|
|
277
285
|
const bucket = cells.get(key)
|
|
278
|
-
|
|
279
286
|
if (bucket) {
|
|
280
287
|
bucket.push(index)
|
|
281
288
|
} else {
|
|
@@ -284,7 +291,6 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
284
291
|
}
|
|
285
292
|
}
|
|
286
293
|
})
|
|
287
|
-
|
|
288
294
|
return {
|
|
289
295
|
cutouts,
|
|
290
296
|
cellSize,
|
|
@@ -307,19 +313,16 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
307
313
|
bounds,
|
|
308
314
|
cutoutIndex.cellSize
|
|
309
315
|
)
|
|
310
|
-
|
|
311
316
|
cutoutIndex.mark += 1
|
|
312
317
|
if (cutoutIndex.mark >= 0xffffffff) {
|
|
313
318
|
cutoutIndex.marks.fill(0)
|
|
314
319
|
cutoutIndex.mark = 1
|
|
315
320
|
}
|
|
316
|
-
|
|
317
321
|
for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
|
|
318
322
|
for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
|
|
319
323
|
const bucket = cutoutIndex.cells.get(
|
|
320
324
|
PcbScene3dCutoutGeometryFilter.#cellKey(cellX, cellY)
|
|
321
325
|
)
|
|
322
|
-
|
|
323
326
|
if (bucket) {
|
|
324
327
|
for (const index of bucket) {
|
|
325
328
|
PcbScene3dCutoutGeometryFilter.#appendCutoutCandidate(
|