pcb-scene3d-viewer 1.1.18 → 1.1.20
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 +179 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +988 -0
- package/src/PcbScene3dExternalModels.mjs +24 -5
- package/src/PcbScene3dModelBounds.mjs +13 -1
- package/src/PcbScene3dModelSeatingPolicy.mjs +392 -0
- package/src/scene3d.mjs +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pcb-scene3d-viewer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.20",
|
|
4
4
|
"description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pcb",
|
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
"check:format": "prettier --check ."
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@sunbox/occt-import-js": "^0.0.25",
|
|
54
55
|
"circuitjson-toolkit": "^1.0.1",
|
|
56
|
+
"earcut": "^3.0.2",
|
|
55
57
|
"fflate": "^0.8.2",
|
|
56
|
-
"@sunbox/occt-import-js": "^0.0.25",
|
|
57
58
|
"three": "^0.183.2"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import earcut from 'earcut'
|
|
2
|
+
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
3
|
+
|
|
4
|
+
const MAX_HOLE_POINTS = 48
|
|
5
|
+
const MIN_HOLE_POINTS = 8
|
|
6
|
+
const GEOMETRY_EPSILON = 0.001
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds PCB substrate meshes, including through-board drill cutouts.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbAssemblyBoardSubstrateBuilder {
|
|
12
|
+
/**
|
|
13
|
+
* Builds a board prism from an outer outline and available drill holes.
|
|
14
|
+
* @param {string} name Mesh name.
|
|
15
|
+
* @param {number[][]} outlinePoints Board outline points in mils.
|
|
16
|
+
* @param {object} sceneDescription Prepared scene description.
|
|
17
|
+
* @param {number} thickness Board thickness in mils.
|
|
18
|
+
* @param {number[]} color Board RGB color.
|
|
19
|
+
* @returns {object | null}
|
|
20
|
+
*/
|
|
21
|
+
static build(name, outlinePoints, sceneDescription, thickness, color) {
|
|
22
|
+
const outer = PcbAssemblyMeshUtils.cleanLoop(outlinePoints)
|
|
23
|
+
if (outer.length < 3) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const holes = PcbAssemblyBoardSubstrateBuilder.#boardHoleLoops(
|
|
28
|
+
sceneDescription,
|
|
29
|
+
outer
|
|
30
|
+
)
|
|
31
|
+
if (!holes.length) {
|
|
32
|
+
return PcbAssemblyMeshUtils.prism(name, outer, 0, thickness, color)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return PcbAssemblyBoardSubstrateBuilder.#prismWithHoles(
|
|
36
|
+
name,
|
|
37
|
+
outer,
|
|
38
|
+
holes,
|
|
39
|
+
thickness,
|
|
40
|
+
color
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Builds an extruded board polygon with inner drill walls.
|
|
46
|
+
* @param {string} name Mesh name.
|
|
47
|
+
* @param {number[][]} outer Outer board loop.
|
|
48
|
+
* @param {number[][][]} holes Inner hole loops.
|
|
49
|
+
* @param {number} thickness Board thickness in mils.
|
|
50
|
+
* @param {number[]} color Board RGB color.
|
|
51
|
+
* @returns {object | null}
|
|
52
|
+
*/
|
|
53
|
+
static #prismWithHoles(name, outer, holes, thickness, color) {
|
|
54
|
+
const loops = [outer, ...holes]
|
|
55
|
+
const holeIndexes = []
|
|
56
|
+
const points = []
|
|
57
|
+
const flat = []
|
|
58
|
+
|
|
59
|
+
for (const loop of loops) {
|
|
60
|
+
if (points.length) {
|
|
61
|
+
holeIndexes.push(points.length)
|
|
62
|
+
}
|
|
63
|
+
for (const point of loop) {
|
|
64
|
+
points.push(point)
|
|
65
|
+
flat.push(point[0], point[1])
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const triangles = earcut(flat, holeIndexes, 2)
|
|
70
|
+
if (!triangles.length) {
|
|
71
|
+
return PcbAssemblyMeshUtils.prism(name, outer, 0, thickness, color)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
|
|
75
|
+
const bottomZ = -halfThickness
|
|
76
|
+
const topZ = halfThickness
|
|
77
|
+
const topOffset = points.length
|
|
78
|
+
const vertices = [
|
|
79
|
+
...points.map((point) => [point[0], point[1], bottomZ]),
|
|
80
|
+
...points.map((point) => [point[0], point[1], topZ])
|
|
81
|
+
]
|
|
82
|
+
const faces = []
|
|
83
|
+
|
|
84
|
+
for (let index = 0; index + 2 < triangles.length; index += 3) {
|
|
85
|
+
const a = triangles[index]
|
|
86
|
+
const b = triangles[index + 1]
|
|
87
|
+
const c = triangles[index + 2]
|
|
88
|
+
faces.push([c, b, a])
|
|
89
|
+
faces.push([a + topOffset, b + topOffset, c + topOffset])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let loopStart = 0
|
|
93
|
+
PcbAssemblyBoardSubstrateBuilder.#appendLoopWalls(
|
|
94
|
+
faces,
|
|
95
|
+
loopStart,
|
|
96
|
+
outer.length,
|
|
97
|
+
topOffset,
|
|
98
|
+
false
|
|
99
|
+
)
|
|
100
|
+
loopStart += outer.length
|
|
101
|
+
for (const hole of holes) {
|
|
102
|
+
PcbAssemblyBoardSubstrateBuilder.#appendLoopWalls(
|
|
103
|
+
faces,
|
|
104
|
+
loopStart,
|
|
105
|
+
hole.length,
|
|
106
|
+
topOffset,
|
|
107
|
+
true
|
|
108
|
+
)
|
|
109
|
+
loopStart += hole.length
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
vertices,
|
|
115
|
+
faces,
|
|
116
|
+
...(Array.isArray(color) ? { color } : {})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Appends side-wall faces for one polygon loop.
|
|
122
|
+
* @param {number[][]} faces Mutable face list.
|
|
123
|
+
* @param {number} start Loop start index.
|
|
124
|
+
* @param {number} length Loop length.
|
|
125
|
+
* @param {number} topOffset Top vertex offset.
|
|
126
|
+
* @param {boolean} reverse Whether to reverse wall orientation.
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
static #appendLoopWalls(faces, start, length, topOffset, reverse) {
|
|
130
|
+
for (let index = 0; index < length; index += 1) {
|
|
131
|
+
const current = start + index
|
|
132
|
+
const next = start + ((index + 1) % length)
|
|
133
|
+
faces.push(
|
|
134
|
+
reverse
|
|
135
|
+
? [next, current, current + topOffset, next + topOffset]
|
|
136
|
+
: [current, next, next + topOffset, current + topOffset]
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolves all usable board hole loops.
|
|
143
|
+
* @param {object} sceneDescription Prepared scene description.
|
|
144
|
+
* @param {number[][]} outer Board outline loop.
|
|
145
|
+
* @returns {number[][][]}
|
|
146
|
+
*/
|
|
147
|
+
static #boardHoleLoops(sceneDescription, outer) {
|
|
148
|
+
const contourLoops =
|
|
149
|
+
PcbAssemblyBoardSubstrateBuilder.#drillCutoutLoops(sceneDescription)
|
|
150
|
+
const rawLoops = contourLoops.length
|
|
151
|
+
? contourLoops
|
|
152
|
+
: PcbAssemblyBoardSubstrateBuilder.#primitiveHoleLoops(
|
|
153
|
+
sceneDescription
|
|
154
|
+
)
|
|
155
|
+
const seen = new Set()
|
|
156
|
+
const holes = []
|
|
157
|
+
|
|
158
|
+
for (const loop of rawLoops) {
|
|
159
|
+
const normalized =
|
|
160
|
+
PcbAssemblyBoardSubstrateBuilder.#normalizeHoleLoop(loop)
|
|
161
|
+
if (
|
|
162
|
+
normalized.length < 3 ||
|
|
163
|
+
Math.abs(
|
|
164
|
+
PcbAssemblyBoardSubstrateBuilder.#signedArea(normalized)
|
|
165
|
+
) <= GEOMETRY_EPSILON
|
|
166
|
+
) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const center =
|
|
171
|
+
PcbAssemblyBoardSubstrateBuilder.#centroid(normalized)
|
|
172
|
+
if (
|
|
173
|
+
!PcbAssemblyBoardSubstrateBuilder.#pointInPolygon(center, outer)
|
|
174
|
+
) {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const signature =
|
|
179
|
+
PcbAssemblyBoardSubstrateBuilder.#holeSignature(normalized)
|
|
180
|
+
if (seen.has(signature)) {
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
seen.add(signature)
|
|
184
|
+
holes.push(normalized)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return holes
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reads existing drill cutout contours from scene silkscreen details.
|
|
192
|
+
* @param {object} sceneDescription Prepared scene description.
|
|
193
|
+
* @returns {Array<Array<object | number[]>>}
|
|
194
|
+
*/
|
|
195
|
+
static #drillCutoutLoops(sceneDescription) {
|
|
196
|
+
const silkscreen = sceneDescription?.detail?.silkscreen || {}
|
|
197
|
+
return ['top', 'bottom'].flatMap((side) =>
|
|
198
|
+
Array.isArray(silkscreen?.[side]?.drillCutouts)
|
|
199
|
+
? silkscreen[side].drillCutouts
|
|
200
|
+
: []
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Builds fallback hole contours from pads and vias.
|
|
206
|
+
* @param {object} sceneDescription Prepared scene description.
|
|
207
|
+
* @returns {number[][][]}
|
|
208
|
+
*/
|
|
209
|
+
static #primitiveHoleLoops(sceneDescription) {
|
|
210
|
+
const detail = sceneDescription?.detail || {}
|
|
211
|
+
return [
|
|
212
|
+
...PcbAssemblyBoardSubstrateBuilder.#array(detail.pads).map((pad) =>
|
|
213
|
+
PcbAssemblyBoardSubstrateBuilder.#padHoleLoop(pad)
|
|
214
|
+
),
|
|
215
|
+
...PcbAssemblyBoardSubstrateBuilder.#array(detail.vias).map((via) =>
|
|
216
|
+
PcbAssemblyBoardSubstrateBuilder.#viaHoleLoop(via)
|
|
217
|
+
)
|
|
218
|
+
].filter(Boolean)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds a fallback pad drill loop.
|
|
223
|
+
* @param {object} pad Pad primitive.
|
|
224
|
+
* @returns {number[][] | null}
|
|
225
|
+
*/
|
|
226
|
+
static #padHoleLoop(pad) {
|
|
227
|
+
const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
|
|
228
|
+
pad?.holeDiameter,
|
|
229
|
+
pad?.drillDiameter,
|
|
230
|
+
pad?.holeSize,
|
|
231
|
+
pad?.drill,
|
|
232
|
+
pad?.holeGeometry?.diameter,
|
|
233
|
+
pad?.holeGeometry?.width
|
|
234
|
+
])
|
|
235
|
+
if (!diameter) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const x = Number(pad?.x)
|
|
240
|
+
const y = Number(pad?.y)
|
|
241
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const slotLength = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
|
|
246
|
+
pad?.holeSlotLength,
|
|
247
|
+
pad?.slotLength,
|
|
248
|
+
pad?.holeGeometry?.slotLength,
|
|
249
|
+
pad?.holeGeometry?.length
|
|
250
|
+
])
|
|
251
|
+
if (slotLength > diameter) {
|
|
252
|
+
return PcbAssemblyBoardSubstrateBuilder.#rotatedPoints(
|
|
253
|
+
PcbAssemblyMeshUtils.capsulePoints(
|
|
254
|
+
x - (slotLength - diameter) / 2,
|
|
255
|
+
y,
|
|
256
|
+
x + (slotLength - diameter) / 2,
|
|
257
|
+
y,
|
|
258
|
+
diameter / 2
|
|
259
|
+
),
|
|
260
|
+
x,
|
|
261
|
+
y,
|
|
262
|
+
Number(pad?.holeRotation ?? pad?.rotation ?? 0)
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, 24)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Builds a fallback via drill loop.
|
|
271
|
+
* @param {object} via Via primitive.
|
|
272
|
+
* @returns {number[][] | null}
|
|
273
|
+
*/
|
|
274
|
+
static #viaHoleLoop(via) {
|
|
275
|
+
const diameter = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
|
|
276
|
+
via?.holeDiameter,
|
|
277
|
+
via?.drillDiameter,
|
|
278
|
+
via?.holeSize,
|
|
279
|
+
via?.drill
|
|
280
|
+
])
|
|
281
|
+
const x = Number(via?.x)
|
|
282
|
+
const y = Number(via?.y)
|
|
283
|
+
|
|
284
|
+
return diameter && Number.isFinite(x) && Number.isFinite(y)
|
|
285
|
+
? PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, 24)
|
|
286
|
+
: null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Normalizes one candidate hole contour.
|
|
291
|
+
* @param {Array<object | number[]>} loop Candidate loop.
|
|
292
|
+
* @returns {number[][]}
|
|
293
|
+
*/
|
|
294
|
+
static #normalizeHoleLoop(loop) {
|
|
295
|
+
const points = PcbAssemblyMeshUtils.cleanLoop(
|
|
296
|
+
PcbAssemblyBoardSubstrateBuilder.#array(loop).map((point) => [
|
|
297
|
+
Number(point?.x ?? point?.[0]),
|
|
298
|
+
Number(point?.y ?? point?.[1])
|
|
299
|
+
])
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return PcbAssemblyBoardSubstrateBuilder.#simplifyLoop(points)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Reduces very dense circular cutouts to export-friendly loops.
|
|
307
|
+
* @param {number[][]} loop Source loop.
|
|
308
|
+
* @returns {number[][]}
|
|
309
|
+
*/
|
|
310
|
+
static #simplifyLoop(loop) {
|
|
311
|
+
if (loop.length <= MAX_HOLE_POINTS) {
|
|
312
|
+
return loop
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const target = Math.max(MAX_HOLE_POINTS, MIN_HOLE_POINTS)
|
|
316
|
+
const step = loop.length / target
|
|
317
|
+
return PcbAssemblyMeshUtils.cleanLoop(
|
|
318
|
+
Array.from({ length: target }, (_entry, index) => {
|
|
319
|
+
return loop[Math.floor(index * step)]
|
|
320
|
+
})
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Rotates points around a center.
|
|
326
|
+
* @param {number[][]} points Source points.
|
|
327
|
+
* @param {number} centerX Center X.
|
|
328
|
+
* @param {number} centerY Center Y.
|
|
329
|
+
* @param {number} rotationDeg Rotation in degrees.
|
|
330
|
+
* @returns {number[][]}
|
|
331
|
+
*/
|
|
332
|
+
static #rotatedPoints(points, centerX, centerY, rotationDeg) {
|
|
333
|
+
if (Math.abs(Number(rotationDeg || 0)) < GEOMETRY_EPSILON) {
|
|
334
|
+
return points
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const angle = (Number(rotationDeg || 0) * Math.PI) / 180
|
|
338
|
+
const cos = Math.cos(angle)
|
|
339
|
+
const sin = Math.sin(angle)
|
|
340
|
+
return points.map((point) => {
|
|
341
|
+
const dx = point[0] - centerX
|
|
342
|
+
const dy = point[1] - centerY
|
|
343
|
+
return [
|
|
344
|
+
centerX + dx * cos - dy * sin,
|
|
345
|
+
centerY + dx * sin + dy * cos
|
|
346
|
+
]
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Builds a stable duplicate key for one hole loop.
|
|
352
|
+
* @param {number[][]} loop Hole loop.
|
|
353
|
+
* @returns {string}
|
|
354
|
+
*/
|
|
355
|
+
static #holeSignature(loop) {
|
|
356
|
+
const bounds = PcbAssemblyBoardSubstrateBuilder.#bounds(loop)
|
|
357
|
+
return [bounds.minX, bounds.minY, bounds.maxX, bounds.maxY]
|
|
358
|
+
.map((value) => String(Math.round(value * 10) / 10))
|
|
359
|
+
.join(':')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Computes point bounds.
|
|
364
|
+
* @param {number[][]} points Points.
|
|
365
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
366
|
+
*/
|
|
367
|
+
static #bounds(points) {
|
|
368
|
+
return points.reduce(
|
|
369
|
+
(bounds, point) => ({
|
|
370
|
+
minX: Math.min(bounds.minX, point[0]),
|
|
371
|
+
minY: Math.min(bounds.minY, point[1]),
|
|
372
|
+
maxX: Math.max(bounds.maxX, point[0]),
|
|
373
|
+
maxY: Math.max(bounds.maxY, point[1])
|
|
374
|
+
}),
|
|
375
|
+
{
|
|
376
|
+
minX: Infinity,
|
|
377
|
+
minY: Infinity,
|
|
378
|
+
maxX: -Infinity,
|
|
379
|
+
maxY: -Infinity
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Computes a polygon centroid approximation.
|
|
386
|
+
* @param {number[][]} points Polygon points.
|
|
387
|
+
* @returns {number[]}
|
|
388
|
+
*/
|
|
389
|
+
static #centroid(points) {
|
|
390
|
+
return [
|
|
391
|
+
points.reduce((sum, point) => sum + point[0], 0) / points.length,
|
|
392
|
+
points.reduce((sum, point) => sum + point[1], 0) / points.length
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Computes signed polygon area.
|
|
398
|
+
* @param {number[][]} points Polygon points.
|
|
399
|
+
* @returns {number}
|
|
400
|
+
*/
|
|
401
|
+
static #signedArea(points) {
|
|
402
|
+
let area = 0
|
|
403
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
404
|
+
const current = points[index]
|
|
405
|
+
const next = points[(index + 1) % points.length]
|
|
406
|
+
area += current[0] * next[1] - next[0] * current[1]
|
|
407
|
+
}
|
|
408
|
+
return area / 2
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Tests whether a point lies inside a polygon.
|
|
413
|
+
* @param {number[]} point Candidate point.
|
|
414
|
+
* @param {number[][]} polygon Polygon points.
|
|
415
|
+
* @returns {boolean}
|
|
416
|
+
*/
|
|
417
|
+
static #pointInPolygon(point, polygon) {
|
|
418
|
+
let inside = false
|
|
419
|
+
|
|
420
|
+
for (
|
|
421
|
+
let index = 0, previous = polygon.length - 1;
|
|
422
|
+
index < polygon.length;
|
|
423
|
+
previous = index, index += 1
|
|
424
|
+
) {
|
|
425
|
+
const currentPoint = polygon[index]
|
|
426
|
+
const previousPoint = polygon[previous]
|
|
427
|
+
const crosses =
|
|
428
|
+
currentPoint[1] > point[1] !== previousPoint[1] > point[1]
|
|
429
|
+
const xAtY =
|
|
430
|
+
((previousPoint[0] - currentPoint[0]) *
|
|
431
|
+
(point[1] - currentPoint[1])) /
|
|
432
|
+
(previousPoint[1] - currentPoint[1]) +
|
|
433
|
+
currentPoint[0]
|
|
434
|
+
|
|
435
|
+
if (crosses && point[0] < xAtY) {
|
|
436
|
+
inside = !inside
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return inside
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Returns the first positive finite number.
|
|
445
|
+
* @param {unknown[]} values Candidate values.
|
|
446
|
+
* @returns {number}
|
|
447
|
+
*/
|
|
448
|
+
static #firstPositive(values) {
|
|
449
|
+
for (const value of values) {
|
|
450
|
+
const number = Number(value)
|
|
451
|
+
if (Number.isFinite(number) && number > 0) {
|
|
452
|
+
return number
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return 0
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Normalizes a value to an array.
|
|
461
|
+
* @param {unknown} value Candidate value.
|
|
462
|
+
* @returns {any[]}
|
|
463
|
+
*/
|
|
464
|
+
static #array(value) {
|
|
465
|
+
return Array.isArray(value) ? value : []
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const MIL_TO_MM = 0.0254
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps internal PCB Z-up mesh coordinates into the exported viewer frame.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbAssemblyExportCoordinateFrame {
|
|
7
|
+
/**
|
|
8
|
+
* Converts one internal vertex from mils into exported millimetres.
|
|
9
|
+
* @param {number[]} vertex Internal mesh vertex in mils.
|
|
10
|
+
* @returns {number[]}
|
|
11
|
+
*/
|
|
12
|
+
static vertexMilToMm(vertex) {
|
|
13
|
+
const x = Number(vertex?.[0] || 0)
|
|
14
|
+
const y = Number(vertex?.[1] || 0)
|
|
15
|
+
const z = Number(vertex?.[2] || 0)
|
|
16
|
+
|
|
17
|
+
return [x * MIL_TO_MM, z * MIL_TO_MM, -y * MIL_TO_MM]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks measured progress while PCB assembly meshes are built.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbAssemblyGeometryBuildProgress {
|
|
5
|
+
/** @type {((progress: { value: number, message: string }) => void) | null} */
|
|
6
|
+
#onProgress
|
|
7
|
+
|
|
8
|
+
/** @type {number} */
|
|
9
|
+
#totalUnits
|
|
10
|
+
|
|
11
|
+
/** @type {number} */
|
|
12
|
+
#completedUnits
|
|
13
|
+
|
|
14
|
+
/** @type {number} */
|
|
15
|
+
#lastValue
|
|
16
|
+
|
|
17
|
+
/** @type {number} */
|
|
18
|
+
#startValue
|
|
19
|
+
|
|
20
|
+
/** @type {number} */
|
|
21
|
+
#endValue
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ totalUnits?: number, onProgress?: (progress: { value: number, message: string }) => void, startValue?: number, endValue?: number }} options Progress options.
|
|
25
|
+
*/
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.#onProgress =
|
|
28
|
+
typeof options.onProgress === 'function' ? options.onProgress : null
|
|
29
|
+
this.#totalUnits = Math.max(Number(options.totalUnits || 1), 1)
|
|
30
|
+
this.#completedUnits = 0
|
|
31
|
+
this.#lastValue = -1
|
|
32
|
+
this.#startValue = PcbAssemblyGeometryBuildProgress.#clampPercent(
|
|
33
|
+
options.startValue ?? 10
|
|
34
|
+
)
|
|
35
|
+
this.#endValue = Math.max(
|
|
36
|
+
this.#startValue,
|
|
37
|
+
PcbAssemblyGeometryBuildProgress.#clampPercent(
|
|
38
|
+
options.endValue ?? 75
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a progress tracker sized to the prepared scene detail.
|
|
45
|
+
* @param {{ detail?: object, externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
46
|
+
* @param {((progress: { value: number, message: string }) => void) | undefined} onProgress Progress callback.
|
|
47
|
+
* @param {{ startValue?: number, endValue?: number }} [options] Progress range options.
|
|
48
|
+
* @returns {PcbAssemblyGeometryBuildProgress}
|
|
49
|
+
*/
|
|
50
|
+
static create(sceneDescription, onProgress, options = {}) {
|
|
51
|
+
return new PcbAssemblyGeometryBuildProgress({
|
|
52
|
+
onProgress,
|
|
53
|
+
startValue: options.startValue,
|
|
54
|
+
endValue: options.endValue,
|
|
55
|
+
totalUnits:
|
|
56
|
+
1 +
|
|
57
|
+
PcbAssemblyGeometryBuildProgress.#countCopperUnits(
|
|
58
|
+
sceneDescription?.detail || {}
|
|
59
|
+
) +
|
|
60
|
+
PcbAssemblyGeometryBuildProgress.#countSilkscreenUnits(
|
|
61
|
+
sceneDescription?.detail?.silkscreen || {}
|
|
62
|
+
) +
|
|
63
|
+
PcbAssemblyGeometryBuildProgress.#countComponentUnits(
|
|
64
|
+
sceneDescription
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Advances progress by measured build units.
|
|
71
|
+
* @param {number} units Completed units.
|
|
72
|
+
* @param {string} message Progress message.
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async advance(units, message) {
|
|
76
|
+
this.#completedUnits += Math.max(Number(units || 0), 0)
|
|
77
|
+
await this.#emit(
|
|
78
|
+
Math.min(
|
|
79
|
+
this.#scaledValue(this.#completedUnits / this.#totalUnits),
|
|
80
|
+
this.#endValue - 1
|
|
81
|
+
),
|
|
82
|
+
message
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Marks geometry building complete.
|
|
88
|
+
* @param {string} message Completion message.
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async finish(message) {
|
|
92
|
+
this.#completedUnits = this.#totalUnits
|
|
93
|
+
await this.#emit(this.#endValue, message)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Emits progress when the visible integer value changes.
|
|
98
|
+
* @param {number} value Progress value.
|
|
99
|
+
* @param {string} message Progress message.
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async #emit(value, message) {
|
|
103
|
+
if (!this.#onProgress || value === this.#lastValue) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.#lastValue = value
|
|
108
|
+
this.#onProgress({
|
|
109
|
+
value,
|
|
110
|
+
message
|
|
111
|
+
})
|
|
112
|
+
await PcbAssemblyGeometryBuildProgress.#yieldToBrowser()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Maps a unit completion ratio into the configured progress range.
|
|
117
|
+
* @param {number} ratio Completion ratio.
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
#scaledValue(ratio) {
|
|
121
|
+
const clampedRatio = Math.max(Math.min(Number(ratio || 0), 1), 0)
|
|
122
|
+
return Math.round(
|
|
123
|
+
this.#startValue +
|
|
124
|
+
clampedRatio * (this.#endValue - this.#startValue)
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Counts measured copper work units.
|
|
130
|
+
* @param {object} detail Scene detail.
|
|
131
|
+
* @returns {number}
|
|
132
|
+
*/
|
|
133
|
+
static #countCopperUnits(detail) {
|
|
134
|
+
return (
|
|
135
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.tracks).length +
|
|
136
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.arcs).length +
|
|
137
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.fills).length +
|
|
138
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.polygons).length +
|
|
139
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.pads).length * 2 +
|
|
140
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.vias).length +
|
|
141
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.copperTexts).length
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Counts measured silkscreen work units.
|
|
147
|
+
* @param {object} silkscreen Scene silkscreen detail.
|
|
148
|
+
* @returns {number}
|
|
149
|
+
*/
|
|
150
|
+
static #countSilkscreenUnits(silkscreen) {
|
|
151
|
+
return ['top', 'bottom'].reduce((total, side) => {
|
|
152
|
+
const detail = silkscreen?.[side] || {}
|
|
153
|
+
return (
|
|
154
|
+
total +
|
|
155
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.tracks).length +
|
|
156
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.arcs).length +
|
|
157
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.fills).length +
|
|
158
|
+
PcbAssemblyGeometryBuildProgress.#array(detail.texts).length
|
|
159
|
+
)
|
|
160
|
+
}, 0)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Counts component model loading work units.
|
|
165
|
+
* @param {{ externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
166
|
+
* @returns {number}
|
|
167
|
+
*/
|
|
168
|
+
static #countComponentUnits(sceneDescription) {
|
|
169
|
+
return PcbAssemblyGeometryBuildProgress.#array(
|
|
170
|
+
sceneDescription?.externalPlacements
|
|
171
|
+
).filter((placement) => placement?.externalModel).length
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Normalizes a value to an array.
|
|
176
|
+
* @param {unknown} value Candidate value.
|
|
177
|
+
* @returns {any[]}
|
|
178
|
+
*/
|
|
179
|
+
static #array(value) {
|
|
180
|
+
return Array.isArray(value) ? value : []
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Clamps a progress percentage.
|
|
185
|
+
* @param {number} value Candidate percentage.
|
|
186
|
+
* @returns {number}
|
|
187
|
+
*/
|
|
188
|
+
static #clampPercent(value) {
|
|
189
|
+
return Math.max(Math.min(Number(value || 0), 100), 0)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Yields control so browsers can paint the progress dialog.
|
|
194
|
+
* @returns {Promise<void>}
|
|
195
|
+
*/
|
|
196
|
+
static #yieldToBrowser() {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
globalThis.setTimeout?.(resolve, 0) || resolve()
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|