pcb-scene3d-viewer 1.3.3 → 1.3.5
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 +5 -0
- package/docs/api.md +18 -0
- package/docs/model-format.md +5 -0
- package/docs/release-notes-v1.3.5.md +8 -0
- package/package.json +3 -2
- package/src/PcbAssemblyFillGeometryResolver.mjs +16 -0
- package/src/PcbScene3dCopperOcclusionClipper.mjs +4 -5
- package/src/PcbScene3dCopperOcclusionGeometry.mjs +182 -0
- package/src/PcbScene3dCopperOcclusionPlanes.mjs +145 -0
- package/src/PcbScene3dCopperTextFactory.mjs +8 -10
- package/src/PcbScene3dCutoutGeometryFilter.mjs +40 -8
- package/src/PcbScene3dExternalModelRepeatedOwnerPackageCenterRepair.mjs +78 -32
- package/src/PcbScene3dGeneratedGeometryBuilder.mjs +87 -0
- package/src/PcbScene3dGeometryTransfer.mjs +398 -0
- package/src/PcbScene3dGeometryWorker.mjs +45 -0
- package/src/PcbScene3dGeometryWorkerClient.mjs +182 -0
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +17 -27
- package/src/PcbScene3dRuntime.mjs +33 -54
- package/src/PcbScene3dSilkscreenChunkedFactory.mjs +15 -3
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +35 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +58 -22
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +4 -3
- package/src/PcbScene3dStepLoader.mjs +11 -0
- package/src/PcbScene3dWorkerClient.mjs +12 -6
package/README.md
CHANGED
|
@@ -47,6 +47,10 @@ second quarter-turn when the imported assembly envelope proves that its depth
|
|
|
47
47
|
and height axes are already exchanged. See the
|
|
48
48
|
[1.3.3 release notes](docs/release-notes-v1.3.3.md).
|
|
49
49
|
|
|
50
|
+
Version 1.3.5 reports OCCT worker resource failures to the current STEP load
|
|
51
|
+
and permits a later retry. See the
|
|
52
|
+
[1.3.5 release notes](docs/release-notes-v1.3.5.md).
|
|
53
|
+
|
|
50
54
|
## CircuitJSON 1.1 convergence
|
|
51
55
|
|
|
52
56
|
Version 1.2.2 accepts the common document and prepared-context shapes returned
|
|
@@ -155,6 +159,7 @@ const controller = new PcbScene3dController(viewportNode, document)
|
|
|
155
159
|
|
|
156
160
|
- [API](docs/api.md)
|
|
157
161
|
- [CircuitJSON usage](docs/circuitjson.md)
|
|
162
|
+
- [1.3.5 release notes](docs/release-notes-v1.3.5.md)
|
|
158
163
|
- [1.3.3 release notes](docs/release-notes-v1.3.3.md)
|
|
159
164
|
- [1.3.2 release notes](docs/release-notes-v1.3.2.md)
|
|
160
165
|
- [1.3.1 release notes](docs/release-notes-v1.3.1.md)
|
package/docs/api.md
CHANGED
|
@@ -212,6 +212,9 @@ are unavailable, the viewer dynamically imports the ESM factory directly; it
|
|
|
212
212
|
does not inject a classic script or depend on a global factory. Worker transfer
|
|
213
213
|
uses a loader-owned byte snapshot, and rejected ESM initialization is evicted
|
|
214
214
|
so callers retain their input and can retry transient failures.
|
|
215
|
+
Worker resource failures are returned to the loader as failed import results;
|
|
216
|
+
the loader rejects the affected request with the reported cause and can retry
|
|
217
|
+
the next load without an uncaught worker exception.
|
|
215
218
|
|
|
216
219
|
Static `authHeaders` are sent only to the main model origin. A host that
|
|
217
220
|
intentionally authorizes another origin can return headers from
|
|
@@ -365,3 +368,18 @@ The package also exports focused factories used by the runtime:
|
|
|
365
368
|
These factories accept Three.js constructors and normalized scene-detail
|
|
366
369
|
objects. They are exported for tests and advanced hosts, but most applications
|
|
367
370
|
should use `PcbScene3dRuntime` or `PcbScene3dController`.
|
|
371
|
+
|
|
372
|
+
### Copper occlusion geometry
|
|
373
|
+
|
|
374
|
+
Covered copper is clipped against vertical occlusion prisms at the contour
|
|
375
|
+
boundaries. Convex contours are clipped directly; concave contours are
|
|
376
|
+
triangulated into convex pieces. Intersection vertices interpolate all three
|
|
377
|
+
coordinates, preserving sloped surfaces and vertical relief walls without
|
|
378
|
+
recursive mesh subdivision. Sampled circular openings use tangent planes with
|
|
379
|
+
at least 32 segments (or the source contour resolution when greater), preventing
|
|
380
|
+
copper slivers inside the analytic circle. This keeps mesh growth tied to the
|
|
381
|
+
opening boundaries rather than board dimensions or a subdivision depth.
|
|
382
|
+
|
|
383
|
+
Repeated external-model package placement uses ownership indexes scoped to one
|
|
384
|
+
repair invocation. Input scenes remain mutable: subsequent applications rebuild
|
|
385
|
+
the indexes and observe edits to pad and component rows.
|
package/docs/model-format.md
CHANGED
|
@@ -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.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pcb-scene3d-viewer 1.3.5
|
|
2
|
+
|
|
3
|
+
The STEP loader now forwards a failed OCCT worker result with its resource
|
|
4
|
+
failure message instead of reporting only that the importer returned no meshes.
|
|
5
|
+
Failed loads are evicted from the model cache, so a later request can retry.
|
|
6
|
+
|
|
7
|
+
This release pairs with `@sunbox/occt-import-js` 0.0.29, which posts failures
|
|
8
|
+
as results rather than uncaught worker exceptions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pcb-scene3d-viewer",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pcb",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"docs/release-notes-v1.3.1.md",
|
|
41
41
|
"docs/release-notes-v1.3.2.md",
|
|
42
42
|
"docs/release-notes-v1.3.3.md",
|
|
43
|
+
"docs/release-notes-v1.3.5.md",
|
|
43
44
|
"docs/model-format.md",
|
|
44
45
|
"docs/testing.md",
|
|
45
46
|
"spec",
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
},
|
|
62
63
|
"dependencies": {
|
|
63
64
|
"@sunbox/occt-import-js": "^0.0.28",
|
|
64
|
-
"circuitjson-toolkit": "^1.4.
|
|
65
|
+
"circuitjson-toolkit": "^1.4.3",
|
|
65
66
|
"earcut": "3.0.2",
|
|
66
67
|
"fflate": "^0.8.2",
|
|
67
68
|
"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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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 =
|
|
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) {
|