pcb-scene3d-viewer 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/api.md CHANGED
@@ -365,3 +365,18 @@ The package also exports focused factories used by the runtime:
365
365
  These factories accept Three.js constructors and normalized scene-detail
366
366
  objects. They are exported for tests and advanced hosts, but most applications
367
367
  should use `PcbScene3dRuntime` or `PcbScene3dController`.
368
+
369
+ ### Copper occlusion geometry
370
+
371
+ Covered copper is clipped against vertical occlusion prisms at the contour
372
+ boundaries. Convex contours are clipped directly; concave contours are
373
+ triangulated into convex pieces. Intersection vertices interpolate all three
374
+ coordinates, preserving sloped surfaces and vertical relief walls without
375
+ recursive mesh subdivision. Sampled circular openings use tangent planes with
376
+ at least 32 segments (or the source contour resolution when greater), preventing
377
+ copper slivers inside the analytic circle. This keeps mesh growth tied to the
378
+ opening boundaries rather than board dimensions or a subdivision depth.
379
+
380
+ Repeated external-model package placement uses ownership indexes scoped to one
381
+ repair invocation. Input scenes remain mutable: subsequent applications rebuild
382
+ the indexes and observe edits to pad and component rows.
@@ -233,3 +233,8 @@ truthy field adds a solder-mask ring on that board surface while the plated
233
233
  through-hole barrel remains copper. A mixed via therefore renders one covered
234
234
  annulus and one exposed annulus; a via with both fields explicitly false stays
235
235
  on the exposed-copper path. The source toolkit owns this classification.
236
+
237
+ Filled `contours` may contain point loops or line/arc segment loops. Segment
238
+ records are recognized by their type or start endpoint before point conversion;
239
+ arc `x`/`y` fields describe centers, not vertices. Arc segments can provide an
240
+ explicit signed `sweepAngle` to preserve direction and sweeps across zero.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@sunbox/occt-import-js": "^0.0.28",
64
- "circuitjson-toolkit": "^1.4.1",
64
+ "circuitjson-toolkit": "^1.4.3",
65
65
  "earcut": "3.0.2",
66
66
  "fflate": "^0.8.2",
67
67
  "polygon-clipping": "^0.15.7",
@@ -378,6 +378,22 @@ export class PcbAssemblyFillGeometryResolver {
378
378
  return []
379
379
  }
380
380
 
381
+ // Segment x/y fields can denote arc centers. Do not reinterpret them
382
+ // as contour vertices before checking the segment's explicit shape.
383
+ if (
384
+ list.some(
385
+ (entry) =>
386
+ ['line', 'arc'].includes(
387
+ String(entry?.type || '').toLowerCase()
388
+ ) ||
389
+ (entry?.x1 != null && entry?.y1 != null) ||
390
+ (entry?.startX != null && entry?.startY != null) ||
391
+ entry?.start != null
392
+ )
393
+ ) {
394
+ return PcbAssemblyFillGeometryResolver.#segmentLoop(list)
395
+ }
396
+
381
397
  const pointLoop = PcbAssemblyFillGeometryResolver.#pointLoop(list)
