pcb-scene3d-viewer 1.1.19 → 1.1.21
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 +3 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +467 -0
- package/src/PcbAssemblyExportCoordinateFrame.mjs +19 -0
- package/src/PcbAssemblyGeometryBuildProgress.mjs +201 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +994 -0
- package/src/PcbAssemblyMeshUtils.mjs +541 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +447 -0
- package/src/PcbAssemblyPolygonTriangulator.mjs +301 -0
- package/src/PcbAssemblyStepWriter.mjs +952 -0
- package/src/PcbAssemblyWrlWriter.mjs +144 -0
- package/src/PcbScene3dCameraRig.mjs +11 -5
- package/src/PcbScene3dComponentVisibility.mjs +161 -0
- package/src/PcbScene3dController.mjs +29 -4
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +1 -1
- package/src/PcbScene3dInteractionHints.mjs +14 -6
- package/src/PcbScene3dRenderScheduler.mjs +55 -0
- package/src/PcbScene3dRuntime.mjs +56 -4
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +64 -3
- package/src/PcbScene3dSelectionVisibilityBinder.mjs +119 -0
- package/src/PcbScene3dText.mjs +4 -2
- package/src/scene3d.mjs +12 -0
- package/src/styles/scene3d.css +48 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const EPSILON = 1e-9
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Triangulates simple planar polygon faces for tessellated assembly export.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbAssemblyPolygonTriangulator {
|
|
7
|
+
/**
|
|
8
|
+
* Triangulates one mesh face without assuming the polygon is convex.
|
|
9
|
+
* @param {number[]} indexes Face vertex indexes.
|
|
10
|
+
* @param {number[][]} vertices Mesh vertices.
|
|
11
|
+
* @returns {number[][]}
|
|
12
|
+
*/
|
|
13
|
+
static triangulateFace(indexes, vertices) {
|
|
14
|
+
const projected = PcbAssemblyPolygonTriangulator.#projectFace(
|
|
15
|
+
indexes,
|
|
16
|
+
vertices
|
|
17
|
+
)
|
|
18
|
+
if (!projected.length) {
|
|
19
|
+
return PcbAssemblyPolygonTriangulator.#fanTriangulate(indexes)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nodes = indexes.map((sourceIndex, index) => ({
|
|
23
|
+
sourceIndex,
|
|
24
|
+
x: projected[index][0],
|
|
25
|
+
y: projected[index][1]
|
|
26
|
+
}))
|
|
27
|
+
const signedArea = PcbAssemblyPolygonTriangulator.#signedArea(nodes)
|
|
28
|
+
if (Math.abs(signedArea) <= EPSILON) {
|
|
29
|
+
return PcbAssemblyPolygonTriangulator.#fanTriangulate(indexes)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
PcbAssemblyPolygonTriangulator.#earClip(
|
|
34
|
+
signedArea < 0 ? [...nodes].reverse() : [...nodes],
|
|
35
|
+
signedArea < 0
|
|
36
|
+
) || PcbAssemblyPolygonTriangulator.#fanTriangulate(indexes)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ear-clips one counter-clockwise projected polygon.
|
|
42
|
+
* @param {{ sourceIndex: number, x: number, y: number }[]} nodes Polygon nodes.
|
|
43
|
+
* @param {boolean} reverseOutput Whether output must be reversed to match source winding.
|
|
44
|
+
* @returns {number[][] | null}
|
|
45
|
+
*/
|
|
46
|
+
static #earClip(nodes, reverseOutput) {
|
|
47
|
+
const remaining = [...nodes]
|
|
48
|
+
const triangles = []
|
|
49
|
+
let guard = remaining.length * remaining.length
|
|
50
|
+
|
|
51
|
+
while (remaining.length > 3 && guard > 0) {
|
|
52
|
+
guard -= 1
|
|
53
|
+
const earIndex = PcbAssemblyPolygonTriangulator.#findEar(remaining)
|
|
54
|
+
if (earIndex < 0) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
triangles.push(
|
|
59
|
+
PcbAssemblyPolygonTriangulator.#triangleAt(
|
|
60
|
+
remaining,
|
|
61
|
+
earIndex,
|
|
62
|
+
reverseOutput
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
remaining.splice(earIndex, 1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (remaining.length === 3) {
|
|
69
|
+
triangles.push(
|
|
70
|
+
PcbAssemblyPolygonTriangulator.#triangleFromNodes(
|
|
71
|
+
remaining[0],
|
|
72
|
+
remaining[1],
|
|
73
|
+
remaining[2],
|
|
74
|
+
reverseOutput
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return triangles
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Finds the next removable ear.
|
|
84
|
+
* @param {{ sourceIndex: number, x: number, y: number }[]} nodes Polygon nodes.
|
|
85
|
+
* @returns {number}
|
|
86
|
+
*/
|
|
87
|
+
static #findEar(nodes) {
|
|
88
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
89
|
+
if (PcbAssemblyPolygonTriangulator.#isEar(nodes, index)) {
|
|
90
|
+
return index
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return -1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns true when the node at index forms a valid ear.
|
|
99
|
+
* @param {{ sourceIndex: number, x: number, y: number }[]} nodes Polygon nodes.
|
|
100
|
+
* @param {number} index Candidate index.
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
static #isEar(nodes, index) {
|
|
104
|
+
const previous = nodes[(index - 1 + nodes.length) % nodes.length]
|
|
105
|
+
const current = nodes[index]
|
|
106
|
+
const next = nodes[(index + 1) % nodes.length]
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
PcbAssemblyPolygonTriangulator.#cross2d(previous, current, next) <=
|
|
110
|
+
EPSILON
|
|
111
|
+
) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return !nodes.some((node, nodeIndex) => {
|
|
116
|
+
if (
|
|
117
|
+
nodeIndex === index ||
|
|
118
|
+
nodeIndex === (index - 1 + nodes.length) % nodes.length ||
|
|
119
|
+
nodeIndex === (index + 1) % nodes.length
|
|
120
|
+
) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return PcbAssemblyPolygonTriangulator.#pointInsideTriangle(
|
|
125
|
+
node,
|
|
126
|
+
previous,
|
|
127
|
+
current,
|
|
128
|
+
next
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds the triangle around one ear index.
|
|
135
|
+
* @param {{ sourceIndex: number, x: number, y: number }[]} nodes Polygon nodes.
|
|
136
|
+
* @param {number} index Ear index.
|
|
137
|
+
* @param {boolean} reverseOutput Whether output must be reversed.
|
|
138
|
+
* @returns {number[]}
|
|
139
|
+
*/
|
|
140
|
+
static #triangleAt(nodes, index, reverseOutput) {
|
|
141
|
+
return PcbAssemblyPolygonTriangulator.#triangleFromNodes(
|
|
142
|
+
nodes[(index - 1 + nodes.length) % nodes.length],
|
|
143
|
+
nodes[index],
|
|
144
|
+
nodes[(index + 1) % nodes.length],
|
|
145
|
+
reverseOutput
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Builds one source-index triangle.
|
|
151
|
+
* @param {{ sourceIndex: number }} first First node.
|
|
152
|
+
* @param {{ sourceIndex: number }} second Second node.
|
|
153
|
+
* @param {{ sourceIndex: number }} third Third node.
|
|
154
|
+
* @param {boolean} reverseOutput Whether output must be reversed.
|
|
155
|
+
* @returns {number[]}
|
|
156
|
+
*/
|
|
157
|
+
static #triangleFromNodes(first, second, third, reverseOutput) {
|
|
158
|
+
return reverseOutput
|
|
159
|
+
? [first.sourceIndex, third.sourceIndex, second.sourceIndex]
|
|
160
|
+
: [first.sourceIndex, second.sourceIndex, third.sourceIndex]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Projects a 3D face onto its dominant 2D plane.
|
|
165
|
+
* @param {number[]} indexes Face vertex indexes.
|
|
166
|
+
* @param {number[][]} vertices Mesh vertices.
|
|
167
|
+
* @returns {number[][]}
|
|
168
|
+
*/
|
|
169
|
+
static #projectFace(indexes, vertices) {
|
|
170
|
+
const points = indexes
|
|
171
|
+
.map((index) => vertices[index])
|
|
172
|
+
.filter((point) => Array.isArray(point) && point.length >= 3)
|
|
173
|
+
const normal = PcbAssemblyPolygonTriangulator.#newellNormal(points)
|
|
174
|
+
const dominantAxis =
|
|
175
|
+
Math.abs(normal[0]) >= Math.abs(normal[1]) &&
|
|
176
|
+
Math.abs(normal[0]) >= Math.abs(normal[2])
|
|
177
|
+
? 0
|
|
178
|
+
: Math.abs(normal[1]) >= Math.abs(normal[2])
|
|
179
|
+
? 1
|
|
180
|
+
: 2
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
Math.abs(normal[0]) <= EPSILON &&
|
|
184
|
+
Math.abs(normal[1]) <= EPSILON &&
|
|
185
|
+
Math.abs(normal[2]) <= EPSILON
|
|
186
|
+
) {
|
|
187
|
+
return []
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return points.map((point) =>
|
|
191
|
+
dominantAxis === 0
|
|
192
|
+
? [Number(point[1] || 0), Number(point[2] || 0)]
|
|
193
|
+
: dominantAxis === 1
|
|
194
|
+
? [Number(point[0] || 0), Number(point[2] || 0)]
|
|
195
|
+
: [Number(point[0] || 0), Number(point[1] || 0)]
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Computes a polygon normal with Newell's method.
|
|
201
|
+
* @param {number[][]} points Face points.
|
|
202
|
+
* @returns {number[]}
|
|
203
|
+
*/
|
|
204
|
+
static #newellNormal(points) {
|
|
205
|
+
const normal = [0, 0, 0]
|
|
206
|
+
|
|
207
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
208
|
+
const current = points[index]
|
|
209
|
+
const next = points[(index + 1) % points.length]
|
|
210
|
+
normal[0] +=
|
|
211
|
+
(Number(current[1] || 0) - Number(next[1] || 0)) *
|
|
212
|
+
(Number(current[2] || 0) + Number(next[2] || 0))
|
|
213
|
+
normal[1] +=
|
|
214
|
+
(Number(current[2] || 0) - Number(next[2] || 0)) *
|
|
215
|
+
(Number(current[0] || 0) + Number(next[0] || 0))
|
|
216
|
+
normal[2] +=
|
|
217
|
+
(Number(current[0] || 0) - Number(next[0] || 0)) *
|
|
218
|
+
(Number(current[1] || 0) + Number(next[1] || 0))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return normal
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Computes signed polygon area in projected coordinates.
|
|
226
|
+
* @param {{ x: number, y: number }[]} nodes Polygon nodes.
|
|
227
|
+
* @returns {number}
|
|
228
|
+
*/
|
|
229
|
+
static #signedArea(nodes) {
|
|
230
|
+
let area = 0
|
|
231
|
+
|
|
232
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
233
|
+
const current = nodes[index]
|
|
234
|
+
const next = nodes[(index + 1) % nodes.length]
|
|
235
|
+
area += current.x * next.y - next.x * current.y
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return area / 2
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Computes the 2D cross product for three points.
|
|
243
|
+
* @param {{ x: number, y: number }} first First point.
|
|
244
|
+
* @param {{ x: number, y: number }} second Second point.
|
|
245
|
+
* @param {{ x: number, y: number }} third Third point.
|
|
246
|
+
* @returns {number}
|
|
247
|
+
*/
|
|
248
|
+
static #cross2d(first, second, third) {
|
|
249
|
+
return (
|
|
250
|
+
(second.x - first.x) * (third.y - first.y) -
|
|
251
|
+
(second.y - first.y) * (third.x - first.x)
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Returns true when a point lies strictly inside a triangle.
|
|
257
|
+
* @param {{ x: number, y: number }} point Candidate point.
|
|
258
|
+
* @param {{ x: number, y: number }} first First triangle point.
|
|
259
|
+
* @param {{ x: number, y: number }} second Second triangle point.
|
|
260
|
+
* @param {{ x: number, y: number }} third Third triangle point.
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*/
|
|
263
|
+
static #pointInsideTriangle(point, first, second, third) {
|
|
264
|
+
const firstCross = PcbAssemblyPolygonTriangulator.#cross2d(
|
|
265
|
+
first,
|
|
266
|
+
second,
|
|
267
|
+
point
|
|
268
|
+
)
|
|
269
|
+
const secondCross = PcbAssemblyPolygonTriangulator.#cross2d(
|
|
270
|
+
second,
|
|
271
|
+
third,
|
|
272
|
+
point
|
|
273
|
+
)
|
|
274
|
+
const thirdCross = PcbAssemblyPolygonTriangulator.#cross2d(
|
|
275
|
+
third,
|
|
276
|
+
first,
|
|
277
|
+
point
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
firstCross > EPSILON &&
|
|
282
|
+
secondCross > EPSILON &&
|
|
283
|
+
thirdCross > EPSILON
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Falls back to simple fan triangles for degenerate faces.
|
|
289
|
+
* @param {number[]} indexes Face vertex indexes.
|
|
290
|
+
* @returns {number[][]}
|
|
291
|
+
*/
|
|
292
|
+
static #fanTriangulate(indexes) {
|
|
293
|
+
const triangles = []
|
|
294
|
+
|
|
295
|
+
for (let index = 1; index < indexes.length - 1; index += 1) {
|
|
296
|
+
triangles.push([indexes[0], indexes[index], indexes[index + 1]])
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return triangles
|
|
300
|
+
}
|
|
301
|
+
}
|