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.
Files changed (63) hide show
  1. package/README.md +46 -21
  2. package/docs/api.md +109 -8
  3. package/docs/circuitjson.md +143 -29
  4. package/docs/model-format.md +50 -7
  5. package/docs/release-notes-v1.2.0.md +122 -0
  6. package/docs/testing.md +30 -1
  7. package/package.json +6 -3
  8. package/spec/library-scope.md +10 -2
  9. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  10. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  12. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  13. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  14. package/src/PcbModelArchiveExporter.mjs +28 -112
  15. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  16. package/src/PcbScene3dAabbIndex.mjs +464 -0
  17. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
  18. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  19. package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
  23. package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
  24. package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
  25. package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
  26. package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
  27. package/src/PcbScene3dController.mjs +40 -34
  28. package/src/PcbScene3dCopperFactory.mjs +36 -22
  29. package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
  30. package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
  31. package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
  32. package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
  33. package/src/PcbScene3dCopperTextFactory.mjs +12 -4
  34. package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
  35. package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
  36. package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
  37. package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
  38. package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
  39. package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
  40. package/src/PcbScene3dDrillPathFactory.mjs +86 -16
  41. package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
  46. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
  47. package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
  48. package/src/PcbScene3dModelContent.mjs +236 -0
  49. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  50. package/src/PcbScene3dModelIdentity.mjs +106 -0
  51. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  52. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  53. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  54. package/src/PcbScene3dRuntime.mjs +45 -45
  55. package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
  56. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  57. package/src/PcbScene3dShellRenderer.mjs +75 -6
  58. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  59. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  60. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  61. package/src/PcbScene3dStepLoader.mjs +11 -10
  62. package/src/PcbScene3dText.mjs +1 -1
  63. package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
@@ -1,21 +1,22 @@
1
1
  import { PcbScene3dCircularCutoutOverlap } from './PcbScene3dCircularCutoutOverlap.mjs'
2
2
  import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
3
+ import { PcbScene3dCutoutGridIndex } from './PcbScene3dCutoutGridIndex.mjs'
3
4
  import { PcbScene3dGeometryBoundsResolver } from './PcbScene3dGeometryBoundsResolver.mjs'
5
+ import { PcbScene3dPreparedPolygon } from './PcbScene3dPreparedPolygon.mjs'
4
6
  import { PcbScene3dTerminalCutoutClassifier } from './PcbScene3dTerminalCutoutClassifier.mjs'
7
+ import { PcbScene3dTriangleVertexQueryBounds } from './PcbScene3dTriangleVertexQueryBounds.mjs'
5
8
 
6
9
  /** Clips filled 2D geometry against drill-cutout polygons. */