382
398
  if (pointLoop.length >= 3) {
383
399
  return pointLoop
@@ -1,4 +1,4 @@
1
- import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
1
+ import { PcbScene3dCopperOcclusionGeometry } from './PcbScene3dCopperOcclusionGeometry.mjs'
2
2
 
3
3
  /**
4
4
  * Clips mask-covered copper relief where opaque overlay artwork covers it.
@@ -12,13 +12,12 @@ export class PcbScene3dCopperOcclusionClipper {
12
12
  * @returns {any | null}
13
13
  */
14
14
  static filter(THREE, geometry, cutouts) {
15
- geometry.computeVertexNormals?.()
16
- const clippedGeometry = PcbScene3dCutoutGeometryFilter.filter(
15
+ const clippedGeometry = PcbScene3dCopperOcclusionGeometry.filter(
17
16
  THREE,
18
17
  geometry,
19
- cutouts,
20
- { maxDepth: 12, maxEdgeLength: 2, discardTerminalOverlaps: true }
18
+ cutouts
21
19
  )
20
+ clippedGeometry.computeVertexNormals?.()
22
21
 
23
22
  return clippedGeometry.getAttribute?.('position')?.count
24
23
  ? clippedGeometry
@@ -0,0 +1,182 @@
1
+ import { PcbScene3dAabbIndex } from './PcbScene3dAabbIndex.mjs'
2
+ import { PcbScene3dCopperOcclusionPlanes } from './PcbScene3dCopperOcclusionPlanes.mjs'
3
+
4
+ /** Subtracts vertical occlusion prisms without recursive triangle subdivision. */
5
+ export class PcbScene3dCopperOcclusionGeometry {
6
+ /**
7
+ * Clips every triangle, interpolating XYZ on its original plane.
8
+ * @param {any} THREE Three.js namespace.
9
+ * @param {any} geometry Source triangle geometry.
10
+ * @param {object[][]} cutouts Normalized occlusion contours.
11
+ * @returns {any} Clipped or unchanged geometry.
12
+ */
13
+ static filter(THREE, geometry, cutouts) {
14
+ if (
15
+ !Array.isArray(cutouts) ||
16
+ !cutouts.length ||
17
+ !geometry?.getAttribute ||
18
+ !THREE.BufferGeometry ||
19
+ !THREE.Float32BufferAttribute
20
+ )
21
+ return geometry
22
+ const source = geometry.index ? geometry.toNonIndexed() : geometry
23
+ const position = source.getAttribute('position')
24
+ if (!position?.count) return geometry
25
+ const prisms = PcbScene3dCopperOcclusionPlanes.prepare(cutouts)
26
+ const index = new PcbScene3dAabbIndex(prisms)
27
+ const positions = []
28
+ let changed = false
29
+ for (let i = 0; i + 2 < position.count; i += 3) {
30
+ const triangle = [0, 1, 2].map((offset) => ({
31
+ x: position.getX(i + offset),
32
+ y: position.getY(i + offset),
33
+ z: position.getZ(i + offset)
34
+ }))
35
+ let polygons = [triangle]
36
+ for (const prism of index.query(this.#bounds(triangle))) {
37
+ const remaining = []
38
+ for (const polygon of polygons) {
39
+ const pieces = this.#subtract(polygon, prism)
40
+ if (pieces.length !== 1 || pieces[0] !== polygon)
41
+ changed = true
42
+ for (const piece of pieces) remaining.push(piece)
43
+ }
44
+ polygons = remaining
45
+ if (!polygons.length) break
46
+ }
47
+ for (const polygon of polygons) {
48
+ for (let vertex = 1; vertex + 1 < polygon.length; vertex += 1) {
49
+ const a = polygon[0],
50
+ b = polygon[vertex],
51
+ c = polygon[vertex + 1]
52
+ positions.push(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z)
53
+ }
54
+ }
55
+ }
56
+ if (source !== geometry) source.dispose?.()
57
+ if (!changed) return geometry
58
+ const result = new THREE.BufferGeometry()
59
+ result.setAttribute(
60
+ 'position',
61
+ new THREE.Float32BufferAttribute(positions, 3)
62
+ )
63
+ return result
64
+ }
65
+
66
+ /**
67
+ * Subtracts a convex prism, retaining convex outside pieces for triangulation.
68
+ * @param {object[]} polygon Convex polygon on a 3D plane.
69
+ * @param {object} prism Inward half planes and XY bounds.
70
+ * @returns {object[][]} Visible polygons.
71
+ */
72
+ static #subtract(polygon, prism) {
73
+ const bounds = this.#bounds(polygon)
74
+ if (
75
+ bounds.minX > prism.bounds.maxX ||
76
+ bounds.maxX < prism.bounds.minX ||
77
+ bounds.minY > prism.bounds.maxY ||
78
+ bounds.maxY < prism.bounds.minY
79
+ )
80
+ return [polygon]
81
+ let inside = polygon
82
+ const outside = []
83
+ for (const plane of prism.planes) {
84
+ const split = this.#split(inside, plane)
85
+ // No intersection means all tentative pieces together are the input.
86
+ if (split.inside.length < 3) return [polygon]
87
+ if (split.outside.length >= 3) outside.push(split.outside)
88
+ inside = split.inside
89
+ }
90
+ return this.#hasArea(inside) ? outside : [polygon]
91
+ }
92
+
93
+ /**
94
+ * Splits a planar polygon using an XY half plane, including vertical walls.
95
+ * @param {object[]} polygon Convex source polygon.
96
+ * @param {{ x: number, y: number, offset: number }} plane Inward half plane.
97
+ * @returns {{ inside: object[], outside: object[] }} Split polygon sides.
98
+ */
99
+ static #split(polygon, plane) {
100
+ const inside = [],
101
+ outside = []
102
+ let hasOutside = false
103
+ let previous = polygon[polygon.length - 1]
104
+ let previousDistance =
105
+ plane.x * previous.x + plane.y * previous.y - plane.offset
106
+ for (const point of polygon) {
107
+ const distance =
108
+ plane.x * point.x + plane.y * point.y - plane.offset
109
+ if (
110
+ (distance > 0 && previousDistance < 0) ||
111
+ (distance < 0 && previousDistance > 0)
112
+ ) {
113
+ const fraction =
114
+ previousDistance / (previousDistance - distance)
115
+ const crossing = {
116
+ x: previous.x + fraction * (point.x - previous.x),
117
+ y: previous.y + fraction * (point.y - previous.y),
118
+ z: previous.z + fraction * (point.z - previous.z)
119
+ }
120
+ inside.push(crossing)
121
+ outside.push(crossing)
122
+ }
123
+ if (distance >= 0) inside.push(point)
124
+ if (distance <= 0) outside.push(point)
125
+ if (distance < 0) hasOutside = true
126
+ previous = point
127
+ previousDistance = distance
128
+ }
129
+ return {
130
+ inside,
131
+ outside: hasOutside && this.#hasArea(outside) ? outside : []
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Rejects boundary-only intersections without dropping vertical surfaces.
137
+ * @param {object[]} polygon Convex planar polygon.
138
+ * @returns {boolean} Whether at least one triangle has nonzero 3D area.
139
+ */
140
+ static #hasArea(polygon) {
141
+ if (polygon.length < 3) return false
142
+ const a = polygon[0]
143
+ for (let i = 1; i + 1 < polygon.length; i += 1) {
144
+ const b = polygon[i],
145
+ c = polygon[i + 1]
146
+ const ux = b.x - a.x,
147
+ uy = b.y - a.y,
148
+ uz = b.z - a.z
149
+ const vx = c.x - a.x,
150
+ vy = c.y - a.y,
151
+ vz = c.z - a.z
152
+ if (
153
+ Math.hypot(
154
+ uy * vz - uz * vy,
155
+ uz * vx - ux * vz,
156
+ ux * vy - uy * vx
157
+ ) > 1e-10
158
+ )
159
+ return true
160
+ }
161
+ return false
162
+ }
163
+
164
+ /**
165
+ * Resolves XY bounds for a planar polygon.
166
+ * @param {object[]} points Polygon vertices.
167
+ * @returns {object} Axis-aligned bounds.
168
+ */
169
+ static #bounds(points) {
170
+ let minX = Infinity,
171
+ maxX = -Infinity,
172
+ minY = Infinity,
173
+ maxY = -Infinity
174
+ for (const { x, y } of points) {
175
+ minX = Math.min(minX, x)
176
+ maxX = Math.max(maxX, x)
177
+ minY = Math.min(minY, y)
178
+ maxY = Math.max(maxY, y)
179
+ }
180
+ return { minX, maxX, minY, maxY }
181
+ }
182
+ }
@@ -0,0 +1,145 @@
1
+ import earcut from 'earcut'
2
+ import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
3
+
4
+ /** Prepares convex vertical prisms for copper occlusion subtraction. */
5
+ export class PcbScene3dCopperOcclusionPlanes {
6
+ /**
7
+ * Decomposes simple concave contours while retaining convex contours whole.
8
+ * @param {{ x: number, y: number }[][]} cutouts Occlusion contours.
9
+ * @returns {object[]} Convex prisms with bounds and inward half planes.
10
+ */
11
+ static prepare(cutouts) {
12
+ const result = []
13
+ for (const cutout of cutouts) {
14
+ const points = []
15
+ for (const point of Array.isArray(cutout) ? cutout : []) {
16
+ const x = Number(point?.x || 0)
17
+ const y = Number(point?.y || 0)
18
+ const previous = points[points.length - 1]
19
+ if (
20
+ Number.isFinite(x) &&
21
+ Number.isFinite(y) &&
22
+ (!previous || x !== previous.x || y !== previous.y)
23
+ ) {
24
+ points.push({ x, y })
25
+ }
26
+ }
27
+ if (
28
+ points.length > 1 &&
29
+ points[0].x === points.at(-1).x &&
30
+ points[0].y === points.at(-1).y
31
+ )
32
+ points.pop()
33
+ if (points.length < 3) continue
34
+ const circle = PcbScene3dCutoutCircleDetector.resolve(points)
35
+ if (circle) {
36
+ result.push(this.#circle(circle, points.length))
37
+ continue
38
+ }
39
+ if (this.#isConvex(points)) {
40
+ result.push(this.#polygon(points))
41
+ continue
42
+ }
43
+ const indices = earcut(points.flatMap(({ x, y }) => [x, y]))
44
+ for (let i = 0; i < indices.length; i += 3) {
45
+ result.push(
46
+ this.#polygon([
47
+ points[indices[i]],
48
+ points[indices[i + 1]],
49
+ points[indices[i + 2]]
50
+ ])
51
+ )
52
+ }
53
+ }
54
+ return result.filter((entry) => entry.planes.length >= 3)
55
+ }
56
+
57
+ /**
58
+ * Checks turn orientation, including collinear vertices.
59
+ * @param {object[]} points Contour vertices.
60
+ * @returns {boolean} Whether the contour is convex.
61
+ */
62
+ static #isConvex(points) {
63
+ let direction = 0
64
+ for (let i = 0; i < points.length; i += 1) {
65
+ const a = points[i],
66
+ b = points[(i + 1) % points.length],
67
+ c = points[(i + 2) % points.length]
68
+ const cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x)
69
+ if (cross === 0) continue
70
+ if (direction && Math.sign(cross) !== direction) return false
71
+ direction = Math.sign(cross)
72
+ }
73
+ return direction !== 0
74
+ }
75
+
76
+ /**
77
+ * Builds normalized inward half planes for either contour winding.
78
+ * @param {object[]} points Convex contour vertices.
79
+ * @returns {object} Prepared prism.
80
+ */
81
+ static #polygon(points) {
82
+ let area = 0
83
+ const origin = points[0]
84
+ for (let i = 1; i + 1 < points.length; i += 1) {
85
+ area +=
86
+ (points[i].x - origin.x) * (points[i + 1].y - origin.y) -
87
+ (points[i].y - origin.y) * (points[i + 1].x - origin.x)
88
+ }
89
+ const direction = Math.sign(area)
90
+ const planes = []
91
+ const bounds = {
92
+ minX: Infinity,
93
+ minY: Infinity,
94
+ maxX: -Infinity,
95
+ maxY: -Infinity
96
+ }
97
+ for (let i = 0; i < points.length; i += 1) {
98
+ const a = points[i],
99
+ b = points[(i + 1) % points.length]
100
+ const length = Math.hypot(b.x - a.x, b.y - a.y)
101
+ if (length && direction) {
102
+ const x = (direction * (a.y - b.y)) / length
103
+ const y = (direction * (b.x - a.x)) / length
104
+ planes.push({ x, y, offset: x * a.x + y * a.y })
105
+ }
106
+ bounds.minX = Math.min(bounds.minX, a.x)
107
+ bounds.maxX = Math.max(bounds.maxX, a.x)
108
+ bounds.minY = Math.min(bounds.minY, a.y)
109
+ bounds.maxY = Math.max(bounds.maxY, a.y)
110
+ }
111
+ return { planes, bounds }
112
+ }
113
+
114
+ /**
115
+ * Uses tangents so sampled circles never leave copper slivers inside a pad.
116
+ * @param {object} circle Analytic circle metadata.
117
+ * @param {number} pointCount Source contour resolution.
118
+ * @returns {object} Conservative circular prism.
119
+ */
120
+ static #circle(circle, pointCount) {
121
+ const count = Math.max(32, pointCount)
122
+ const { centerX, centerY, radius } = circle
123
+ const planes = []
124
+ for (let i = 0; i < count; i += 1) {
125
+ const angle = (2 * Math.PI * i) / count
126
+ const x = Math.cos(angle),
127
+ y = Math.sin(angle)
128
+ planes.push({
129
+ x: -x,
130
+ y: -y,
131
+ offset: -x * centerX - y * centerY - radius
132
+ })
133
+ }
134
+ const extent = radius / Math.cos(Math.PI / count)
135
+ return {
136
+ planes,
137
+ bounds: {
138
+ minX: centerX - extent,
139
+ maxX: centerX + extent,
140
+ minY: centerY - extent,
141
+ maxY: centerY + extent
142
+ }
143
+ }
144
+ }
145
+ }
@@ -45,7 +45,7 @@ export class PcbScene3dCopperTextFactory {
45
45
  * @param {any[]} texts
46
46
  * @param {number} z
47
47
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
48
- * @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]
48
+ * @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, preparedCutoutCache?: WeakMap }} [options]
49
49
  * @returns {any}
