pcb-scene3d-viewer 1.1.21 → 1.1.31
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 +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- package/src/scene3d.mjs +3 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
2
|
+
import { PcbAssemblyFillRingNormalizer } from './PcbAssemblyFillRingNormalizer.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves filled PCB detail primitives into exportable polygon loops.
|
|
6
|
+
*/
|
|
7
|
+
export class PcbAssemblyFillGeometryResolver {
|
|
8
|
+
/**
|
|
9
|
+
* Resolves one or more independent fill islands.
|
|
10
|
+
* @param {object} source Filled primitive.
|
|
11
|
+
* @returns {{ outer: number[][], holes: number[][][] }[]}
|
|
12
|
+
*/
|
|
13
|
+
static resolveAll(source) {
|
|
14
|
+
return PcbAssemblyFillGeometryResolver.inspect(source).loopSets
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the outer loop and inner cutout loops for one fill primitive.
|
|
19
|
+
* @param {object} source Filled primitive.
|
|
20
|
+
* @returns {{ outer: number[][], holes: number[][][] }}
|
|
21
|
+
*/
|
|
22
|
+
static resolve(source) {
|
|
23
|
+
const report = PcbAssemblyFillGeometryResolver.inspect(source)
|
|
24
|
+
return report.loopSets[0] || { outer: [], holes: [] }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves fill geometry and reports discarded saved-fill rings.
|
|
29
|
+
* @param {object} source Filled primitive.
|
|
30
|
+
* @returns {{ loopSets: { outer: number[][], holes: number[][][] }[], diagnostics: object[] }}
|
|
31
|
+
*/
|
|
32
|
+
static inspect(source) {
|
|
33
|
+
const ringReport =
|
|
34
|
+
PcbAssemblyFillGeometryResolver.#inspectRingGeometry(source)
|
|
35
|
+
if (ringReport.hasRingGeometry) {
|
|
36
|
+
return {
|
|
37
|
+
loopSets: ringReport.loopSets,
|
|
38
|
+
diagnostics: ringReport.diagnostics
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
loopSets: [PcbAssemblyFillGeometryResolver.#resolveLegacy(source)],
|
|
44
|
+
diagnostics: []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves non-B-Rep fill geometry.
|
|
50
|
+
* @param {object} source Filled primitive.
|
|
51
|
+
* @returns {{ outer: number[][], holes: number[][][] }}
|
|
52
|
+
*/
|
|
53
|
+
static #resolveLegacy(source) {
|
|
54
|
+
const contours = PcbAssemblyFillGeometryResolver.#contours(source)
|
|
55
|
+
if (contours.length) {
|
|
56
|
+
return {
|
|
57
|
+
outer: contours[0],
|
|
58
|
+
holes: contours.slice(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
outer: PcbAssemblyFillGeometryResolver.#outerLoop(source),
|
|
64
|
+
holes: PcbAssemblyFillGeometryResolver.#holeLoops(source)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves ring-style geometry and diagnostics when present.
|
|
70
|
+
* @param {object} source Filled primitive.
|
|
71
|
+
* @returns {{ hasRingGeometry: boolean, loopSets: { outer: number[][], holes: number[][][] }[], diagnostics: object[] }}
|
|
72
|
+
*/
|
|
73
|
+
static #inspectRingGeometry(source) {
|
|
74
|
+
const shapes = PcbAssemblyFillGeometryResolver.#ringShapes(source)
|
|
75
|
+
if (shapes.length) {
|
|
76
|
+
return PcbAssemblyFillGeometryResolver.#inspectRingShapes(shapes)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const shape = source?.brep_shape || source?.brepShape || null
|
|
80
|
+
if (!shape) {
|
|
81
|
+
return {
|
|
82
|
+
hasRingGeometry: false,
|
|
83
|
+
loopSets: [],
|
|
84
|
+
diagnostics: []
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return PcbAssemblyFillGeometryResolver.#inspectRingShapes([shape])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolves ring-style geometry from shape objects.
|
|
93
|
+
* @param {object[]} shapes Ring shape objects.
|
|
94
|
+
* @returns {{ hasRingGeometry: boolean, loopSets: { outer: number[][], holes: number[][][] }[], diagnostics: object[] }}
|
|
95
|
+
*/
|
|
96
|
+
static #inspectRingShapes(shapes) {
|
|
97
|
+
const diagnostics = []
|
|
98
|
+
const loopSets = shapes
|
|
99
|
+
.map((shape, shapeIndex) =>
|
|
100
|
+
PcbAssemblyFillGeometryResolver.#ringGeometryFromShape(
|
|
101
|
+
shape,
|
|
102
|
+
shapeIndex,
|
|
103
|
+
diagnostics
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
.filter((geometry) =>
|
|
107
|
+
PcbAssemblyFillGeometryResolver.#isValidLoop(geometry.outer)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
hasRingGeometry: true,
|
|
112
|
+
loopSets,
|
|
113
|
+
diagnostics
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolves ring-style geometry from one shape object.
|
|
119
|
+
* @param {object} shape Ring shape object.
|
|
120
|
+
* @param {number} shapeIndex Shape index.
|
|
121
|
+
* @param {object[]} diagnostics Diagnostic accumulator.
|
|
122
|
+
* @returns {{ outer: number[][], holes: number[][][] }}
|
|
123
|
+
*/
|
|
124
|
+
static #ringGeometryFromShape(shape, shapeIndex, diagnostics) {
|
|
125
|
+
const outer = PcbAssemblyFillGeometryResolver.#normalizedRingLoop(
|
|
126
|
+
shape.outer_ring ||
|
|
127
|
+
shape.outerRing ||
|
|
128
|
+
shape.outer ||
|
|
129
|
+
shape.outer_loop ||
|
|
130
|
+
shape.outerLoop,
|
|
131
|
+
{
|
|
132
|
+
role: 'outer',
|
|
133
|
+
shapeIndex,
|
|
134
|
+
ringIndex: 0,
|
|
135
|
+
winding: 'positive'
|
|
136
|
+
},
|
|
137
|
+
diagnostics
|
|
138
|
+
)
|
|
139
|
+
return {
|
|
140
|
+
outer,
|
|
141
|
+
holes: PcbAssemblyFillGeometryResolver.#array(
|
|
142
|
+
shape.inner_rings ||
|
|
143
|
+
shape.innerRings ||
|
|
144
|
+
shape.holes ||
|
|
145
|
+
shape.inner ||
|
|
146
|
+
shape.cutouts
|
|
147
|
+
)
|
|
148
|
+
.map((ring, ringIndex) =>
|
|
149
|
+
PcbAssemblyFillGeometryResolver.#normalizedRingLoop(
|
|
150
|
+
ring,
|
|
151
|
+
{
|
|
152
|
+
role: 'hole',
|
|
153
|
+
shapeIndex,
|
|
154
|
+
ringIndex,
|
|
155
|
+
winding: 'negative'
|
|
156
|
+
},
|
|
157
|
+
diagnostics
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
.filter((loop) =>
|
|
161
|
+
PcbAssemblyFillGeometryResolver.#isValidLoop(loop)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolves independent ring-shape islands.
|
|
168
|
+
* @param {object} source Filled primitive.
|
|
169
|
+
* @returns {object[]}
|
|
170
|
+
*/
|
|
171
|
+
static #ringShapes(source) {
|
|
172
|
+
return PcbAssemblyFillGeometryResolver.#array(
|
|
173
|
+
source?.brep_shapes ||
|
|
174
|
+
source?.brepShapes ||
|
|
175
|
+
source?.brep_shape_array ||
|
|
176
|
+
source?.brepShapeArray
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolves a ring object into normalized loop geometry.
|
|
182
|
+
* @param {object | object[]} ring Ring source.
|
|
183
|
+
* @param {{ role: string, shapeIndex: number, ringIndex: number, winding: 'positive' | 'negative' }} options Normalization options.
|
|
184
|
+
* @param {object[]} diagnostics Diagnostic accumulator.
|
|
185
|
+
* @returns {number[][]}
|
|
186
|
+
*/
|
|
187
|
+
static #normalizedRingLoop(ring, options, diagnostics) {
|
|
188
|
+
const report = PcbAssemblyFillRingNormalizer.normalize(
|
|
189
|
+
PcbAssemblyFillGeometryResolver.#expandBulgedPoints(
|
|
190
|
+
ring?.vertices ||
|
|
191
|
+
ring?.cwVertices ||
|
|
192
|
+
ring?.cw_vertices ||
|
|
193
|
+
ring?.points ||
|
|
194
|
+
ring ||
|
|
195
|
+
[]
|
|
196
|
+
),
|
|
197
|
+
options
|
|
198
|
+
)
|
|
199
|
+
if (report.diagnostic) {
|
|
200
|
+
diagnostics.push(report.diagnostic)
|
|
201
|
+
}
|
|
202
|
+
return report.loop
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Samples point-level bulge arcs in one ring.
|
|
207
|
+
* @param {unknown[]} points Candidate ring points.
|
|
208
|
+
* @returns {number[][]}
|
|
209
|
+
*/
|
|
210
|
+
static #expandBulgedPoints(points) {
|
|
211
|
+
const vertices = PcbAssemblyFillGeometryResolver.#array(points).map(
|
|
212
|
+
(point) => ({
|
|
213
|
+
point: PcbAssemblyFillGeometryResolver.#point(point),
|
|
214
|
+
bulge: PcbAssemblyFillGeometryResolver.#pointBulge(point)
|
|
215
|
+
})
|
|
216
|
+
)
|
|
217
|
+
const loop =
|
|
218
|
+
PcbAssemblyFillGeometryResolver.#removeClosingVertex(vertices)
|
|
219
|
+
const expanded = []
|
|
220
|
+
|
|
221
|
+
for (let index = 0; index < loop.length; index += 1) {
|
|
222
|
+
const current = loop[index]
|
|
223
|
+
const next = loop[(index + 1) % loop.length]
|
|
224
|
+
if (!current?.point) {
|
|
225
|
+
expanded.push([NaN, NaN])
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
expanded.push(current.point)
|
|
230
|
+
if (next?.point) {
|
|
231
|
+
expanded.push(
|
|
232
|
+
...PcbAssemblyFillGeometryResolver.#bulgeArcPoints(
|
|
233
|
+
current.point,
|
|
234
|
+
next.point,
|
|
235
|
+
current.bulge
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return expanded
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Removes an explicit closing vertex while preserving per-segment bulge data.
|
|
246
|
+
* @param {{ point: number[] | null, bulge: number }[]} vertices Ring vertices.
|
|
247
|
+
* @returns {{ point: number[] | null, bulge: number }[]}
|
|
248
|
+
*/
|
|
249
|
+
static #removeClosingVertex(vertices) {
|
|
250
|
+
const first = vertices[0]?.point
|
|
251
|
+
const last = vertices[vertices.length - 1]?.point
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
first &&
|
|
255
|
+
last &&
|
|
256
|
+
Math.abs(first[0] - last[0]) < 0.001 &&
|
|
257
|
+
Math.abs(first[1] - last[1]) < 0.001
|
|
258
|
+
) {
|
|
259
|
+
return vertices.slice(0, -1)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return vertices
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolves a bulge value from object or tuple point forms.
|
|
267
|
+
* @param {unknown} point Candidate point.
|
|
268
|
+
* @returns {number}
|
|
269
|
+
*/
|
|
270
|
+
static #pointBulge(point) {
|
|
271
|
+
const value = Array.isArray(point) ? point[2] : point?.bulge
|
|
272
|
+
const bulge = Number(value || 0)
|
|
273
|
+
return Number.isFinite(bulge) ? bulge : 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Samples the arc between two points from a polyline bulge value.
|
|
278
|
+
* @param {number[]} start Arc start point.
|
|
279
|
+
* @param {number[]} end Arc end point.
|
|
280
|
+
* @param {number} bulge Polyline bulge value.
|
|
281
|
+
* @returns {number[][]}
|
|
282
|
+
*/
|
|
283
|
+
static #bulgeArcPoints(start, end, bulge) {
|
|
284
|
+
if (Math.abs(bulge) <= 0.001) {
|
|
285
|
+
return []
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const dx = end[0] - start[0]
|
|
289
|
+
const dy = end[1] - start[1]
|
|
290
|
+
const chordLength = Math.hypot(dx, dy)
|
|
291
|
+
if (chordLength <= 0.001) {
|
|
292
|
+
return []
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const sweep = 4 * Math.atan(bulge)
|
|
296
|
+
const radius = chordLength / (2 * Math.sin(Math.abs(sweep) / 2))
|
|
297
|
+
const midpoint = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
|
|
298
|
+
const centerOffset = (chordLength * (1 - bulge * bulge)) / (4 * bulge)
|
|
299
|
+
const center = [
|
|
300
|
+
midpoint[0] - (dy / chordLength) * centerOffset,
|
|
301
|
+
midpoint[1] + (dx / chordLength) * centerOffset
|
|
302
|
+
]
|
|
303
|
+
const startAngle = Math.atan2(
|
|
304
|
+
start[1] - center[1],
|
|
305
|
+
start[0] - center[0]
|
|
306
|
+
)
|
|
307
|
+
const segments = Math.max(
|
|
308
|
+
Math.ceil(Math.abs(sweep) / (Math.PI / 16)),
|
|
309
|
+
4
|
|
310
|
+
)
|
|
311
|
+
const points = []
|
|
312
|
+
|
|
313
|
+
for (let index = 1; index < segments; index += 1) {
|
|
314
|
+
const angle = startAngle + (sweep * index) / segments
|
|
315
|
+
points.push([
|
|
316
|
+
center[0] + Math.cos(angle) * radius,
|
|
317
|
+
center[1] + Math.sin(angle) * radius
|
|
318
|
+
])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return points
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resolves contour loops from a multi-contour fill primitive.
|
|
326
|
+
* @param {object} source Filled primitive.
|
|
327
|
+
* @returns {number[][][]}
|
|
328
|
+
*/
|
|
329
|
+
static #contours(source) {
|
|
330
|
+
return PcbAssemblyFillGeometryResolver.#array(source?.contours)
|
|
331
|
+
.map((contour) =>
|
|
332
|
+
PcbAssemblyFillGeometryResolver.#pointOrSegmentLoop(contour)
|
|
333
|
+
)
|
|
334
|
+
.filter((loop) =>
|
|
335
|
+
PcbAssemblyFillGeometryResolver.#isValidLoop(loop)
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Resolves the primary fill outline.
|
|
341
|
+
* @param {object} source Filled primitive.
|
|
342
|
+
* @returns {number[][]}
|
|
343
|
+
*/
|
|
344
|
+
static #outerLoop(source) {
|
|
345
|
+
const explicitLoop = PcbAssemblyFillGeometryResolver.#pointLoop(
|
|
346
|
+
source?.points || source?.vertices || source?.polygon || []
|
|
347
|
+
)
|
|
348
|
+
if (explicitLoop.length >= 3) {
|
|
349
|
+
return explicitLoop
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return PcbAssemblyFillGeometryResolver.#rectangleLoop(source)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolves authored cutout loops.
|
|
357
|
+
* @param {object} source Filled primitive.
|
|
358
|
+
* @returns {number[][][]}
|
|
359
|
+
*/
|
|
360
|
+
static #holeLoops(source) {
|
|
361
|
+
return PcbAssemblyFillGeometryResolver.#array(source?.holes)
|
|
362
|
+
.map((hole) =>
|
|
363
|
+
PcbAssemblyFillGeometryResolver.#pointOrSegmentLoop(hole)
|
|
364
|
+
)
|
|
365
|
+
.filter((loop) =>
|
|
366
|
+
PcbAssemblyFillGeometryResolver.#isValidLoop(loop)
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Resolves a contour that may contain points or segment records.
|
|
372
|
+
* @param {unknown[]} entries Contour entries.
|
|
373
|
+
* @returns {number[][]}
|
|
374
|
+
*/
|
|
375
|
+
static #pointOrSegmentLoop(entries) {
|
|
376
|
+
const list = PcbAssemblyFillGeometryResolver.#array(entries)
|
|
377
|
+
if (!list.length) {
|
|
378
|
+
return []
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const pointLoop = PcbAssemblyFillGeometryResolver.#pointLoop(list)
|
|
382
|
+
if (pointLoop.length >= 3) {
|
|
383
|
+
return pointLoop
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return PcbAssemblyFillGeometryResolver.#segmentLoop(list)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolves a point loop from object or tuple points.
|
|
391
|
+
* @param {unknown[]} points Candidate points.
|
|
392
|
+
* @returns {number[][]}
|
|
393
|
+
*/
|
|
394
|
+
static #pointLoop(points) {
|
|
395
|
+
const loop = PcbAssemblyFillGeometryResolver.#array(points)
|
|
396
|
+
.map((point) => PcbAssemblyFillGeometryResolver.#point(point))
|
|
397
|
+
.filter(Boolean)
|
|
398
|
+
|
|
399
|
+
return PcbAssemblyMeshUtils.cleanLoop(loop)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Resolves a point from an object or tuple.
|
|
404
|
+
* @param {unknown} point Candidate point.
|
|
405
|
+
* @returns {number[] | null}
|
|
406
|
+
*/
|
|
407
|
+
static #point(point) {
|
|
408
|
+
const x = Array.isArray(point) ? point[0] : point?.x
|
|
409
|
+
const y = Array.isArray(point) ? point[1] : point?.y
|
|
410
|
+
const normalized = [Number(x), Number(y)]
|
|
411
|
+
|
|
412
|
+
return normalized.every(Number.isFinite) ? normalized : null
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Resolves an ordered loop from line or arc segment records.
|
|
417
|
+
* @param {object[]} segments Segment records.
|
|
418
|
+
* @returns {number[][]}
|
|
419
|
+
*/
|
|
420
|
+
static #segmentLoop(segments) {
|
|
421
|
+
const points = []
|
|
422
|
+
|
|
423
|
+
segments.forEach((segment, index) => {
|
|
424
|
+
if (index === 0) {
|
|
425
|
+
const start =
|
|
426
|
+
PcbAssemblyFillGeometryResolver.#segmentStart(segment)
|
|
427
|
+
if (start) points.push(start)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
points.push(
|
|
431
|
+
...PcbAssemblyFillGeometryResolver.#segmentTailPoints(segment)
|
|
432
|
+
)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
return PcbAssemblyMeshUtils.cleanLoop(points)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Resolves the start point for one segment.
|
|
440
|
+
* @param {object} segment Segment record.
|
|
441
|
+
* @returns {number[] | null}
|
|
442
|
+
*/
|
|
443
|
+
static #segmentStart(segment) {
|
|
444
|
+
return (
|
|
445
|
+
PcbAssemblyFillGeometryResolver.#fieldPoint(segment, 'x1', 'y1') ||
|
|
446
|
+
PcbAssemblyFillGeometryResolver.#fieldPoint(
|
|
447
|
+
segment,
|
|
448
|
+
'startX',
|
|
449
|
+
'startY'
|
|
450
|
+
) ||
|
|
451
|
+
PcbAssemblyFillGeometryResolver.#point(segment?.start)
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves the end or sampled arc points for one segment.
|
|
457
|
+
* @param {object} segment Segment record.
|
|
458
|
+
* @returns {number[][]}
|
|
459
|
+
*/
|
|
460
|
+
static #segmentTailPoints(segment) {
|
|
461
|
+
const isArc =
|
|
462
|
+
String(segment?.type || '').toLowerCase() === 'arc' ||
|
|
463
|
+
Number.isFinite(Number(segment?.radius))
|
|
464
|
+
if (isArc) {
|
|
465
|
+
return PcbAssemblyFillGeometryResolver.#arcPoints(segment)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const end =
|
|
469
|
+
PcbAssemblyFillGeometryResolver.#fieldPoint(segment, 'x2', 'y2') ||
|
|
470
|
+
PcbAssemblyFillGeometryResolver.#fieldPoint(
|
|
471
|
+
segment,
|
|
472
|
+
'endX',
|
|
473
|
+
'endY'
|
|
474
|
+
) ||
|
|
475
|
+
PcbAssemblyFillGeometryResolver.#point(segment?.end)
|
|
476
|
+
|
|
477
|
+
return end ? [end] : []
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Samples an arc segment into tail points.
|
|
482
|
+
* @param {object} arc Arc segment.
|
|
483
|
+
* @returns {number[][]}
|
|
484
|
+
*/
|
|
485
|
+
static #arcPoints(arc) {
|
|
486
|
+
const center = PcbAssemblyFillGeometryResolver.#arcCenter(arc)
|
|
487
|
+
const radius = Number(arc?.radius || 0)
|
|
488
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
489
|
+
return []
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const start = Number(arc?.startAngle || 0)
|
|
493
|
+
const sweep = PcbAssemblyMeshUtils.resolveSweep(arc)
|
|
494
|
+
const segments = Math.max(Math.ceil(Math.abs(sweep) / 8), 4)
|
|
495
|
+
const points = []
|
|
496
|
+
|
|
497
|
+
for (let index = 1; index <= segments; index += 1) {
|
|
498
|
+
const angle = ((start + (sweep * index) / segments) * Math.PI) / 180
|
|
499
|
+
points.push([
|
|
500
|
+
center.x + Math.cos(angle) * radius,
|
|
501
|
+
center.y + Math.sin(angle) * radius
|
|
502
|
+
])
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return points
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Resolves the center point for an arc segment.
|
|
510
|
+
* @param {object} arc Arc segment.
|
|
511
|
+
* @returns {{ x: number, y: number }}
|
|
512
|
+
*/
|
|
513
|
+
static #arcCenter(arc) {
|
|
514
|
+
return {
|
|
515
|
+
x: Number(arc?.x ?? arc?.cx ?? arc?.centerX ?? arc?.center?.x ?? 0),
|
|
516
|
+
y: Number(arc?.y ?? arc?.cy ?? arc?.centerY ?? arc?.center?.y ?? 0)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Resolves a rectangle fill loop from corner fields.
|
|
522
|
+
* @param {object} source Fill primitive.
|
|
523
|
+
* @returns {number[][]}
|
|
524
|
+
*/
|
|
525
|
+
static #rectangleLoop(source) {
|
|
526
|
+
const x1 = Number(source?.x1 ?? source?.left)
|
|
527
|
+
const y1 = Number(source?.y1 ?? source?.top)
|
|
528
|
+
const x2 = Number(source?.x2 ?? source?.right)
|
|
529
|
+
const y2 = Number(source?.y2 ?? source?.bottom)
|
|
530
|
+
if (![x1, y1, x2, y2].every(Number.isFinite)) {
|
|
531
|
+
return []
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const minX = Math.min(x1, x2)
|
|
535
|
+
const maxX = Math.max(x1, x2)
|
|
536
|
+
const minY = Math.min(y1, y2)
|
|
537
|
+
const maxY = Math.max(y1, y2)
|
|
538
|
+
if (maxX - minX <= 0 || maxY - minY <= 0) {
|
|
539
|
+
return []
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return [
|
|
543
|
+
[minX, minY],
|
|
544
|
+
[maxX, minY],
|
|
545
|
+
[maxX, maxY],
|
|
546
|
+
[minX, maxY]
|
|
547
|
+
]
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Resolves a finite point from paired fields.
|
|
552
|
+
* @param {object} source Source object.
|
|
553
|
+
* @param {string} xField X field.
|
|
554
|
+
* @param {string} yField Y field.
|
|
555
|
+
* @returns {number[] | null}
|
|
556
|
+
*/
|
|
557
|
+
static #fieldPoint(source, xField, yField) {
|
|
558
|
+
const point = [Number(source?.[xField]), Number(source?.[yField])]
|
|
559
|
+
return point.every(Number.isFinite) ? point : null
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Checks whether one loop has enough non-collinear area.
|
|
564
|
+
* @param {number[][]} loop Candidate loop.
|
|
565
|
+
* @returns {boolean}
|
|
566
|
+
*/
|
|
567
|
+
static #isValidLoop(loop) {
|
|
568
|
+
return PcbAssemblyFillRingNormalizer.isValidLoop(loop)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Normalizes a value to an array.
|
|
573
|
+
* @param {unknown} value Candidate value.
|
|
574
|
+
* @returns {any[]}
|
|
575
|
+
*/
|
|
576
|
+
static #array(value) {
|
|
577
|
+
return Array.isArray(value) ? value : []
|
|
578
|
+
}
|
|
579
|
+
}
|