7
10
  export class PcbScene3dCutoutGeometryFilter {
8
11
  static #GEOMETRY_EPSILON = 0.001
9
12
  static #DEFAULT_MAX_DEPTH = 9
10
13
  static #DEFAULT_MAX_EDGE_LENGTH = 4
11
- static #SPATIAL_INDEX_MIN_CELL_SIZE = 8
12
- static #SPATIAL_INDEX_MAX_CELLS_PER_CUTOUT = 128
13
14
  /**
14
15
  * Removes triangles that still overlap cutouts after triangulation.
15
16
  * @param {any} THREE
16
17
  * @param {any} geometry
17
18
  * @param {{ x: number, y: number }[][]} cutouts
18
- * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean }} [options]
19
+ * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map }} [options]
19
20
  * @returns {any}
20
21
  */
21
22
  static filter(THREE, geometry, cutouts, options = {}) {
@@ -35,8 +36,10 @@ export class PcbScene3dCutoutGeometryFilter {
35
36
  if (!position?.count) {
36
37
  return geometry
37
38
  }
38
- const preparedCutouts =
39
- PcbScene3dCutoutGeometryFilter.#prepareCutouts(cutouts)
39
+ const preparedCutouts = PcbScene3dCutoutGeometryFilter.#prepareCutouts(
40
+ cutouts,
41
+ PcbScene3dCutoutGeometryFilter.#resolvePreparedPolygonCache(options)
42
+ )
40
43
  if (
41
44
  PcbScene3dGeometryBoundsResolver.missesAllPositionBounds(
42
45
  position,
@@ -45,10 +48,7 @@ export class PcbScene3dCutoutGeometryFilter {
45
48
  )
46
49
  )
47
50
  return geometry
48
- const cutoutIndex =
49
- PcbScene3dCutoutGeometryFilter.#buildCutoutSpatialIndex(
50
- preparedCutouts
51
- )
51
+ const cutoutIndex = new PcbScene3dCutoutGridIndex(preparedCutouts)
52
52
  const settings =
53
53
  PcbScene3dCutoutGeometryFilter.#resolveSettings(options)
54
54
  const positions = []
@@ -82,7 +82,7 @@ export class PcbScene3dCutoutGeometryFilter {
82
82
  }
83
83
  /**
84
84
  * Resolves clipping settings.
85
- * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean }} options
85
+ * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map }} options
86
86
  * @returns {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number, discardTerminalOverlaps: boolean }}
87
87
  */
88
88
  static #resolveSettings(options) {
@@ -105,66 +105,64 @@ export class PcbScene3dCutoutGeometryFilter {
105
105
  }
106
106
  }
107
107
  /**
108
- * Prepares cutout polygons with bounds for fast overlap checks.
109
- * @param {{ x: number, y: number }[][]} cutouts
110
- * @returns {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }[]}
108
+ * Resolves a supported request-scoped prepared polygon cache.
109
+ * @param {{ preparedPolygonCache?: Map }} options Request options.
110
+ * @returns {Map | null}
111
111
  */
112
- static #prepareCutouts(cutouts) {
113
- return cutouts
114
- .filter((cutout) => Array.isArray(cutout) && cutout.length >= 3)
115
- .map((cutout) => {
116
- const circularCutout =
117
- PcbScene3dCutoutCircleDetector.resolve(cutout)
118
- return {
119
- points: cutout,
120
- segments:
121
- PcbScene3dCutoutGeometryFilter.#buildCutoutSegments(
122
- cutout
123
- ),
124
- bounds: PcbScene3dCutoutGeometryFilter.#resolveBounds(
125
- cutout
126
- ),
127
- ...circularCutout
128
- }
129
- })
112
+ static #resolvePreparedPolygonCache(options) {
113
+ return options?.preparedPolygonCache instanceof Map
114
+ ? options.preparedPolygonCache
115
+ : null
130
116
  }
117
+
131
118
  /**
132
- * Builds reusable segment metadata for one cutout polygon.
133
- * @param {{ x: number, y: number }[]} points
134
- * @returns {{ start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]}
119
+ * Prepares valid cutout polygons for exact indexed queries.
120
+ * @param {{ x: number, y: number }[][]} cutouts
121
+ * @param {Map | null} preparedPolygonCache Request-scoped prepared cache.
122
+ * @returns {PcbScene3dPreparedPolygon[]}
135
123
  */
