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
|
@@ -9,6 +9,8 @@ import { PcbScene3dCopperFillMeshBuilder } from './PcbScene3dCopperFillMeshBuild
|
|
|
9
9
|
import { PcbScene3dMaskCoveredCopperSideGroupBuilder } from './PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs'
|
|
10
10
|
import { PcbScene3dCopperDrillCutoutBuilder } from './PcbScene3dCopperDrillCutoutBuilder.mjs'
|
|
11
11
|
import { PcbScene3dCopperFillAreaClipper } from './PcbScene3dCopperFillAreaClipper.mjs'
|
|
12
|
+
import { PcbScene3dCopperFillCoverageContext } from './PcbScene3dCopperFillCoverageContext.mjs'
|
|
13
|
+
import { PcbScene3dCopperFillLoopSetResolver } from './PcbScene3dCopperFillLoopSetResolver.mjs'
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Builds copper-detail meshes for the interactive 3D PCB scene.
|
|
@@ -284,6 +286,15 @@ export class PcbScene3dCopperFactory {
|
|
|
284
286
|
occlusionCutouts,
|
|
285
287
|
options = {}
|
|
286
288
|
) {
|
|
289
|
+
const fills = detail?.fills || []
|
|
290
|
+
const loopSets = PcbScene3dCopperFillLoopSetResolver.resolve(
|
|
291
|
+
fills,
|
|
292
|
+
normalizeBoardPoint,
|
|
293
|
+
mirrorY
|
|
294
|
+
)
|
|
295
|
+
const shouldUnionCoveredLayerPrimitives =
|
|
296
|
+
PcbScene3dCopperFactory.#shouldUnionCoveredLayerPrimitives(options)
|
|
297
|
+
let coverageContext = null
|
|
287
298
|
let trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
|
|
288
299
|
THREE,
|
|
289
300
|
detail?.tracks || [],
|
|
@@ -303,28 +314,32 @@ export class PcbScene3dCopperFactory {
|
|
|
303
314
|
occlusionCutouts
|
|
304
315
|
)
|
|
305
316
|
if (
|
|
306
|
-
|
|
317
|
+
shouldUnionCoveredLayerPrimitives &&
|
|
318
|
+
loopSets.length &&
|
|
319
|
+
(trackMesh || arcMesh)
|
|
307
320
|
) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
coverageContext =
|
|
322
|
+
PcbScene3dCopperFillCoverageContext.fromLoopSets(loopSets)
|
|
323
|
+
if (trackMesh) {
|
|
324
|
+
trackMesh = PcbScene3dCopperFillAreaClipper.filterPrepared(
|
|
325
|
+
THREE,
|
|
326
|
+
trackMesh,
|
|
327
|
+
coverageContext,
|
|
328
|
+
{ subdividePartialTriangles: false }
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
if (arcMesh) {
|
|
332
|
+
arcMesh = PcbScene3dCopperFillAreaClipper.filterPrepared(
|
|
333
|
+
THREE,
|
|
334
|
+
arcMesh,
|
|
335
|
+
coverageContext,
|
|
336
|
+
{ subdividePartialTriangles: false }
|
|
337
|
+
)
|
|
338
|
+
}
|
|
324
339
|
}
|
|
325
340
|
const fillMesh = PcbScene3dCopperFillMeshBuilder.build(
|
|
326
341
|
THREE,
|
|
327
|
-
|
|
342
|
+
fills,
|
|
328
343
|
z,
|
|
329
344
|
PcbScene3dCopperPrismBuilder.thicknessMil(),
|
|
330
345
|
normalizeBoardPoint,
|
|
@@ -333,10 +348,9 @@ export class PcbScene3dCopperFactory {
|
|
|
333
348
|
occlusionCutouts,
|
|
334
349
|
{
|
|
335
350
|
surfaceOnly: true,
|
|
336
|
-
clipContainedFillOverlaps:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
351
|
+
clipContainedFillOverlaps: shouldUnionCoveredLayerPrimitives,
|
|
352
|
+
loopSets,
|
|
353
|
+
coverageContext
|
|
340
354
|
}
|
|
341
355
|
)
|
|
342
356
|
return PcbScene3dMaskCoveredCopperSideGroupBuilder.build(THREE, {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PcbScene3dCopperFillCoverageContext } from './PcbScene3dCopperFillCoverageContext.mjs'
|
|
2
|
+
import { PcbScene3dCopperFillLoopSetResolver } from './PcbScene3dCopperFillLoopSetResolver.mjs'
|
|
3
|
+
import { PcbScene3dTriangleVertexQueryBounds } from './PcbScene3dTriangleVertexQueryBounds.mjs'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Clips redundant copper relief where track geometry is already inside a fill.
|
|
5
7
|
*/
|
|
6
8
|
export class PcbScene3dCopperFillAreaClipper {
|
|
7
|
-
static #AREA_EPSILON = 0.001
|
|
8
9
|
static #GEOMETRY_EPSILON = 0.001
|
|
9
10
|
static #MAX_DEPTH = 10
|
|
10
11
|
static #MAX_EDGE_LENGTH = 2
|
|
@@ -27,17 +28,44 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
27
28
|
mirrorY,
|
|
28
29
|
options = {}
|
|
29
30
|
) {
|
|
30
|
-
const
|
|
31
|
+
const loopSets = PcbScene3dCopperFillLoopSetResolver.resolve(
|
|
31
32
|
fills,
|
|
32
33
|
normalizeBoardPoint,
|
|
33
34
|
mirrorY
|
|
34
35
|
)
|
|
36
|
+
const coverageContext =
|
|
37
|
+
PcbScene3dCopperFillCoverageContext.fromLoopSets(loopSets)
|
|
38
|
+
|
|
39
|
+
return PcbScene3dCopperFillAreaClipper.filterPrepared(
|
|
40
|
+
THREE,
|
|
41
|
+
mesh,
|
|
42
|
+
coverageContext,
|
|
43
|
+
options
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Removes triangles covered by a prepared fill-coverage context.
|
|
49
|
+
* @param {any} THREE Three.js namespace.
|
|
50
|
+
* @param {any | null} mesh Mesh to clip.
|
|
51
|
+
* @param {{ queryAreas: (bounds: object, target: object[], options?: object) => object[] }} coverageContext Prepared side-local fill coverage.
|
|
52
|
+
* @param {{ subdividePartialTriangles?: boolean, beforeSourceIndex?: number, allowedSourceIndexes?: Set<number> | number[] | null }} [options] Clipping and source-order options.
|
|
53
|
+
* @returns {any | null}
|
|
54
|
+
*/
|
|
55
|
+
static filterPrepared(THREE, mesh, coverageContext, options = {}) {
|
|
56
|
+
if (!mesh || coverageContext?.areaCount === 0) {
|
|
57
|
+
return mesh
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
const sourceGeometry = mesh?.geometry?.index
|
|
36
61
|
? mesh.geometry.toNonIndexed?.() || mesh.geometry
|
|
37
62
|
: mesh?.geometry
|
|
38
63
|
const position = sourceGeometry?.getAttribute?.('position')
|
|
39
64
|
|
|
40
|
-
if (
|
|
65
|
+
if (
|
|
66
|
+
!position?.count ||
|
|
67
|
+
typeof coverageContext?.queryAreas !== 'function'
|
|
68
|
+
) {
|
|
41
69
|
return mesh
|
|
42
70
|
}
|
|
43
71
|
|
|
@@ -50,7 +78,7 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
50
78
|
position,
|
|
51
79
|
index
|
|
52
80
|
),
|
|
53
|
-
|
|
81
|
+
coverageContext,
|
|
54
82
|
0,
|
|
55
83
|
state,
|
|
56
84
|
options
|
|
@@ -75,113 +103,29 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
75
103
|
return mesh
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
/**
|
|
79
|
-
* Resolves normalized fill areas with internal holes.
|
|
80
|
-
* @param {object[]} fills Copper fill primitives.
|
|
81
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
82
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
83
|
-
* @returns {{ outer: { x: number, y: number }[], holes: { points: { x: number, y: number }[], bounds: object, segments: object[] }[], bounds: object, segments: object[] }[]}
|
|
84
|
-
*/
|
|
85
|
-
static #prepareAreas(fills, normalizeBoardPoint, mirrorY) {
|
|
86
|
-
return (fills || []).flatMap((fill) =>
|
|
87
|
-
PcbAssemblyFillGeometryResolver.resolveAll(fill)
|
|
88
|
-
.map((loops) =>
|
|
89
|
-
PcbScene3dCopperFillAreaClipper.#prepareLoopSet(
|
|
90
|
-
loops,
|
|
91
|
-
normalizeBoardPoint,
|
|
92
|
-
mirrorY
|
|
93
|
-
)
|
|
94
|
-
)
|
|
95
|
-
.filter(Boolean)
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Resolves one normalized fill loop set.
|
|
101
|
-
* @param {{ outer?: any[], holes?: any[][] }} loops Fill loop set.
|
|
102
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
103
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
104
|
-
* @returns {object | null}
|
|
105
|
-
*/
|
|
106
|
-
static #prepareLoopSet(loops, normalizeBoardPoint, mirrorY) {
|
|
107
|
-
const outer = PcbScene3dCopperFillAreaClipper.#normalizeLoop(
|
|
108
|
-
loops?.outer,
|
|
109
|
-
normalizeBoardPoint,
|
|
110
|
-
mirrorY
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
if (!PcbScene3dCopperFillAreaClipper.#isValidLoop(outer)) {
|
|
114
|
-
return null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const holes = (loops?.holes || [])
|
|
118
|
-
.map((hole) =>
|
|
119
|
-
PcbScene3dCopperFillAreaClipper.#normalizeLoop(
|
|
120
|
-
hole,
|
|
121
|
-
normalizeBoardPoint,
|
|
122
|
-
mirrorY
|
|
123
|
-
)
|
|
124
|
-
)
|
|
125
|
-
.filter((hole) =>
|
|
126
|
-
PcbScene3dCopperFillAreaClipper.#isValidLoop(hole)
|
|
127
|
-
)
|
|
128
|
-
.map((hole) => ({
|
|
129
|
-
points: hole,
|
|
130
|
-
bounds: PcbScene3dCopperFillAreaClipper.#resolveBounds(hole),
|
|
131
|
-
segments: PcbScene3dCopperFillAreaClipper.#segments(hole)
|
|
132
|
-
}))
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
outer,
|
|
136
|
-
holes,
|
|
137
|
-
bounds: PcbScene3dCopperFillAreaClipper.#resolveBounds(outer),
|
|
138
|
-
segments: PcbScene3dCopperFillAreaClipper.#segments(outer)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Normalizes one fill loop.
|
|
144
|
-
* @param {any[]} loop Source loop points.
|
|
145
|
-
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
146
|
-
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
147
|
-
* @returns {{ x: number, y: number }[]}
|
|
148
|
-
*/
|
|
149
|
-
static #normalizeLoop(loop, normalizeBoardPoint, mirrorY) {
|
|
150
|
-
const points = []
|
|
151
|
-
for (const point of loop || []) {
|
|
152
|
-
const normalized = normalizeBoardPoint(
|
|
153
|
-
Number(point?.x ?? point?.[0]),
|
|
154
|
-
Number(point?.y ?? point?.[1])
|
|
155
|
-
)
|
|
156
|
-
const nextPoint = {
|
|
157
|
-
x: Number(normalized?.x),
|
|
158
|
-
y: mirrorY ? -Number(normalized?.y) : Number(normalized?.y)
|
|
159
|
-
}
|
|
160
|
-
if (Number.isFinite(nextPoint.x) && Number.isFinite(nextPoint.y)) {
|
|
161
|
-
points.push(nextPoint)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return PcbScene3dCopperFillAreaClipper.#cleanLoop(points)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
106
|
/**
|
|
168
107
|
* Appends one triangle after removing filled-area overlap.
|
|
169
108
|
* @param {number[]} positions Output position buffer.
|
|
170
109
|
* @param {{ x: number, y: number, z: number }[]} triangle Source triangle.
|
|
171
|
-
* @param {object[]}
|
|
110
|
+
* @param {{ queryAreas: (bounds: object, target: object[], options?: object) => object[] }} coverageContext Prepared fill coverage.
|
|
172
111
|
* @param {number} depth Subdivision depth.
|
|
173
112
|
* @param {{ changed: boolean }} state Mutation state.
|
|
174
|
-
* @param {{ subdividePartialTriangles?: boolean }} options Clipping options.
|
|
113
|
+
* @param {{ subdividePartialTriangles?: boolean, beforeSourceIndex?: number, allowedSourceIndexes?: Set<number> | number[] | null }} options Clipping options.
|
|
175
114
|
* @returns {void}
|
|
176
115
|
*/
|
|
177
116
|
static #appendFilteredTriangle(
|
|
178
117
|
positions,
|
|
179
118
|
triangle,
|
|
180
|
-
|
|
119
|
+
coverageContext,
|
|
181
120
|
depth,
|
|
182
121
|
state,
|
|
183
122
|
options
|
|
184
123
|
) {
|
|
124
|
+
const areas = coverageContext.queryAreas(
|
|
125
|
+
PcbScene3dCopperFillAreaClipper.#resolveBounds(triangle),
|
|
126
|
+
[],
|
|
127
|
+
options
|
|
128
|
+
)
|
|
185
129
|
const coverage =
|
|
186
130
|
PcbScene3dCopperFillAreaClipper.#resolveTriangleCoverage(
|
|
187
131
|
triangle,
|
|
@@ -233,7 +177,7 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
233
177
|
PcbScene3dCopperFillAreaClipper.#appendFilteredTriangle(
|
|
234
178
|
positions,
|
|
235
179
|
child,
|
|
236
|
-
|
|
180
|
+
coverageContext,
|
|
237
181
|
depth + 1,
|
|
238
182
|
state,
|
|
239
183
|
options
|
|
@@ -258,12 +202,19 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
258
202
|
*/
|
|
259
203
|
static #resolveTriangleCoverage(triangle, areas) {
|
|
260
204
|
const samples = [
|
|
261
|
-
|
|
205
|
+
triangle[0],
|
|
206
|
+
triangle[1],
|
|
207
|
+
triangle[2],
|
|
262
208
|
PcbScene3dCopperFillAreaClipper.#centroid(triangle)
|
|
263
209
|
]
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
210
|
+
let coveredSamples = 0
|
|
211
|
+
for (const point of samples) {
|
|
212
|
+
if (
|
|
213
|
+
PcbScene3dCopperFillAreaClipper.#isPointInAnyArea(point, areas)
|
|
214
|
+
) {
|
|
215
|
+
coveredSamples += 1
|
|
216
|
+
}
|
|
217
|
+
}
|
|
267
218
|
const crosses =
|
|
268
219
|
PcbScene3dCopperFillAreaClipper.#triangleCrossesAnyBoundary(
|
|
269
220
|
triangle,
|
|
@@ -288,15 +239,19 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
288
239
|
* @returns {boolean}
|
|
289
240
|
*/
|
|
290
241
|
static #isPointInAnyArea(point, areas) {
|
|
291
|
-
|
|
292
|
-
PcbScene3dCopperFillAreaClipper.#isPointInArea(point, area)
|
|
293
|
-
|
|
242
|
+
for (const area of areas) {
|
|
243
|
+
if (PcbScene3dCopperFillAreaClipper.#isPointInArea(point, area)) {
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return false
|
|
294
249
|
}
|
|
295
250
|
|
|
296
251
|
/**
|
|
297
252
|
* Returns true when one point is inside a fill outer and outside holes.
|
|
298
253
|
* @param {{ x: number, y: number }} point Candidate point.
|
|
299
|
-
* @param {{ outer:
|
|
254
|
+
* @param {{ outer: object, holes: object[], bounds: object }} area Filled copper area.
|
|
300
255
|
* @returns {boolean}
|
|
301
256
|
*/
|
|
302
257
|
static #isPointInArea(point, area) {
|
|
@@ -305,7 +260,10 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
305
260
|
area.bounds,
|
|
306
261
|
point
|
|
307
262
|
) ||
|
|
308
|
-
!PcbScene3dCopperFillAreaClipper.#
|
|
263
|
+
!PcbScene3dCopperFillAreaClipper.#pointInPreparedPolygon(
|
|
264
|
+
point,
|
|
265
|
+
area.outer
|
|
266
|
+
)
|
|
309
267
|
) {
|
|
310
268
|
return false
|
|
311
269
|
}
|
|
@@ -316,9 +274,9 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
316
274
|
hole.bounds,
|
|
317
275
|
point
|
|
318
276
|
) &&
|
|
319
|
-
PcbScene3dCopperFillAreaClipper.#
|
|
277
|
+
PcbScene3dCopperFillAreaClipper.#pointInPreparedPolygon(
|
|
320
278
|
point,
|
|
321
|
-
hole
|
|
279
|
+
hole
|
|
322
280
|
)
|
|
323
281
|
)
|
|
324
282
|
}
|
|
@@ -341,7 +299,8 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
341
299
|
) &&
|
|
342
300
|
PcbScene3dCopperFillAreaClipper.#triangleCrossesAreaBoundary(
|
|
343
301
|
triangle,
|
|
344
|
-
area
|
|
302
|
+
area,
|
|
303
|
+
triangleBounds
|
|
345
304
|
)
|
|
346
305
|
)
|
|
347
306
|
}
|
|
@@ -349,15 +308,16 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
349
308
|
/**
|
|
350
309
|
* Returns true when a triangle crosses one filled area's boundaries.
|
|
351
310
|
* @param {{ x: number, y: number }[]} triangle Triangle points.
|
|
352
|
-
* @param {{ outer:
|
|
311
|
+
* @param {{ outer: object, holes: object[] }} area Filled copper area.
|
|
312
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds Triangle bounds.
|
|
353
313
|
* @returns {boolean}
|
|
354
314
|
*/
|
|
355
|
-
static #triangleCrossesAreaBoundary(triangle, area) {
|
|
356
|
-
return [area, ...area.holes].some((loop) =>
|
|
315
|
+
static #triangleCrossesAreaBoundary(triangle, area, triangleBounds) {
|
|
316
|
+
return [area.outer, ...area.holes].some((loop) =>
|
|
357
317
|
PcbScene3dCopperFillAreaClipper.#triangleCrossesLoopBoundary(
|
|
358
318
|
triangle,
|
|
359
|
-
loop
|
|
360
|
-
|
|
319
|
+
loop,
|
|
320
|
+
triangleBounds
|
|
361
321
|
)
|
|
362
322
|
)
|
|
363
323
|
}
|
|
@@ -365,11 +325,11 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
365
325
|
/**
|
|
366
326
|
* Returns true when triangle edges cross one loop.
|
|
367
327
|
* @param {{ x: number, y: number }[]} triangle Triangle points.
|
|
368
|
-
* @param {{ x: number, y: number }[]} loop
|
|
369
|
-
* @param {{
|
|
328
|
+
* @param {{ points: { x: number, y: number }[], bounds: object, querySegments: (bounds: object, target: object[]) => object[], queryVertices: (bounds: object, target: object[]) => object[] }} loop Prepared loop.
|
|
329
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds Triangle bounds.
|
|
370
330
|
* @returns {boolean}
|
|
371
331
|
*/
|
|
372
|
-
static #triangleCrossesLoopBoundary(triangle, loop,
|
|
332
|
+
static #triangleCrossesLoopBoundary(triangle, loop, triangleBounds) {
|
|
373
333
|
for (let index = 0; index < triangle.length; index += 1) {
|
|
374
334
|
const start = triangle[index]
|
|
375
335
|
const end = triangle[(index + 1) % triangle.length]
|
|
@@ -378,25 +338,37 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
378
338
|
end
|
|
379
339
|
])
|
|
380
340
|
if (
|
|
381
|
-
|
|
382
|
-
(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
341
|
+
loop
|
|
342
|
+
.querySegments(edgeBounds, [])
|
|
343
|
+
.some(
|
|
344
|
+
(segment) =>
|
|
345
|
+
PcbScene3dCopperFillAreaClipper.#boundsOverlap(
|
|
346
|
+
edgeBounds,
|
|
347
|
+
segment.bounds
|
|
348
|
+
) &&
|
|
349
|
+
PcbScene3dCopperFillAreaClipper.#segmentsIntersect(
|
|
350
|
+
start,
|
|
351
|
+
end,
|
|
352
|
+
segment.start,
|
|
353
|
+
segment.end
|
|
354
|
+
)
|
|
355
|
+
)
|
|
394
356
|
) {
|
|
395
357
|
return true
|
|
396
358
|
}
|
|
397
359
|
}
|
|
398
360
|
|
|
399
|
-
|
|
361
|
+
const queryBounds = PcbScene3dTriangleVertexQueryBounds.resolve(
|
|
362
|
+
triangle,
|
|
363
|
+
triangleBounds,
|
|
364
|
+
loop.bounds,
|
|
365
|
+
PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON
|
|
366
|
+
)
|
|
367
|
+
const vertices = queryBounds
|
|
368
|
+
? loop.queryVertices(queryBounds, [])
|
|
369
|
+
: loop.points
|
|
370
|
+
|
|
371
|
+
return vertices.some((point) =>
|
|
400
372
|
PcbScene3dCopperFillAreaClipper.#isPointInsideTriangle(
|
|
401
373
|
point,
|
|
402
374
|
triangle
|
|
@@ -418,25 +390,6 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
418
390
|
}))
|
|
419
391
|
}
|
|
420
392
|
|
|
421
|
-
/**
|
|
422
|
-
* Builds loop segments with cached bounds.
|
|
423
|
-
* @param {{ x: number, y: number }[]} points Loop points.
|
|
424
|
-
* @returns {{ start: object, end: object, bounds: object }[]}
|
|
425
|
-
*/
|
|
426
|
-
static #segments(points) {
|
|
427
|
-
return points.map((start, index) => {
|
|
428
|
-
const end = points[(index + 1) % points.length]
|
|
429
|
-
return {
|
|
430
|
-
start,
|
|
431
|
-
end,
|
|
432
|
-
bounds: PcbScene3dCopperFillAreaClipper.#resolveBounds([
|
|
433
|
-
start,
|
|
434
|
-
end
|
|
435
|
-
])
|
|
436
|
-
}
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
|
|
440
393
|
/**
|
|
441
394
|
* Subdivides one triangle into four children.
|
|
442
395
|
* @param {{ x: number, y: number, z: number }[]} triangle Source triangle.
|
|
@@ -518,20 +471,26 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
518
471
|
}
|
|
519
472
|
|
|
520
473
|
/**
|
|
521
|
-
*
|
|
474
|
+
* Replays the legacy horizontal-ray expression on prepared edge candidates.
|
|
522
475
|
* @param {{ x: number, y: number }} point Candidate point.
|
|
523
|
-
* @param {{
|
|
476
|
+
* @param {{ bounds: object, querySegments: (bounds: object, target: object[]) => object[] }} polygon Prepared polygon.
|
|
524
477
|
* @returns {boolean}
|
|
525
478
|
*/
|
|
526
|
-
static #
|
|
479
|
+
static #pointInPreparedPolygon(point, polygon) {
|
|
480
|
+
const segments = polygon.querySegments(
|
|
481
|
+
{
|
|
482
|
+
minX: point.x,
|
|
483
|
+
maxX: polygon.bounds.maxX,
|
|
484
|
+
minY: point.y,
|
|
485
|
+
maxY: point.y
|
|
486
|
+
},
|
|
487
|
+
[]
|
|
488
|
+
)
|
|
527
489
|
let inside = false
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
) {
|
|
533
|
-
const current = polygon[index]
|
|
534
|
-
const previous = polygon[previousIndex]
|
|
490
|
+
|
|
491
|
+
for (const segment of segments) {
|
|
492
|
+
const current = segment.end
|
|
493
|
+
const previous = segment.start
|
|
535
494
|
const intersects =
|
|
536
495
|
current.y > point.y !== previous.y > point.y &&
|
|
537
496
|
point.x <
|
|
@@ -552,19 +511,22 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
552
511
|
* @returns {boolean}
|
|
553
512
|
*/
|
|
554
513
|
static #isPointInsideTriangle(point, triangle) {
|
|
555
|
-
|
|
514
|
+
let hasNegative = false
|
|
515
|
+
let hasPositive = false
|
|
516
|
+
|
|
517
|
+
for (let index = 0; index < triangle.length; index += 1) {
|
|
518
|
+
const current = triangle[index]
|
|
556
519
|
const next = triangle[(index + 1) % triangle.length]
|
|
557
|
-
|
|
520
|
+
const sign =
|
|
558
521
|
(point.x - next.x) * (current.y - next.y) -
|
|
559
522
|
(current.x - next.x) * (point.y - next.y)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
)
|
|
523
|
+
|
|
524
|
+
hasNegative ||=
|
|
525
|
+
sign < -PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON
|
|
526
|
+
hasPositive ||=
|
|
527
|
+
sign > PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON
|
|
528
|
+
}
|
|
529
|
+
|
|
568
530
|
return !(hasNegative && hasPositive)
|
|
569
531
|
}
|
|
570
532
|
|
|
@@ -599,70 +561,6 @@ export class PcbScene3dCopperFillAreaClipper {
|
|
|
599
561
|
return (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)
|
|
600
562
|
}
|
|
601
563
|
|
|
602
|
-
/**
|
|
603
|
-
* Removes duplicate and closing points.
|
|
604
|
-
* @param {{ x: number, y: number }[]} points Candidate points.
|
|
605
|
-
* @returns {{ x: number, y: number }[]}
|
|
606
|
-
*/
|
|
607
|
-
static #cleanLoop(points) {
|
|
608
|
-
const output = []
|
|
609
|
-
for (const point of points || []) {
|
|
610
|
-
const previous = output[output.length - 1]
|
|
611
|
-
if (
|
|
612
|
-
previous &&
|
|
613
|
-
Math.abs(previous.x - point.x) <
|
|
614
|
-
PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON &&
|
|
615
|
-
Math.abs(previous.y - point.y) <
|
|
616
|
-
PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON
|
|
617
|
-
) {
|
|
618
|
-
continue
|
|
619
|
-
}
|
|
620
|
-
output.push(point)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const first = output[0]
|
|
624
|
-
const last = output[output.length - 1]
|
|
625
|
-
if (
|
|
626
|
-
first &&
|
|
627
|
-
last &&
|
|
628
|
-
Math.abs(first.x - last.x) <
|
|
629
|
-
PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON &&
|
|
630
|
-
Math.abs(first.y - last.y) <
|
|
631
|
-
PcbScene3dCopperFillAreaClipper.#GEOMETRY_EPSILON
|
|
632
|
-
) {
|
|
633
|
-
output.pop()
|
|
634
|
-
}
|
|
635
|
-
return output
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Checks whether one loop has enough area.
|
|
640
|
-
* @param {{ x: number, y: number }[]} loop Candidate loop.
|
|
641
|
-
* @returns {boolean}
|
|
642
|
-
*/
|
|
643
|
-
static #isValidLoop(loop) {
|
|
644
|
-
return (
|
|
645
|
-
loop.length >= 3 &&
|
|
646
|
-
Math.abs(PcbScene3dCopperFillAreaClipper.#signedArea(loop)) >
|
|
647
|
-
PcbScene3dCopperFillAreaClipper.#AREA_EPSILON
|
|
648
|
-
)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Computes signed loop area.
|
|
653
|
-
* @param {{ x: number, y: number }[]} loop Candidate loop.
|
|
654
|
-
* @returns {number}
|
|
655
|
-
*/
|
|
656
|
-
static #signedArea(loop) {
|
|
657
|
-
let area = 0
|
|
658
|
-
for (let index = 0; index < loop.length; index += 1) {
|
|
659
|
-
const current = loop[index]
|
|
660
|
-
const next = loop[(index + 1) % loop.length]
|
|
661
|
-
area += current.x * next.y - next.x * current.y
|
|
662
|
-
}
|
|
663
|
-
return area / 2
|
|
664
|
-
}
|
|
665
|
-
|
|
666
564
|
/**
|
|
667
565
|
* Resolves bounds for points.
|
|
668
566
|
* @param {{ x: number, y: number }[]} points Candidate points.
|