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.
Files changed (55) hide show
  1. package/README.md +3 -2
  2. package/docs/api.md +27 -1
  3. package/docs/circuitjson.md +63 -18
  4. package/docs/model-format.md +13 -2
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +2 -2
  7. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
  8. package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
  9. package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
  10. package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
  12. package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
  13. package/src/PcbAssemblyGltfValidator.mjs +460 -0
  14. package/src/PcbAssemblyGltfWriter.mjs +801 -0
  15. package/src/PcbAssemblyMeshUtils.mjs +117 -0
  16. package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
  17. package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
  18. package/src/PcbAssemblyStepWriter.mjs +24 -2
  19. package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
  20. package/src/PcbModelArchiveExporter.mjs +521 -7
  21. package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
  22. package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
  23. package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
  24. package/src/PcbScene3dComponentVisibility.mjs +100 -5
  25. package/src/PcbScene3dController.mjs +25 -55
  26. package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
  27. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
  28. package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
  29. package/src/PcbScene3dCopperFactory.mjs +99 -85
  30. package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
  31. package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
  33. package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
  34. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
  35. package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
  36. package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
  37. package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
  38. package/src/PcbScene3dExternalModels.mjs +16 -7
  39. package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
  40. package/src/PcbScene3dFallbackVisibility.mjs +58 -1
  41. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
  42. package/src/PcbScene3dPadFactory.mjs +35 -2
  43. package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
  44. package/src/PcbScene3dRuntime.mjs +94 -100
  45. package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
  46. package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
  47. package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
  48. package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
  49. package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
  50. package/src/PcbScene3dSelectionStyler.mjs +52 -2
  51. package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
  52. package/src/PcbScene3dText.mjs +1 -0
  53. package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
  54. package/src/PcbScene3dViaFactory.mjs +27 -7
  55. 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
- ? layerId === PcbScene3dCopperLayerFilter.#BOTTOM_COPPER_LAYER_ID
46
- : layerId === PcbScene3dCopperLayerFilter.#TOP_COPPER_LAYER_ID
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(