136
- static #buildCutoutSegments(points) {
137
- const segments = []
138
- for (let index = 0; index < points.length; index += 1) {
139
- const start = points[index]
140
- const end = points[(index + 1) % points.length]
141
- const dx = end.x - start.x
142
- const dy = end.y - start.y
143
- segments.push({
144
- start,
145
- end,
146
- dx,
147
- dy,
148
- lengthSquared: dx * dx + dy * dy,
149
- bounds: {
150
- minX: Math.min(start.x, end.x),
151
- maxX: Math.max(start.x, end.x),
152
- minY: Math.min(start.y, end.y),
153
- maxY: Math.max(start.y, end.y)
124
+ static #prepareCutouts(cutouts, preparedPolygonCache) {
125
+ return cutouts
126
+ .filter((cutout) => Array.isArray(cutout) && cutout.length >= 3)
127
+ .map((cutout, sourceIndex) => {
128
+ let prepared = preparedPolygonCache?.has(cutout)
129
+ ? preparedPolygonCache.get(cutout)
130
+ : null
131
+
132
+ if (
133
+ prepared?.circleDetectionEnabled !== true ||
134
+ !['raw', 'raw-numeric'].includes(
135
+ prepared?.pointRepresentation
136
+ )
137
+ ) {
138
+ const metadataPoints = cutout.map((point) => ({
139
+ x: Number(point?.x || 0),
140
+ y: Number(point?.y || 0)
141
+ }))
142
+ prepared = new PcbScene3dPreparedPolygon(cutout, {
143
+ source: cutout,
144
+ sourceIndex,
145
+ epsilon:
146
+ PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON,
147
+ detectCircle: true,
148
+ metadataPoints,
149
+ pointRepresentation: 'raw'
150
+ })
151
+ preparedPolygonCache?.set(cutout, prepared)
154
152
  }
153
+
154
+ return prepared
155
155
  })
156
- }
157
- return segments
158
156
  }
159
157
  /**
160
158
  * Appends a triangle, subdividing near cutouts before removal.
161
159
  * @param {number[]} positions
162
160
  * @param {{ x: number, y: number, z: number }[]} triangle
163
- * @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} cutouts
161
+ * @param {PcbScene3dPreparedPolygon[]} cutouts
164
162
  * @param {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number, discardTerminalOverlaps: boolean }} settings
165
163
  * @param {number} depth
166
164
  * @param {{ changed: boolean }} state
167
- * @param {object | null} [cutoutIndex]
165
+ * @param {PcbScene3dCutoutGridIndex | null} [cutoutIndex]
168
166
  * @returns {void}
169
167
  */
170
168
  static #appendFilteredTriangle(
@@ -179,10 +177,7 @@ export class PcbScene3dCutoutGeometryFilter {
179
177
  const triangleBounds =
180
178
  PcbScene3dCutoutGeometryFilter.#resolveBounds(triangle)
181
179
  const candidateCutouts = cutoutIndex
182
- ? PcbScene3dCutoutGeometryFilter.#collectCandidateCutouts(
183
- triangleBounds,
184
- cutoutIndex
185
- )
180
+ ? cutoutIndex.query(triangleBounds)
186
181
  : cutouts
187
182
  const overlappingCutouts = []
188
183
  for (const cutout of candidateCutouts) {
@@ -193,7 +188,8 @@ export class PcbScene3dCutoutGeometryFilter {
193
188
  ) &&
194
189
  PcbScene3dCutoutGeometryFilter.#doesTriangleOverlapCutout(
195
190
  triangle,
196
- cutout
191
+ cutout,
192
+ triangleBounds
197
193
  )
198
194
  ) {
199
195
  overlappingCutouts.push(cutout)
@@ -251,166 +247,6 @@ export class PcbScene3dCutoutGeometryFilter {
251
247
  )
252
248
  }
253
249
 
254
- /**
255
- * Builds a spatial index for prepared cutout bounds.
256
- * @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} cutouts
257
- * @returns {{ cutouts: object[], cellSize: number, cells: Map<string, number[]>, overflowIndexes: number[], marks: Uint32Array, mark: number }}
258
- */
259
- static #buildCutoutSpatialIndex(cutouts) {
260
- const cellSize =
261
- PcbScene3dCutoutGeometryFilter.#resolveSpatialCellSize(cutouts)
262
- const cells = new Map()
263
- const overflowIndexes = []
264
- cutouts.forEach((cutout, index) => {
265
- const range = PcbScene3dCutoutGeometryFilter.#resolveCellRange(
266
- cutout.bounds,
267
- cellSize
268
- )
269
- const cellCount =
270
- (range.maxX - range.minX + 1) * (range.maxY - range.minY + 1)
271
- if (
272
- cellCount >
273
- PcbScene3dCutoutGeometryFilter
274
- .#SPATIAL_INDEX_MAX_CELLS_PER_CUTOUT
275
- ) {
276
- overflowIndexes.push(index)
277
- return
278
- }
279
- for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
280
- for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
281
- const key = PcbScene3dCutoutGeometryFilter.#cellKey(
282
- cellX,
283
- cellY
284
- )
285
- const bucket = cells.get(key)
286
- if (bucket) {
287
- bucket.push(index)
288
- } else {
289
- cells.set(key, [index])
290
- }
291
- }
292
- }
293
- })
294
- return {
295
- cutouts,
296
- cellSize,
297
- cells,
298
- overflowIndexes,
299
- marks: new Uint32Array(cutouts.length),
300
- mark: 0
301
- }
302
- }
303
-
304
- /**
305
- * Collects cutouts whose spatial buckets overlap one triangle bounds box.
306
- * @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
307
- * @param {{ cutouts: object[], cellSize: number, cells: Map<string, number[]>, overflowIndexes: number[], marks: Uint32Array, mark: number }} cutoutIndex
308
- * @returns {object[]}
309
- */
310
- static #collectCandidateCutouts(bounds, cutoutIndex) {
311
- const candidates = []
312
- const range = PcbScene3dCutoutGeometryFilter.#resolveCellRange(
313
- bounds,
314
- cutoutIndex.cellSize
315
- )
316
- cutoutIndex.mark += 1
317
- if (cutoutIndex.mark >= 0xffffffff) {
318
- cutoutIndex.marks.fill(0)
319
- cutoutIndex.mark = 1
320
- }
321
- for (let cellX = range.minX; cellX <= range.maxX; cellX += 1) {
322
- for (let cellY = range.minY; cellY <= range.maxY; cellY += 1) {
323
- const bucket = cutoutIndex.cells.get(
324
- PcbScene3dCutoutGeometryFilter.#cellKey(cellX, cellY)
325
- )
326
- if (bucket) {
327
- for (const index of bucket) {
328
- PcbScene3dCutoutGeometryFilter.#appendCutoutCandidate(
329
- candidates,
330
- cutoutIndex,
331
- index
332
- )
333
- }
334
- }
335
- }
336
- }
337
-
338
- for (const index of cutoutIndex.overflowIndexes) {
339
- PcbScene3dCutoutGeometryFilter.#appendCutoutCandidate(
340
- candidates,
341
- cutoutIndex,
342
- index
343
- )
344
- }
345
- return candidates
346
- }
347
-
348
- /**
349
- * Appends one unique spatial-index cutout candidate.
350
- * @param {object[]} candidates
351
- * @param {{ cutouts: object[], marks: Uint32Array, mark: number }} cutoutIndex
352
- * @param {number} index
353
- * @returns {void}
354
- */
355
- static #appendCutoutCandidate(candidates, cutoutIndex, index) {
356
- if (cutoutIndex.marks[index] === cutoutIndex.mark) {
357
- return
358
- }
359
-
360
- cutoutIndex.marks[index] = cutoutIndex.mark
361
- candidates.push(cutoutIndex.cutouts[index])
362
- }
363
-
364
- /**
365
- * Resolves a spatial index cell size from typical cutout spans.
366
- * @param {{ bounds: { minX: number, maxX: number, minY: number, maxY: number } }[]} cutouts
367
- * @returns {number}
368
- */
369
- static #resolveSpatialCellSize(cutouts) {
370
- const spans = cutouts
371
- .map((cutout) =>
372
- Math.max(
373
- Number(cutout.bounds.maxX) - Number(cutout.bounds.minX),
374
- Number(cutout.bounds.maxY) - Number(cutout.bounds.minY),
375
- 0
376
- )
377
- )
378
- .filter((span) => Number.isFinite(span))
379
- .sort((left, right) => left - right)
380
- const medianSpan = spans[Math.floor(spans.length / 2)] || 0
381
-
382
- return Math.max(
383
- medianSpan * 4,
384
- PcbScene3dCutoutGeometryFilter.#DEFAULT_MAX_EDGE_LENGTH * 2,
385
- PcbScene3dCutoutGeometryFilter.#SPATIAL_INDEX_MIN_CELL_SIZE
386
- )
387
- }
388
-
389
- /**
390
- * Resolves the inclusive spatial cell range for a bounds box.
391
- * @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
392
- * @param {number} cellSize
393
- * @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
394
- */
395
- static #resolveCellRange(bounds, cellSize) {
396
- return {
397
- minX: Math.floor(Number(bounds.minX) / cellSize),
398
- maxX: Math.floor(Number(bounds.maxX) / cellSize),
399
- minY: Math.floor(Number(bounds.minY) / cellSize),
400
- maxY: Math.floor(Number(bounds.maxY) / cellSize)
401
- }
402
- }
403
-
404
- /**
405
- * Builds one deterministic spatial index key.
406
- * @param {number} cellX
407
- * @param {number} cellY
408
- * @returns {string}
409
- */
410
- static #cellKey(cellX, cellY) {
411
- return `${cellX}:${cellY}`
412
- }
413
-
414
250
  /**
415
251
  * Appends one triangle to the flattened position buffer.
416
252
  * @param {number[]} positions
@@ -572,10 +408,11 @@ export class PcbScene3dCutoutGeometryFilter {
572
408
  /**
573
409
  * Returns true when one triangle intersects or covers a cutout.
574
410
  * @param {{ x: number, y: number }[]} triangle
575
- * @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
411
+ * @param {PcbScene3dPreparedPolygon} cutout
412
+ * @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds
576
413
  * @returns {boolean}
577
414
  */