50
50
  */
51
51
  static buildGroup(THREE, texts, z, normalizeBoardPoint, options = {}) {
@@ -88,7 +88,7 @@ export class PcbScene3dCopperTextFactory {
88
88
  THREE,
89
89
  geometry,
90
90
  options?.drillCutouts,
91
- options?.preparedPolygonCache
91
+ options
92
92
  )
93
93
 
94
94
  if (
@@ -117,21 +117,19 @@ export class PcbScene3dCopperTextFactory {
117
117
  * @param {any} THREE
118
118
  * @param {any} geometry
119
119
  * @param {{ x: number, y: number }[][] | undefined} drillCutouts
120
- * @param {Map | undefined} preparedPolygonCache Request-scoped prepared cache.
120
+ * @param {{ preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} options Build-scoped cutout caches.
121
121
  * @returns {any}
122
122
  */
123
- static #filterDrillCutouts(
124
- THREE,
125
- geometry,
126
- drillCutouts,
127
- preparedPolygonCache
128
- ) {
123
+ static #filterDrillCutouts(THREE, geometry, drillCutouts, options) {
129
124
  return Array.isArray(drillCutouts) && drillCutouts.length
130
125
  ? PcbScene3dCutoutGeometryFilter.filter(
131
126
  THREE,
132
127
  geometry,
133
128
  drillCutouts,
134
- { preparedPolygonCache }
129
+ {
130
+ preparedPolygonCache: options?.preparedPolygonCache,
131
+ preparedCutoutCache: options?.preparedCutoutCache
132
+ }
135
133
  )
136
134
  : geometry
137
135
  }
@@ -16,7 +16,7 @@ export class PcbScene3dCutoutGeometryFilter {
16
16
  * @param {any} THREE
17
17
  * @param {any} geometry
18
18
  * @param {{ x: number, y: number }[][]} cutouts
19
- * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map }} [options]
19
+ * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} [options]
20
20
  * @returns {any}
21
21
  */
22
22
  static filter(THREE, geometry, cutouts, options = {}) {
@@ -36,10 +36,12 @@ export class PcbScene3dCutoutGeometryFilter {
36
36
  if (!position?.count) {
37
37
  return geometry
38
38
  }
39
- const preparedCutouts = PcbScene3dCutoutGeometryFilter.#prepareCutouts(
40
- cutouts,
41
- PcbScene3dCutoutGeometryFilter.#resolvePreparedPolygonCache(options)
42
- )
39
+ const queryContext =
40
+ PcbScene3dCutoutGeometryFilter.#resolveQueryContext(
41
+ cutouts,
42
+ options
43
+ )
44
+ const { preparedCutouts } = queryContext
43
45
  if (
44
46
  PcbScene3dGeometryBoundsResolver.missesAllPositionBounds(
45
47
  position,
@@ -48,7 +50,8 @@ export class PcbScene3dCutoutGeometryFilter {
48
50
  )
49
51
  )
50
52
  return geometry
51
- const cutoutIndex = new PcbScene3dCutoutGridIndex(preparedCutouts)
53
+ const cutoutIndex = (queryContext.cutoutIndex ||=
54
+ new PcbScene3dCutoutGridIndex(preparedCutouts))
52
55
  const settings =
53
56
  PcbScene3dCutoutGeometryFilter.#resolveSettings(options)
54
57
  const positions = []
@@ -80,9 +83,38 @@ export class PcbScene3dCutoutGeometryFilter {
80
83
  filteredGeometry.computeVertexNormals?.()
81
84
  return filteredGeometry
82
85
  }
86
+ /**
87
+ * Resolves preparation and a lazily built query index for an immutable
88
+ * cutout collection. Cache ownership belongs to one caller build; calls
89
+ * without a cache always observe the current source coordinates.
90
+ * @param {object[][]} cutouts Source cutout collection.
91
+ * @param {{ preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} options Build-scoped options.
92
+ * @returns {{ preparedCutouts: PcbScene3dPreparedPolygon[], cutoutIndex: PcbScene3dCutoutGridIndex | null }}
93
+ */
94
+ static #resolveQueryContext(cutouts, options) {
95
+ const cache =
96
+ options?.preparedCutoutCache instanceof WeakMap
97
+ ? options.preparedCutoutCache
98
+ : null
99
+ const cached = cache?.get(cutouts)
100
+ if (cached) return cached
101
+
102
+ const context = {
103
+ preparedCutouts: PcbScene3dCutoutGeometryFilter.#prepareCutouts(
104
+ cutouts,
105
+ PcbScene3dCutoutGeometryFilter.#resolvePreparedPolygonCache(
106
+ options
107
+ )
108
+ ),
109
+ cutoutIndex: null
110
+ }
111
+ cache?.set(cutouts, context)
112
+ return context
113
+ }
114
+
83
115
  /**
84
116
  * Resolves clipping settings.
85
- * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map }} options
117
+ * @param {{ maxDepth?: number, maxEdgeLength?: number, discardTerminalOverlaps?: boolean, preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} options
86
118
  * @returns {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number, discardTerminalOverlaps: boolean }}
87
119
  */
88
120
  static #resolveSettings(options) {
@@ -106,7 +138,7 @@ export class PcbScene3dCutoutGeometryFilter {
106
138
  }
107
139
  /**
108
140
  * Resolves a supported request-scoped prepared polygon cache.
109
- * @param {{ preparedPolygonCache?: Map }} options Request options.
141
+ * @param {{ preparedPolygonCache?: Map, preparedCutoutCache?: WeakMap }} options Request options.
110
142
  * @returns {Map | null}
111
143
  */
112
144
  static #resolvePreparedPolygonCache(options) {