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,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
|
-
|
|
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
|
-
*
|
|
109
|
-
* @param {{
|
|
110
|
-
* @returns {
|
|
108
|
+
* Resolves a supported request-scoped prepared polygon cache.
|
|
109
|
+
* @param {{ preparedPolygonCache?: Map }} options Request options.
|
|
110
|
+
* @returns {Map | null}
|
|
111
111
|
*/
|
|
112
|
-
static #
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
*
|
|
133
|
-
* @param {{ x: number, y: number }[]}
|
|
134
|
-
* @
|
|
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 #
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
?
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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 {
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
PcbScene3dCutoutGeometryFilter.#
|
|
750
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|