578
- static #doesTriangleOverlapCutout(triangle, cutout) {
415
+ static #doesTriangleOverlapCutout(triangle, cutout, triangleBounds) {
579
416
  if (
580
417
  !Array.isArray(triangle) ||
581
418
  triangle.length !== 3 ||
@@ -604,7 +441,13 @@ export class PcbScene3dCutoutGeometryFilter {
604
441
  }
605
442
  }
606
443
 
607
- for (const point of cutout.points) {
444
+ const vertexCandidates =
445
+ PcbScene3dCutoutGeometryFilter.#queryCutoutVertices(
446
+ cutout,
447
+ triangle,
448
+ triangleBounds
449
+ )
450
+ for (const point of vertexCandidates) {
608
451
  if (
609
452
  PcbScene3dCutoutGeometryFilter.#isPointInsideOrOnTriangle(
610
453
  point,
@@ -621,10 +464,30 @@ export class PcbScene3dCutoutGeometryFilter {
621
464
  )
622
465
  }
623
466
 
467
+ /**
468
+ * Returns cutout vertices conservatively admitted by triangle tolerance.
469
+ * @param {PcbScene3dPreparedPolygon} cutout
470
+ * @param {{ x: number, y: number }[]} triangle
471
+ * @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds
472
+ * @returns {{ x: number, y: number }[]}
473
+ */
474
+ static #queryCutoutVertices(cutout, triangle, triangleBounds) {
475
+ const queryBounds = PcbScene3dTriangleVertexQueryBounds.resolve(
476
+ triangle,
477
+ triangleBounds,
478
+ cutout.bounds,
479
+ PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
480
+ )
481
+
482
+ return queryBounds
483
+ ? cutout.queryVertices(queryBounds, [])
484
+ : cutout.points
485
+ }
486
+
624
487
  /**
625
488
  * Returns true when a point is inside or on a cutout.
626
489
  * @param {{ x: number, y: number }} point
627
- * @param {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
490
+ * @param {PcbScene3dPreparedPolygon} cutout
628
491
  * @returns {boolean}
629
492
  */
