pcb-scene3d-viewer 1.0.1 → 1.1.1
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/package.json +1 -1
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +14 -2
- package/src/PcbScene3dBoardAssemblyTransform.mjs +180 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +123 -10
- package/src/PcbScene3dBoardShapeFactory.mjs +95 -5
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +1 -1
- package/src/PcbScene3dBodyColor.mjs +26 -0
- package/src/PcbScene3dBufferAttributeFactory.mjs +128 -0
- package/src/PcbScene3dCircularCutoutOverlap.mjs +167 -0
- package/src/PcbScene3dCutoutCircleDetector.mjs +84 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +151 -32
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +3 -30
- package/src/PcbScene3dExternalModels.mjs +30 -15
- package/src/PcbScene3dRuntime.mjs +24 -33
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +80 -1
- package/src/PcbScene3dSilkscreenChunkedFactory.mjs +217 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +49 -8
- package/src/PcbScene3dStepLoader.mjs +100 -27
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates Three.js buffer attributes from importer mesh arrays.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dBufferAttributeFactory {
|
|
5
|
+
/**
|
|
6
|
+
* Creates a floating-point attribute.
|
|
7
|
+
* @param {any} THREE Three.js namespace.
|
|
8
|
+
* @param {ArrayLike<number>} values Source values.
|
|
9
|
+
* @param {number} itemSize Attribute item size.
|
|
10
|
+
* @returns {any}
|
|
11
|
+
*/
|
|
12
|
+
static createFloat32(THREE, values, itemSize) {
|
|
13
|
+
return PcbScene3dBufferAttributeFactory.#create(
|
|
14
|
+
THREE,
|
|
15
|
+
values,
|
|
16
|
+
itemSize,
|
|
17
|
+
Float32Array,
|
|
18
|
+
'Float32BufferAttribute'
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates an unsigned integer attribute.
|
|
24
|
+
* @param {any} THREE Three.js namespace.
|
|
25
|
+
* @param {ArrayLike<number>} values Source values.
|
|
26
|
+
* @param {number} itemSize Attribute item size.
|
|
27
|
+
* @returns {any}
|
|
28
|
+
*/
|
|
29
|
+
static createUint32(THREE, values, itemSize) {
|
|
30
|
+
return PcbScene3dBufferAttributeFactory.#create(
|
|
31
|
+
THREE,
|
|
32
|
+
values,
|
|
33
|
+
itemSize,
|
|
34
|
+
Uint32Array,
|
|
35
|
+
'Uint32BufferAttribute'
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true when a value is an array-like numeric sequence.
|
|
41
|
+
* @param {unknown} values Candidate values.
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
static isNumberSequence(values) {
|
|
45
|
+
return (
|
|
46
|
+
Array.isArray(values) ||
|
|
47
|
+
PcbScene3dBufferAttributeFactory.#isTypedNumberArray(values)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a buffer attribute with the most efficient constructor available.
|
|
53
|
+
* @param {any} THREE Three.js namespace.
|
|
54
|
+
* @param {ArrayLike<number>} values Source values.
|
|
55
|
+
* @param {number} itemSize Attribute item size.
|
|
56
|
+
* @param {Float32ArrayConstructor | Uint32ArrayConstructor} TypedArrayConstructor Fallback typed array constructor.
|
|
57
|
+
* @param {string} fallbackConstructorName Legacy Three attribute constructor name.
|
|
58
|
+
* @returns {any}
|
|
59
|
+
*/
|
|
60
|
+
static #create(
|
|
61
|
+
THREE,
|
|
62
|
+
values,
|
|
63
|
+
itemSize,
|
|
64
|
+
TypedArrayConstructor,
|
|
65
|
+
fallbackConstructorName
|
|
66
|
+
) {
|
|
67
|
+
const array = PcbScene3dBufferAttributeFactory.#normalizeValues(
|
|
68
|
+
values,
|
|
69
|
+
TypedArrayConstructor
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (typeof THREE?.BufferAttribute === 'function') {
|
|
73
|
+
return new THREE.BufferAttribute(array, itemSize)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof THREE?.[fallbackConstructorName] === 'function') {
|
|
77
|
+
return new THREE[fallbackConstructorName](array, itemSize)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
array,
|
|
82
|
+
itemSize,
|
|
83
|
+
count: Math.floor(Number(array.length || 0) / itemSize),
|
|
84
|
+
isBufferAttribute: true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Normalizes plain arrays while preserving importer-owned typed arrays.
|
|
90
|
+
* @param {ArrayLike<number> | undefined | null} values Source values.
|
|
91
|
+
* @param {Float32ArrayConstructor | Uint32ArrayConstructor} TypedArrayConstructor Fallback typed array constructor.
|
|
92
|
+
* @returns {ArrayLike<number>}
|
|
93
|
+
*/
|
|
94
|
+
static #normalizeValues(values, TypedArrayConstructor) {
|
|
95
|
+
if (PcbScene3dBufferAttributeFactory.#isTypedNumberArray(values)) {
|
|
96
|
+
return values
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return new TypedArrayConstructor(Array.from(values || []))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when a sequence is already backed by a typed numeric array.
|
|
104
|
+
* @param {unknown} values Candidate values.
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
static #isTypedNumberArray(values) {
|
|
108
|
+
return (
|
|
109
|
+
ArrayBuffer.isView(values) &&
|
|
110
|
+
!(values instanceof DataView) &&
|
|
111
|
+
!PcbScene3dBufferAttributeFactory.#isBigIntTypedArray(values)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns true when a typed-array view stores bigint values.
|
|
117
|
+
* @param {unknown} values Candidate values.
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
static #isBigIntTypedArray(values) {
|
|
121
|
+
const tag = Object.prototype.toString.call(values)
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
tag === '[object BigInt64Array]' ||
|
|
125
|
+
tag === '[object BigUint64Array]'
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves analytic overlap checks for sampled circular drill cutouts.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dCircularCutoutOverlap {
|
|
7
|
+
/**
|
|
8
|
+
* Returns true when one triangle overlaps a circular cutout.
|
|
9
|
+
* @param {{ x: number, y: number }[]} triangle
|
|
10
|
+
* @param {{ points?: { x: number, y: number }[], centerX?: number, centerY?: number, radius?: number }} cutout
|
|
11
|
+
* @param {number} [epsilon]
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
static overlapsTriangle(triangle, cutout, epsilon = 0.001) {
|
|
15
|
+
const radius = Number(cutout?.radius || 0) + Number(epsilon || 0)
|
|
16
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const center = {
|
|
21
|
+
x: Number(cutout?.centerX || 0),
|
|
22
|
+
y: Number(cutout?.centerY || 0)
|
|
23
|
+
}
|
|
24
|
+
const radiusSquared = radius * radius
|
|
25
|
+
|
|
26
|
+
for (const point of triangle) {
|
|
27
|
+
if (
|
|
28
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <=
|
|
29
|
+
radiusSquared
|
|
30
|
+
) {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
PcbScene3dCircularCutoutOverlap.#isPointInsideOrOnTriangle(
|
|
37
|
+
center,
|
|
38
|
+
triangle,
|
|
39
|
+
epsilon
|
|
40
|
+
)
|
|
41
|
+
) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let index = 0; index < triangle.length; index += 1) {
|
|
46
|
+
const current = triangle[index]
|
|
47
|
+
const next = triangle[(index + 1) % triangle.length]
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
PcbScene3dCircularCutoutOverlap.#distanceToSegmentSquared(
|
|
51
|
+
center,
|
|
52
|
+
current,
|
|
53
|
+
next,
|
|
54
|
+
epsilon
|
|
55
|
+
) <= radiusSquared
|
|
56
|
+
) {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return PcbScene3dCircularCutoutOverlap.#hasSampledBoundaryOverlap(
|
|
62
|
+
triangle,
|
|
63
|
+
cutout,
|
|
64
|
+
epsilon
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns true when a sampled cutout boundary point lies in the triangle.
|
|
70
|
+
* @param {{ x: number, y: number }[]} triangle
|
|
71
|
+
* @param {{ points?: { x: number, y: number }[] }} cutout
|
|
72
|
+
* @param {number} epsilon
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
static #hasSampledBoundaryOverlap(triangle, cutout, epsilon) {
|
|
76
|
+
const boundaryPoints = Array.isArray(cutout?.points)
|
|
77
|
+
? cutout.points
|
|
78
|
+
: []
|
|
79
|
+
const step = Math.max(1, Math.floor(boundaryPoints.length / 8))
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < boundaryPoints.length; index += step) {
|
|
82
|
+
if (
|
|
83
|
+
PcbScene3dCircularCutoutOverlap.#isPointInsideOrOnTriangle(
|
|
84
|
+
boundaryPoints[index],
|
|
85
|
+
triangle,
|
|
86
|
+
epsilon
|
|
87
|
+
)
|
|
88
|
+
) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns true when a point is inside or on one triangle.
|
|
98
|
+
* @param {{ x: number, y: number }} point
|
|
99
|
+
* @param {{ x: number, y: number }[]} triangle
|
|
100
|
+
* @param {number} epsilon
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
static #isPointInsideOrOnTriangle(point, triangle, epsilon) {
|
|
104
|
+
let hasNegative = false
|
|
105
|
+
let hasPositive = false
|
|
106
|
+
|
|
107
|
+
for (let index = 0; index < triangle.length; index += 1) {
|
|
108
|
+
const current = triangle[index]
|
|
109
|
+
const next = triangle[(index + 1) % triangle.length]
|
|
110
|
+
const sign = PcbScene3dCircularCutoutOverlap.#cross(
|
|
111
|
+
point,
|
|
112
|
+
current,
|
|
113
|
+
next
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
hasNegative = hasNegative || sign < -epsilon
|
|
117
|
+
hasPositive = hasPositive || sign > epsilon
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return !(hasNegative && hasPositive)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolves the squared distance from one point to a finite segment.
|
|
125
|
+
* @param {{ x: number, y: number }} point
|
|
126
|
+
* @param {{ x: number, y: number }} start
|
|
127
|
+
* @param {{ x: number, y: number }} end
|
|
128
|
+
* @param {number} epsilon
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
static #distanceToSegmentSquared(point, start, end, epsilon) {
|
|
132
|
+
const dx = end.x - start.x
|
|
133
|
+
const dy = end.y - start.y
|
|
134
|
+
const lengthSquared = dx * dx + dy * dy
|
|
135
|
+
|
|
136
|
+
if (lengthSquared <= epsilon) {
|
|
137
|
+
return (point.x - start.x) ** 2 + (point.y - start.y) ** 2
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ratio = Math.max(
|
|
141
|
+
0,
|
|
142
|
+
Math.min(
|
|
143
|
+
1,
|
|
144
|
+
((point.x - start.x) * dx + (point.y - start.y) * dy) /
|
|
145
|
+
lengthSquared
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
const projectedX = start.x + ratio * dx
|
|
149
|
+
const projectedY = start.y + ratio * dy
|
|
150
|
+
|
|
151
|
+
return (point.x - projectedX) ** 2 + (point.y - projectedY) ** 2
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolves the signed area for three points.
|
|
156
|
+
* @param {{ x: number, y: number }} first
|
|
157
|
+
* @param {{ x: number, y: number }} second
|
|
158
|
+
* @param {{ x: number, y: number }} third
|
|
159
|
+
* @returns {number}
|
|
160
|
+
*/
|
|
161
|
+
static #cross(first, second, third) {
|
|
162
|
+
return (
|
|
163
|
+
(second.x - first.x) * (third.y - first.y) -
|
|
164
|
+
(second.y - first.y) * (third.x - first.x)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects sampled circular drill cutouts for faster geometry clipping.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dCutoutCircleDetector {
|
|
5
|
+
static #DEFAULT_EPSILON = 0.001
|
|
6
|
+
static #MAX_RELATIVE_ERROR = 0.025
|
|
7
|
+
static #MIN_POINT_COUNT = 8
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves circle metadata for sampled circular drill cutouts.
|
|
11
|
+
* @param {{ x: number, y: number }[]} points
|
|
12
|
+
* @param {number} [epsilon]
|
|
13
|
+
* @returns {{ isCircular: true, centerX: number, centerY: number, radius: number } | null}
|
|
14
|
+
*/
|
|
15
|
+
static resolve(points, epsilon = this.#DEFAULT_EPSILON) {
|
|
16
|
+
if (
|
|
17
|
+
!Array.isArray(points) ||
|
|
18
|
+
points.length < PcbScene3dCutoutCircleDetector.#MIN_POINT_COUNT
|
|
19
|
+
) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const center = PcbScene3dCutoutCircleDetector.#resolveCentroid(points)
|
|
24
|
+
const radii = points.map((point) =>
|
|
25
|
+
Math.hypot(point.x - center.x, point.y - center.y)
|
|
26
|
+
)
|
|
27
|
+
const radius =
|
|
28
|
+
radii.reduce((sum, value) => sum + value, 0) / radii.length
|
|
29
|
+
const maxError = Math.max(
|
|
30
|
+
...radii.map((value) => Math.abs(value - radius))
|
|
31
|
+
)
|
|
32
|
+
const tolerance = Math.max(
|
|
33
|
+
Number(epsilon || 0),
|
|
34
|
+
radius * PcbScene3dCutoutCircleDetector.#MAX_RELATIVE_ERROR
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!Number.isFinite(radius) ||
|
|
39
|
+
radius <= Number(epsilon || 0) ||
|
|
40
|
+
maxError > tolerance
|
|
41
|
+
) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
isCircular: true,
|
|
47
|
+
centerX: center.x,
|
|
48
|
+
centerY: center.y,
|
|
49
|
+
radius
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves squared distance from a point to circular cutout center.
|
|
55
|
+
* @param {{ x: number, y: number }} point
|
|
56
|
+
* @param {{ centerX?: number, centerY?: number }} cutout
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
static distanceSquared(point, cutout) {
|
|
60
|
+
const dx = point.x - Number(cutout.centerX || 0)
|
|
61
|
+
const dy = point.y - Number(cutout.centerY || 0)
|
|
62
|
+
return dx * dx + dy * dy
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the centroid of one point list.
|
|
67
|
+
* @param {{ x: number, y: number }[]} points
|
|
68
|
+
* @returns {{ x: number, y: number }}
|
|
69
|
+
*/
|
|
70
|
+
static #resolveCentroid(points) {
|
|
71
|
+
const sum = points.reduce(
|
|
72
|
+
(accumulator, point) => ({
|
|
73
|
+
x: accumulator.x + Number(point.x || 0),
|
|
74
|
+
y: accumulator.y + Number(point.y || 0)
|
|
75
|
+
}),
|
|
76
|
+
{ x: 0, y: 0 }
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
x: sum.x / points.length,
|
|
81
|
+
y: sum.y / points.length
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { PcbScene3dCircularCutoutOverlap } from './PcbScene3dCircularCutoutOverlap.mjs'
|
|
2
|
+
import { PcbScene3dCutoutCircleDetector } from './PcbScene3dCutoutCircleDetector.mjs'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Clips filled 2D geometry against drill-cutout polygons.
|
|
3
6
|
*/
|
|
@@ -79,7 +82,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
79
82
|
/**
|
|
80
83
|
* Resolves clipping settings.
|
|
81
84
|
* @param {{ maxDepth?: number, maxEdgeLength?: number }} options
|
|
82
|
-
* @returns {{ maxDepth: number, maxEdgeLength: number }}
|
|
85
|
+
* @returns {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number }}
|
|
83
86
|
*/
|
|
84
87
|
static #resolveSettings(options) {
|
|
85
88
|
const maxEdgeLength = Math.max(
|
|
@@ -94,24 +97,35 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
94
97
|
PcbScene3dCutoutGeometryFilter.#DEFAULT_MAX_DEPTH,
|
|
95
98
|
0
|
|
96
99
|
),
|
|
97
|
-
maxEdgeLength
|
|
100
|
+
maxEdgeLength,
|
|
101
|
+
maxEdgeLengthSquared: maxEdgeLength * maxEdgeLength
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
/**
|
|
102
106
|
* Prepares cutout polygons with bounds for fast overlap checks.
|
|
103
107
|
* @param {{ x: number, y: number }[][]} cutouts
|
|
104
|
-
* @returns {{ 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 } }[]}
|
|
108
|
+
* @returns {{ 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 }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }[]}
|
|
105
109
|
*/
|
|
106
110
|
static #prepareCutouts(cutouts) {
|
|
107
111
|
return cutouts
|
|
108
112
|
.filter((cutout) => Array.isArray(cutout) && cutout.length >= 3)
|
|
109
|
-
.map((cutout) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
.map((cutout) => {
|
|
114
|
+
const circularCutout =
|
|
115
|
+
PcbScene3dCutoutCircleDetector.resolve(cutout)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
points: cutout,
|
|
119
|
+
segments:
|
|
120
|
+
PcbScene3dCutoutGeometryFilter.#buildCutoutSegments(
|
|
121
|
+
cutout
|
|
122
|
+
),
|
|
123
|
+
bounds: PcbScene3dCutoutGeometryFilter.#resolveBounds(
|
|
124
|
+
cutout
|
|
125
|
+
),
|
|
126
|
+
...circularCutout
|
|
127
|
+
}
|
|
128
|
+
})
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
/**
|
|
@@ -151,7 +165,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
151
165
|
* @param {number[]} positions
|
|
152
166
|
* @param {{ x: number, y: number, z: number }[]} triangle
|
|
153
167
|
* @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
|
|
154
|
-
* @param {{ maxDepth: number, maxEdgeLength: number }} settings
|
|
168
|
+
* @param {{ maxDepth: number, maxEdgeLength: number, maxEdgeLengthSquared: number }} settings
|
|
155
169
|
* @param {number} depth
|
|
156
170
|
* @param {{ changed: boolean }} state
|
|
157
171
|
* @param {object | null} [cutoutIndex]
|
|
@@ -199,8 +213,8 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
199
213
|
|
|
200
214
|
if (
|
|
201
215
|
depth >= settings.maxDepth ||
|
|
202
|
-
PcbScene3dCutoutGeometryFilter.#
|
|
203
|
-
settings.
|
|
216
|
+
PcbScene3dCutoutGeometryFilter.#maxEdgeLengthSquared(triangle) <=
|
|
217
|
+
settings.maxEdgeLengthSquared
|
|
204
218
|
) {
|
|
205
219
|
return
|
|
206
220
|
}
|
|
@@ -443,23 +457,22 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
443
457
|
}
|
|
444
458
|
|
|
445
459
|
/**
|
|
446
|
-
* Resolves the longest edge in one triangle.
|
|
460
|
+
* Resolves the longest squared edge length in one triangle.
|
|
447
461
|
* @param {{ x: number, y: number }[]} triangle
|
|
448
462
|
* @returns {number}
|
|
449
463
|
*/
|
|
450
|
-
static #
|
|
451
|
-
let
|
|
464
|
+
static #maxEdgeLengthSquared(triangle) {
|
|
465
|
+
let maxLengthSquared = 0
|
|
452
466
|
|
|
453
467
|
for (let index = 0; index < triangle.length; index += 1) {
|
|
454
468
|
const point = triangle[index]
|
|
455
469
|
const next = triangle[(index + 1) % triangle.length]
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
470
|
+
const dx = point.x - next.x
|
|
471
|
+
const dy = point.y - next.y
|
|
472
|
+
maxLengthSquared = Math.max(maxLengthSquared, dx * dx + dy * dy)
|
|
460
473
|
}
|
|
461
474
|
|
|
462
|
-
return
|
|
475
|
+
return maxLengthSquared
|
|
463
476
|
}
|
|
464
477
|
|
|
465
478
|
/**
|
|
@@ -496,10 +509,17 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
496
509
|
*/
|
|
497
510
|
static #boundsOverlap(first, second) {
|
|
498
511
|
return (
|
|
499
|
-
first.minX <=
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
first.
|
|
512
|
+
first.minX <=
|
|
513
|
+
second.maxX +
|
|
514
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
515
|
+
first.maxX >=
|
|
516
|
+
second.minX -
|
|
517
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
518
|
+
first.minY <=
|
|
519
|
+
second.maxY +
|
|
520
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
521
|
+
first.maxY >=
|
|
522
|
+
second.minY - PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
503
523
|
)
|
|
504
524
|
}
|
|
505
525
|
|
|
@@ -543,7 +563,7 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
543
563
|
/**
|
|
544
564
|
* Returns true when one triangle intersects or covers a cutout.
|
|
545
565
|
* @param {{ x: number, y: number }[]} triangle
|
|
546
|
-
* @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 } }} cutout
|
|
566
|
+
* @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 }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
547
567
|
* @returns {boolean}
|
|
548
568
|
*/
|
|
549
569
|
static #doesTriangleOverlapCutout(triangle, cutout) {
|
|
@@ -556,6 +576,14 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
556
576
|
return false
|
|
557
577
|
}
|
|
558
578
|
|
|
579
|
+
if (cutout.isCircular) {
|
|
580
|
+
return PcbScene3dCircularCutoutOverlap.overlapsTriangle(
|
|
581
|
+
triangle,
|
|
582
|
+
cutout,
|
|
583
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
559
587
|
for (const point of triangle) {
|
|
560
588
|
if (
|
|
561
589
|
PcbScene3dCutoutGeometryFilter.#isPointInsideOrOnCutout(
|
|
@@ -587,10 +615,28 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
587
615
|
/**
|
|
588
616
|
* Returns true when a point is inside or on a cutout.
|
|
589
617
|
* @param {{ x: number, y: number }} point
|
|
590
|
-
* @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 } }} cutout
|
|
618
|
+
* @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 }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
591
619
|
* @returns {boolean}
|
|
592
620
|
*/
|
|
593
621
|
static #isPointInsideOrOnCutout(point, cutout) {
|
|
622
|
+
if (
|
|
623
|
+
!PcbScene3dCutoutGeometryFilter.#pointOverlapsBounds(
|
|
624
|
+
point,
|
|
625
|
+
cutout.bounds
|
|
626
|
+
)
|
|
627
|
+
) {
|
|
628
|
+
return false
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (cutout.isCircular) {
|
|
632
|
+
return (
|
|
633
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <=
|
|
634
|
+
(Number(cutout.radius || 0) +
|
|
635
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON) **
|
|
636
|
+
2
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
594
640
|
return (
|
|
595
641
|
PcbScene3dCutoutGeometryFilter.#isPointOnCutoutBoundary(
|
|
596
642
|
point,
|
|
@@ -603,13 +649,47 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
603
649
|
)
|
|
604
650
|
}
|
|
605
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Returns true when a point lies within a bounding box tolerance.
|
|
654
|
+
* @param {{ x: number, y: number }} point
|
|
655
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} bounds
|
|
656
|
+
* @returns {boolean}
|
|
657
|
+
*/
|
|
658
|
+
static #pointOverlapsBounds(point, bounds) {
|
|
659
|
+
return (
|
|
660
|
+
point.x >=
|
|
661
|
+
bounds.minX -
|
|
662
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
663
|
+
point.x <=
|
|
664
|
+
bounds.maxX +
|
|
665
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
666
|
+
point.y >=
|
|
667
|
+
bounds.minY -
|
|
668
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON &&
|
|
669
|
+
point.y <=
|
|
670
|
+
bounds.maxY + PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
|
|
606
674
|
/**
|
|
607
675
|
* Returns true when a point lies inside a cutout and away from its border.
|
|
608
676
|
* @param {{ x: number, y: number }} point
|
|
609
|
-
* @param {{ points: { x: number, y: number }[], bounds: { minX: number, maxX: number, minY: number, maxY: number } }} cutout
|
|
677
|
+
* @param {{ points: { x: number, y: number }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
610
678
|
* @returns {boolean}
|
|
611
679
|
*/
|
|
612
680
|
static #isPointStrictlyInsideCutout(point, cutout) {
|
|
681
|
+
if (cutout.isCircular) {
|
|
682
|
+
const radius = Math.max(
|
|
683
|
+
0,
|
|
684
|
+
Number(cutout.radius || 0) -
|
|
685
|
+
PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
686
|
+
)
|
|
687
|
+
return (
|
|
688
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(point, cutout) <
|
|
689
|
+
radius * radius
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
613
693
|
const polygon = cutout.points
|
|
614
694
|
|
|
615
695
|
let inside = false
|
|
@@ -638,18 +718,33 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
638
718
|
/**
|
|
639
719
|
* Returns true when a point lies on a cutout edge.
|
|
640
720
|
* @param {{ x: number, y: number }} point
|
|
641
|
-
* @param {{
|
|
721
|
+
* @param {{ segments: { start: { x: number, y: number }, end: { x: number, y: number }, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }} cutout
|
|
642
722
|
* @returns {boolean}
|
|
643
723
|
*/
|
|
644
724
|
static #isPointOnCutoutBoundary(point, cutout) {
|
|
645
|
-
|
|
725
|
+
if (cutout.isCircular) {
|
|
726
|
+
return (
|
|
727
|
+
Math.abs(
|
|
728
|
+
Math.sqrt(
|
|
729
|
+
PcbScene3dCutoutCircleDetector.distanceSquared(
|
|
730
|
+
point,
|
|
731
|
+
cutout
|
|
732
|
+
)
|
|
733
|
+
) - Number(cutout.radius || 0)
|
|
734
|
+
) <= PcbScene3dCutoutGeometryFilter.#GEOMETRY_EPSILON
|
|
735
|
+
)
|
|
736
|
+
}
|
|
646
737
|
|
|
647
|
-
for (
|
|
738
|
+
for (const segment of cutout.segments) {
|
|
648
739
|
if (
|
|
740
|
+
PcbScene3dCutoutGeometryFilter.#pointOverlapsBounds(
|
|
741
|
+
point,
|
|
742
|
+
segment.bounds
|
|
743
|
+
) &&
|
|
649
744
|
PcbScene3dCutoutGeometryFilter.#isPointOnSegment(
|
|
650
745
|
point,
|
|
651
|
-
|
|
652
|
-
|
|
746
|
+
segment.start,
|
|
747
|
+
segment.end
|
|
653
748
|
)
|
|
654
749
|
) {
|
|
655
750
|
return true
|
|
@@ -703,9 +798,18 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
703
798
|
) {
|
|
704
799
|
const triangleStart = triangle[triangleIndex]
|
|
705
800
|
const triangleEnd = triangle[(triangleIndex + 1) % triangle.length]
|
|
801
|
+
const triangleSegmentBounds =
|
|
802
|
+
PcbScene3dCutoutGeometryFilter.#resolveSegmentBounds(
|
|
803
|
+
triangleStart,
|
|
804
|
+
triangleEnd
|
|
805
|
+
)
|
|
706
806
|
|
|
707
807
|
for (const segment of cutout.segments) {
|
|
708
808
|
if (
|
|
809
|
+
PcbScene3dCutoutGeometryFilter.#boundsOverlap(
|
|
810
|
+
triangleSegmentBounds,
|
|
811
|
+
segment.bounds
|
|
812
|
+
) &&
|
|
709
813
|
PcbScene3dCutoutGeometryFilter.#segmentsIntersect(
|
|
710
814
|
triangleStart,
|
|
711
815
|
triangleEnd,
|
|
@@ -721,6 +825,21 @@ export class PcbScene3dCutoutGeometryFilter {
|
|
|
721
825
|
return false
|
|
722
826
|
}
|
|
723
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Resolves one finite segment bounding box.
|
|
830
|
+
* @param {{ x: number, y: number }} firstStart
|
|
831
|
+
* @param {{ x: number, y: number }} firstEnd
|
|
832
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
|
|
833
|
+
*/
|
|
834
|
+
static #resolveSegmentBounds(firstStart, firstEnd) {
|
|
835
|
+
return {
|
|
836
|
+
minX: Math.min(firstStart.x, firstEnd.x),
|
|
837
|
+
maxX: Math.max(firstStart.x, firstEnd.x),
|
|
838
|
+
minY: Math.min(firstStart.y, firstEnd.y),
|
|
839
|
+
maxY: Math.max(firstStart.y, firstEnd.y)
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
724
843
|
/**
|
|
725
844
|
* Returns true when two finite line segments intersect.
|
|
726
845
|
* @param {{ x: number, y: number }} firstStart
|