pcb-scene3d-viewer 1.3.2 → 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/README.md +6 -0
- package/docs/api.md +15 -0
- package/docs/model-format.md +12 -0
- package/docs/release-notes-v1.3.3.md +28 -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/PcbScene3dExternalModelSourceOriginPolicy.mjs +3 -2
- package/src/PcbScene3dExternalModels.mjs +77 -1
- 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/PcbScene3dWorkerClient.mjs +12 -6
package/README.md
CHANGED
|
@@ -42,6 +42,11 @@ short-circuits non-subdividing copper-fill clipping without changing its
|
|
|
42
42
|
observable geometry. See the
|
|
43
43
|
[1.3.2 release notes](docs/release-notes-v1.3.2.md).
|
|
44
44
|
|
|
45
|
+
Version 1.3.3 preserves validated STEP source anchors and avoids applying a
|
|
46
|
+
second quarter-turn when the imported assembly envelope proves that its depth
|
|
47
|
+
and height axes are already exchanged. See the
|
|
48
|
+
[1.3.3 release notes](docs/release-notes-v1.3.3.md).
|
|
49
|
+
|
|
45
50
|
## CircuitJSON 1.1 convergence
|
|
46
51
|
|
|
47
52
|
Version 1.2.2 accepts the common document and prepared-context shapes returned
|
|
@@ -150,6 +155,7 @@ const controller = new PcbScene3dController(viewportNode, document)
|
|
|
150
155
|
|
|
151
156
|
- [API](docs/api.md)
|
|
152
157
|
- [CircuitJSON usage](docs/circuitjson.md)
|
|
158
|
+
- [1.3.3 release notes](docs/release-notes-v1.3.3.md)
|
|
153
159
|
- [1.3.2 release notes](docs/release-notes-v1.3.2.md)
|
|
154
160
|
- [1.3.1 release notes](docs/release-notes-v1.3.1.md)
|
|
155
161
|
- [1.2.2 release notes](docs/release-notes-v1.2.2.md)
|
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.
|
package/docs/model-format.md
CHANGED
|
@@ -186,6 +186,13 @@ Embedded STEP models can use:
|
|
|
186
186
|
}
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
For imported STEP assemblies, the runtime compares the fully assembled mesh
|
|
190
|
+
envelope with an authored model-bounds projection before applying a separate
|
|
191
|
+
quarter-turn around the model X axis. When the imported assembly has already
|
|
192
|
+
exchanged its depth and height axes, the duplicate tilt is omitted. Toolkits
|
|
193
|
+
can set `modelTransform.preserveSourceAnchor: true` when an authored body origin
|
|
194
|
+
must remain fixed without requesting component-center recovery.
|
|
195
|
+
|
|
189
196
|
## Detail Primitives
|
|
190
197
|
|
|
191
198
|
The runtime expects pre-normalized primitive lists for:
|
|
@@ -226,3 +233,8 @@ truthy field adds a solder-mask ring on that board surface while the plated
|
|
|
226
233
|
through-hole barrel remains copper. A mixed via therefore renders one covered
|
|
227
234
|
annulus and one exposed annulus; a via with both fields explicitly false stays
|
|
228
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,28 @@
|
|
|
1
|
+
# pcb-scene3d-viewer 1.3.3
|
|
2
|
+
|
|
3
|
+
Version 1.3.3 improves external STEP placement when source-authored anchors and
|
|
4
|
+
pre-oriented assemblies are present.
|
|
5
|
+
|
|
6
|
+
## STEP placement fidelity
|
|
7
|
+
|
|
8
|
+
- An explicit `preserveSourceAnchor` transform keeps a validated source body
|
|
9
|
+
origin fixed without activating component-center recovery.
|
|
10
|
+
- Embedded STEP assemblies omit a duplicate quarter-turn only when their loaded
|
|
11
|
+
mesh envelope proves that the authored depth and height axes are already
|
|
12
|
+
exchanged.
|
|
13
|
+
- Board-space component yaw and the original external-model asset remain
|
|
14
|
+
unchanged.
|
|
15
|
+
|
|
16
|
+
## Compatibility
|
|
17
|
+
|
|
18
|
+
- Detection uses source origin, projection metadata, mesh dimensions, and
|
|
19
|
+
bounded error thresholds; it does not inspect filenames, project identities,
|
|
20
|
+
component labels, or library names.
|
|
21
|
+
- Existing scene colors, board geometry, and public APIs remain unchanged.
|
|
22
|
+
|
|
23
|
+
## Verification
|
|
24
|
+
|
|
25
|
+
- Repository-owned tests cover preserved source anchors and embedded-axis
|
|
26
|
+
normalization alongside the existing external-model placement suite.
|
|
27
|
+
- The complete package suite, formatting check, and npm package dry run are
|
|
28
|
+
required for release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pcb-scene3d-viewer",
|
|
3
|
-
"version": "1.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",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"docs/release-notes-v1.3.0.md",
|
|
40
40
|
"docs/release-notes-v1.3.1.md",
|
|
41
41
|
"docs/release-notes-v1.3.2.md",
|
|
42
|
+
"docs/release-notes-v1.3.3.md",
|
|
42
43
|
"docs/model-format.md",
|
|
43
44
|
"docs/testing.md",
|
|
44
45
|
"spec",
|
|
@@ -60,7 +61,7 @@
|
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
63
|
"@sunbox/occt-import-js": "^0.0.28",
|
|
63
|
-
"circuitjson-toolkit": "^1.4.
|
|
64
|
+
"circuitjson-toolkit": "^1.4.3",
|
|
64
65
|
"earcut": "3.0.2",
|
|
65
66
|
"fflate": "^0.8.2",
|
|
66
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 {
|
|
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) {
|