630
493
  static #isPointInsideOrOnCutout(point, cutout) {
@@ -683,7 +546,7 @@ export class PcbScene3dCutoutGeometryFilter {
683
546
  /**
684
547
  * Returns true when a point lies inside a cutout and away from its border.
685
548
  * @param {{ x: number, y: number }} point
686
- * @param {{ points: { x: number, y: number }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
549
+ * @param {PcbScene3dPreparedPolygon} cutout
687
550
  * @returns {boolean}
688
551
  */
689
552
  static #isPointStrictlyInsideCutout(point, cutout) {
@@ -699,35 +562,16 @@ export class PcbScene3dCutoutGeometryFilter {
699
562
  )
700
563
  }
701
564
 
702
- const polygon = cutout.points
703
-
704
- let inside = false
705
- for (
706
- let index = 0, previousIndex = polygon.length - 1;
707
- index < polygon.length;
708
- previousIndex = index, index += 1
709
- ) {
710
- const current = polygon[index]
711
- const previous = polygon[previousIndex]
712
- const intersects =
713
- current.y > point.y !== previous.y > point.y &&
714
- point.x <
715
- ((previous.x - current.x) * (point.y - current.y)) /
716
- (previous.y - current.y) +
717
- current.x
718
-
719
- if (intersects) {
720
- inside = !inside
721
- }
722
- }
723
-
724
- return inside
565
+ return cutout.containsPointStrict(point, {
566
+ segmentBoundsEpsilon:
567
+ PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
568
+ })
725
569
  }
726
570
 
727
571
  /**
728
572
  * Returns true when a point lies on a cutout edge.
729
573
  * @param {{ x: number, y: number }} point
730
- * @param {{ segments: { start: { x: number, y: number }, end: { x: number, y: number }, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
574
+ * @param {PcbScene3dPreparedPolygon} cutout
731
575
  * @returns {boolean}
732
576
  */
733
577
  static #isPointOnCutoutBoundary(point, cutout) {
@@ -744,23 +588,10 @@ export class PcbScene3dCutoutGeometryFilter {
744
588
  )
745
589
  }
746
590
 
747
- for (const segment of cutout.segments) {
748
- if (
749
- PcbScene3dCutoutGeometryFilter.#pointOverlapsBounds(
750
- point,
751
- segment.bounds
752
- ) &&
753
- PcbScene3dCutoutGeometryFilter.#isPointOnSegment(
754
- point,
755
- segment.start,
756
- segment.end
757
- )
758
- ) {
759
- return true
760
- }
761
- }
762
-
763
- return false
591
+ return cutout.isPointOnBoundary(point, {
592
+ segmentBoundsEpsilon:
593
+ PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
594
+ })
764
595
  }
765
596
 
766
597
  /**
@@ -796,7 +627,7 @@ export class PcbScene3dCutoutGeometryFilter {
796
627
  /**
797
628
  * Returns true when any triangle and cutout edges intersect.
798
629
  * @param {{ x: number, y: number }[]} triangle
799
- * @param {{ segments: { start: { x: number, y: number }, end: { x: number, y: number }, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[] }} cutout
630
+ * @param {PcbScene3dPreparedPolygon} cutout
800
631
  * @returns {boolean}
801
632
  */
802
633
  static #hasIntersectingEdges(triangle, cutout) {
@@ -813,7 +644,11 @@ export class PcbScene3dCutoutGeometryFilter {
813
644
  triangleEnd
814
645
  )
815
646
 
816
- for (const segment of cutout.segments) {
647
+ const segmentCandidates = cutout.querySegments(
648
+ triangleSegmentBounds,
649
+ []
650
+ )
651
+ for (const segment of segmentCandidates) {
817
652
  if (
818
653
  PcbScene3dCutoutGeometryFilter.#boundsOverlap(
819
654
  triangleSegmentBounds,