pcb-scene3d-viewer 1.1.49 → 1.2.0
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 +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} PcbScene3dAabbBounds
|
|
3
|
+
* @property {number} minX
|
|
4
|
+
* @property {number} maxX
|
|
5
|
+
* @property {number} minY
|
|
6
|
+
* @property {number} maxY
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} PcbScene3dAabbEntry
|
|
11
|
+
* @property {*} item
|
|
12
|
+
* @property {PcbScene3dAabbBounds} bounds
|
|
13
|
+
* @property {number} sourceIndex
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} PcbScene3dAabbNode
|
|
18
|
+
* @property {PcbScene3dAabbBounds} bounds
|
|
19
|
+
* @property {number} start
|
|
20
|
+
* @property {number} end
|
|
21
|
+
* @property {PcbScene3dAabbNode | null} left
|
|
22
|
+
* @property {PcbScene3dAabbNode | null} right
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Immutable broad-phase index for axis-aligned two-dimensional bounds.
|
|
27
|
+
*/
|
|
28
|
+
export class PcbScene3dAabbIndex {
|
|
29
|
+
static #DEFAULT_LEAF_SIZE = 12
|
|
30
|
+
|
|
31
|
+
/** @type {PcbScene3dAabbEntry[]} */
|
|
32
|
+
#entries
|
|
33
|
+
|
|
34
|
+
/** @type {PcbScene3dAabbEntry[]} */
|
|
35
|
+
#overflow
|
|
36
|
+
|
|
37
|
+
/** @type {PcbScene3dAabbNode | null} */
|
|
38
|
+
#root
|
|
39
|
+
|
|
40
|
+
/** @type {number} */
|
|
41
|
+
#leafSize
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds an index without mutating the source items or their bounds.
|
|
45
|
+
* @param {Iterable<*>} items
|
|
46
|
+
* @param {{ resolveBounds?: (item: *, index: number) => PcbScene3dAabbBounds, resolveSourceIndex?: (item: *, index: number) => number, leafSize?: number }} [options]
|
|
47
|
+
*/
|
|
48
|
+
constructor(items, options = {}) {
|
|
49
|
+
const resolveBounds =
|
|
50
|
+
options.resolveBounds || PcbScene3dAabbIndex.#defaultResolveBounds
|
|
51
|
+
const resolveSourceIndex =
|
|
52
|
+
options.resolveSourceIndex ||
|
|
53
|
+
PcbScene3dAabbIndex.#defaultResolveSourceIndex
|
|
54
|
+
const requestedLeafSize =
|
|
55
|
+
options.leafSize ?? PcbScene3dAabbIndex.#DEFAULT_LEAF_SIZE
|
|
56
|
+
|
|
57
|
+
this.#entries = []
|
|
58
|
+
this.#overflow = []
|
|
59
|
+
this.#leafSize =
|
|
60
|
+
Number.isFinite(requestedLeafSize) && requestedLeafSize > 0
|
|
61
|
+
? Math.max(1, Math.floor(requestedLeafSize))
|
|
62
|
+
: PcbScene3dAabbIndex.#DEFAULT_LEAF_SIZE
|
|
63
|
+
|
|
64
|
+
const sourceItems = Array.from(items || [])
|
|
65
|
+
for (let index = 0; index < sourceItems.length; index += 1) {
|
|
66
|
+
const item = sourceItems[index]
|
|
67
|
+
const resolvedBounds = resolveBounds(item, index)
|
|
68
|
+
const bounds = {
|
|
69
|
+
minX: resolvedBounds?.minX,
|
|
70
|
+
maxX: resolvedBounds?.maxX,
|
|
71
|
+
minY: resolvedBounds?.minY,
|
|
72
|
+
maxY: resolvedBounds?.maxY
|
|
73
|
+
}
|
|
74
|
+
const entry = {
|
|
75
|
+
item,
|
|
76
|
+
bounds,
|
|
77
|
+
sourceIndex: resolveSourceIndex(item, index)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (PcbScene3dAabbIndex.#isFiniteBounds(bounds)) {
|
|
81
|
+
this.#entries.push(entry)
|
|
82
|
+
} else {
|
|
83
|
+
this.#overflow.push(entry)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.#root = this.#buildTree(0, this.#entries.length)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns items whose bounds overlap the requested bounds.
|
|
92
|
+
* @param {PcbScene3dAabbBounds} bounds
|
|
93
|
+
* @param {{ epsilon?: number, stable?: boolean }} [options]
|
|
94
|
+
* @returns {*[]}
|
|
95
|
+
*/
|
|
96
|
+
query(bounds, options = {}) {
|
|
97
|
+
const result = []
|
|
98
|
+
return this.queryInto(bounds, result, options)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Appends items whose bounds overlap the requested bounds to a target.
|
|
103
|
+
* @param {PcbScene3dAabbBounds} bounds
|
|
104
|
+
* @param {*[]} target
|
|
105
|
+
* @param {{ epsilon?: number, stable?: boolean }} [options]
|
|
106
|
+
* @returns {*[]}
|
|
107
|
+
*/
|
|
108
|
+
queryInto(bounds, target, options = {}) {
|
|
109
|
+
const epsilon = options.epsilon ?? 0
|
|
110
|
+
const stable = options.stable === true
|
|
111
|
+
const includeAll = !PcbScene3dAabbIndex.#isFiniteBounds(bounds)
|
|
112
|
+
const matches = stable ? [] : target
|
|
113
|
+
|
|
114
|
+
this.#collectTreeMatches(
|
|
115
|
+
this.#root,
|
|
116
|
+
bounds,
|
|
117
|
+
epsilon,
|
|
118
|
+
includeAll,
|
|
119
|
+
stable,
|
|
120
|
+
matches
|
|
121
|
+
)
|
|
122
|
+
this.#collectOverflowMatches(
|
|
123
|
+
bounds,
|
|
124
|
+
epsilon,
|
|
125
|
+
includeAll,
|
|
126
|
+
stable,
|
|
127
|
+
matches
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (stable) {
|
|
131
|
+
matches.sort(PcbScene3dAabbIndex.#compareSourceIndexes)
|
|
132
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
133
|
+
target.push(matches[index].item)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return target
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds a median-split tree for an entry range.
|
|
142
|
+
* @param {number} start Inclusive entry offset.
|
|
143
|
+
* @param {number} end Exclusive entry offset.
|
|
144
|
+
* @returns {PcbScene3dAabbNode | null}
|
|
145
|
+
*/
|
|
146
|
+
#buildTree(start, end) {
|
|
147
|
+
if (start >= end) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const bounds = this.#resolveRangeBounds(start, end)
|
|
152
|
+
const node = {
|
|
153
|
+
bounds,
|
|
154
|
+
start,
|
|
155
|
+
end,
|
|
156
|
+
left: null,
|
|
157
|
+
right: null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (end - start <= this.#leafSize) {
|
|
161
|
+
return node
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const width = bounds.maxX - bounds.minX
|
|
165
|
+
const height = bounds.maxY - bounds.minY
|
|
166
|
+
const axis = width >= height ? 'x' : 'y'
|
|
167
|
+
const medianIndex = start + Math.floor((end - start) / 2)
|
|
168
|
+
|
|
169
|
+
this.#quickselect(start, end - 1, medianIndex, axis)
|
|
170
|
+
node.left = this.#buildTree(start, medianIndex)
|
|
171
|
+
node.right = this.#buildTree(medianIndex, end)
|
|
172
|
+
return node
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolves aggregate bounds for a non-empty entry range.
|
|
177
|
+
* @param {number} start Inclusive entry offset.
|
|
178
|
+
* @param {number} end Exclusive entry offset.
|
|
179
|
+
* @returns {PcbScene3dAabbBounds}
|
|
180
|
+
*/
|
|
181
|
+
#resolveRangeBounds(start, end) {
|
|
182
|
+
const first = this.#entries[start].bounds
|
|
183
|
+
const bounds = {
|
|
184
|
+
minX: first.minX,
|
|
185
|
+
maxX: first.maxX,
|
|
186
|
+
minY: first.minY,
|
|
187
|
+
maxY: first.maxY
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
191
|
+
const entryBounds = this.#entries[index].bounds
|
|
192
|
+
bounds.minX = Math.min(bounds.minX, entryBounds.minX)
|
|
193
|
+
bounds.maxX = Math.max(bounds.maxX, entryBounds.maxX)
|
|
194
|
+
bounds.minY = Math.min(bounds.minY, entryBounds.minY)
|
|
195
|
+
bounds.maxY = Math.max(bounds.maxY, entryBounds.maxY)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return bounds
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Places the requested entry at its sorted position along one axis.
|
|
203
|
+
* @param {number} left Inclusive lower entry offset.
|
|
204
|
+
* @param {number} right Inclusive upper entry offset.
|
|
205
|
+
* @param {number} targetIndex Desired sorted entry offset.
|
|
206
|
+
* @param {'x' | 'y'} axis Split axis.
|
|
207
|
+
* @returns {void}
|
|
208
|
+
*/
|
|
209
|
+
#quickselect(left, right, targetIndex, axis) {
|
|
210
|
+
let lowerBound = left
|
|
211
|
+
let upperBound = right
|
|
212
|
+
|
|
213
|
+
while (lowerBound < upperBound) {
|
|
214
|
+
const pivot =
|
|
215
|
+
this.#entries[
|
|
216
|
+
lowerBound + Math.floor((upperBound - lowerBound) / 2)
|
|
217
|
+
]
|
|
218
|
+
const partition = this.#partition(
|
|
219
|
+
lowerBound,
|
|
220
|
+
upperBound,
|
|
221
|
+
pivot,
|
|
222
|
+
axis
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if (targetIndex < partition.lower) {
|
|
226
|
+
upperBound = partition.lower - 1
|
|
227
|
+
} else if (targetIndex > partition.upper) {
|
|
228
|
+
lowerBound = partition.upper + 1
|
|
229
|
+
} else {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Partitions an entry range around one pivot coordinate in place.
|
|
237
|
+
* @param {number} left Inclusive lower entry offset.
|
|
238
|
+
* @param {number} right Inclusive upper entry offset.
|
|
239
|
+
* @param {PcbScene3dAabbEntry} pivot Pivot entry.
|
|
240
|
+
* @param {'x' | 'y'} axis Split axis.
|
|
241
|
+
* @returns {{ lower: number, upper: number }} Equal partition offsets.
|
|
242
|
+
*/
|
|
243
|
+
#partition(left, right, pivot, axis) {
|
|
244
|
+
const pivotCoordinate = PcbScene3dAabbIndex.#entryCoordinate(
|
|
245
|
+
pivot,
|
|
246
|
+
axis
|
|
247
|
+
)
|
|
248
|
+
let lower = left
|
|
249
|
+
let index = left
|
|
250
|
+
let upper = right
|
|
251
|
+
|
|
252
|
+
while (index <= upper) {
|
|
253
|
+
const coordinate = PcbScene3dAabbIndex.#entryCoordinate(
|
|
254
|
+
this.#entries[index],
|
|
255
|
+
axis
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if (coordinate < pivotCoordinate) {
|
|
259
|
+
this.#swapEntries(lower, index)
|
|
260
|
+
lower += 1
|
|
261
|
+
index += 1
|
|
262
|
+
} else if (coordinate > pivotCoordinate) {
|
|
263
|
+
this.#swapEntries(index, upper)
|
|
264
|
+
upper -= 1
|
|
265
|
+
} else {
|
|
266
|
+
index += 1
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { lower, upper }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Swaps two entries while building the tree.
|
|
275
|
+
* @param {number} firstIndex
|
|
276
|
+
* @param {number} secondIndex
|
|
277
|
+
* @returns {void}
|
|
278
|
+
*/
|
|
279
|
+
#swapEntries(firstIndex, secondIndex) {
|
|
280
|
+
if (firstIndex === secondIndex) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const first = this.#entries[firstIndex]
|
|
285
|
+
this.#entries[firstIndex] = this.#entries[secondIndex]
|
|
286
|
+
this.#entries[secondIndex] = first
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Collects matching tree entries or items into a target.
|
|
291
|
+
* @param {PcbScene3dAabbNode | null} node
|
|
292
|
+
* @param {PcbScene3dAabbBounds} queryBounds
|
|
293
|
+
* @param {number} epsilon
|
|
294
|
+
* @param {boolean} includeAll
|
|
295
|
+
* @param {boolean} collectEntries
|
|
296
|
+
* @param {(PcbScene3dAabbEntry | *)[]} target
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
#collectTreeMatches(
|
|
300
|
+
node,
|
|
301
|
+
queryBounds,
|
|
302
|
+
epsilon,
|
|
303
|
+
includeAll,
|
|
304
|
+
collectEntries,
|
|
305
|
+
target
|
|
306
|
+
) {
|
|
307
|
+
if (
|
|
308
|
+
!node ||
|
|
309
|
+
(!includeAll &&
|
|
310
|
+
!PcbScene3dAabbIndex.#overlaps(
|
|
311
|
+
node.bounds,
|
|
312
|
+
queryBounds,
|
|
313
|
+
epsilon
|
|
314
|
+
))
|
|
315
|
+
) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (node.left || node.right) {
|
|
320
|
+
this.#collectTreeMatches(
|
|
321
|
+
node.left,
|
|
322
|
+
queryBounds,
|
|
323
|
+
epsilon,
|
|
324
|
+
includeAll,
|
|
325
|
+
collectEntries,
|
|
326
|
+
target
|
|
327
|
+
)
|
|
328
|
+
this.#collectTreeMatches(
|
|
329
|
+
node.right,
|
|
330
|
+
queryBounds,
|
|
331
|
+
epsilon,
|
|
332
|
+
includeAll,
|
|
333
|
+
collectEntries,
|
|
334
|
+
target
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let index = node.start; index < node.end; index += 1) {
|
|
340
|
+
const entry = this.#entries[index]
|
|
341
|
+
if (
|
|
342
|
+
includeAll ||
|
|
343
|
+
PcbScene3dAabbIndex.#overlaps(
|
|
344
|
+
entry.bounds,
|
|
345
|
+
queryBounds,
|
|
346
|
+
epsilon
|
|
347
|
+
)
|
|
348
|
+
) {
|
|
349
|
+
target.push(collectEntries ? entry : entry.item)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Collects matching overflow entries or items into a target.
|
|
356
|
+
* @param {PcbScene3dAabbBounds} queryBounds
|
|
357
|
+
* @param {number} epsilon
|
|
358
|
+
* @param {boolean} includeAll
|
|
359
|
+
* @param {boolean} collectEntries
|
|
360
|
+
* @param {(PcbScene3dAabbEntry | *)[]} target
|
|
361
|
+
* @returns {void}
|
|
362
|
+
*/
|
|
363
|
+
#collectOverflowMatches(
|
|
364
|
+
queryBounds,
|
|
365
|
+
epsilon,
|
|
366
|
+
includeAll,
|
|
367
|
+
collectEntries,
|
|
368
|
+
target
|
|
369
|
+
) {
|
|
370
|
+
for (let index = 0; index < this.#overflow.length; index += 1) {
|
|
371
|
+
const entry = this.#overflow[index]
|
|
372
|
+
if (
|
|
373
|
+
includeAll ||
|
|
374
|
+
PcbScene3dAabbIndex.#overlaps(
|
|
375
|
+
entry.bounds,
|
|
376
|
+
queryBounds,
|
|
377
|
+
epsilon
|
|
378
|
+
)
|
|
379
|
+
) {
|
|
380
|
+
target.push(collectEntries ? entry : entry.item)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Resolves an entry midpoint without overflowing finite coordinates.
|
|
387
|
+
* @param {PcbScene3dAabbEntry} entry
|
|
388
|
+
* @param {'x' | 'y'} axis
|
|
389
|
+
* @returns {number}
|
|
390
|
+
*/
|
|
391
|
+
static #entryCoordinate(entry, axis) {
|
|
392
|
+
if (axis === 'x') {
|
|
393
|
+
return entry.bounds.minX * 0.5 + entry.bounds.maxX * 0.5
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return entry.bounds.minY * 0.5 + entry.bounds.maxY * 0.5
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Compares entry source indexes for stable query results.
|
|
401
|
+
* @param {PcbScene3dAabbEntry} first
|
|
402
|
+
* @param {PcbScene3dAabbEntry} second
|
|
403
|
+
* @returns {number}
|
|
404
|
+
*/
|
|
405
|
+
static #compareSourceIndexes(first, second) {
|
|
406
|
+
if (first.sourceIndex < second.sourceIndex) {
|
|
407
|
+
return -1
|
|
408
|
+
}
|
|
409
|
+
if (first.sourceIndex > second.sourceIndex) {
|
|
410
|
+
return 1
|
|
411
|
+
}
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Returns true when every bound coordinate is finite.
|
|
417
|
+
* @param {PcbScene3dAabbBounds | null | undefined} bounds
|
|
418
|
+
* @returns {boolean}
|
|
419
|
+
*/
|
|
420
|
+
static #isFiniteBounds(bounds) {
|
|
421
|
+
return Boolean(
|
|
422
|
+
bounds &&
|
|
423
|
+
Number.isFinite(bounds.minX) &&
|
|
424
|
+
Number.isFinite(bounds.maxX) &&
|
|
425
|
+
Number.isFinite(bounds.minY) &&
|
|
426
|
+
Number.isFinite(bounds.maxY)
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Returns true when two bounds touch or overlap within an epsilon.
|
|
432
|
+
* @param {PcbScene3dAabbBounds} first
|
|
433
|
+
* @param {PcbScene3dAabbBounds} second
|
|
434
|
+
* @param {number} epsilon
|
|
435
|
+
* @returns {boolean}
|
|
436
|
+
*/
|
|
437
|
+
static #overlaps(first, second, epsilon) {
|
|
438
|
+
return !(
|
|
439
|
+
first.maxX < second.minX - epsilon ||
|
|
440
|
+
first.minX > second.maxX + epsilon ||
|
|
441
|
+
first.maxY < second.minY - epsilon ||
|
|
442
|
+
first.minY > second.maxY + epsilon
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Resolves bounds from an item's bounds property.
|
|
448
|
+
* @param {{ bounds: PcbScene3dAabbBounds }} item
|
|
449
|
+
* @returns {PcbScene3dAabbBounds}
|
|
450
|
+
*/
|
|
451
|
+
static #defaultResolveBounds(item) {
|
|
452
|
+
return item.bounds
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves the source position of an item.
|
|
457
|
+
* @param {*} _item
|
|
458
|
+
* @param {number} index
|
|
459
|
+
* @returns {number}
|
|
460
|
+
*/
|
|
461
|
+
static #defaultResolveSourceIndex(_item, index) {
|
|
462
|
+
return index
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -420,7 +420,7 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
420
420
|
* @returns {number}
|
|
421
421
|
*/
|
|
422
422
|
static #resolveSurfaceColor(board, options) {
|
|
423
|
-
return PcbScene3dBoardMaterialPalette.
|
|
423
|
+
return PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {
|
|
424
424
|
hasBoardAssemblyModel: true,
|
|
425
425
|
sourceFormat: options?.sourceFormat
|
|
426
426
|
})
|
|
@@ -98,9 +98,10 @@ export class PcbScene3dBoardEdgeCutoutBuilder {
|
|
|
98
98
|
* Returns true when a cutout can safely be added as a shape hole.
|
|
99
99
|
* @param {{ x: number, y: number }[]} hole
|
|
100
100
|
* @param {{ x: number, y: number }[]} contour
|
|
101
|
+
* @param {{ isCircular: true, centerX: number, centerY: number, radius: number } | null} [resolvedCircle]
|
|
101
102
|
* @returns {boolean}
|
|
102
103
|
*/
|
|
103
|
-
static isHoleInsideContour(hole, contour) {
|
|
104
|
+
static isHoleInsideContour(hole, contour, resolvedCircle) {
|
|
104
105
|
if (
|
|
105
106
|
!Array.isArray(hole) ||
|
|
106
107
|
!Array.isArray(contour) ||
|
|
@@ -110,10 +111,13 @@ export class PcbScene3dBoardEdgeCutoutBuilder {
|
|
|
110
111
|
return false
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
const circularHole =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
const circularHole =
|
|
115
|
+
resolvedCircle !== undefined
|
|
116
|
+
? resolvedCircle
|
|
117
|
+
: PcbScene3dCutoutCircleDetector.resolve(
|
|
118
|
+
hole,
|
|
119
|
+
PcbScene3dBoardEdgeCutoutBuilder.#GEOMETRY_EPSILON
|
|
120
|
+
)
|
|
117
121
|
if (circularHole) {
|
|
118
122
|
return PcbScene3dBoardEdgeCutoutBuilder.#isCircularHoleInsideContour(
|
|
119
123
|
circularHole,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export class PcbScene3dBoardMaterialPalette {
|
|
5
5
|
static #DEFAULT_SURFACE_COLOR = 0x2a5f27
|
|
6
|
+
static #BOARD_SURFACE_DARKEN_RATIO = 0.88
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Resolves the solder-mask face color for the generated board shell.
|
|
@@ -25,6 +26,19 @@ export class PcbScene3dBoardMaterialPalette {
|
|
|
25
26
|
: PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the darker display color for visible board solder-mask faces.
|
|
31
|
+
* @param {{ surfaceColor?: number } | undefined} board Board metadata.
|
|
32
|
+
* @param {{ hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options] Scene options.
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
static resolveBoardSurfaceColor(board, options = {}) {
|
|
36
|
+
return PcbScene3dBoardMaterialPalette.#darkenHexColor(
|
|
37
|
+
PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, options),
|
|
38
|
+
PcbScene3dBoardMaterialPalette.#BOARD_SURFACE_DARKEN_RATIO
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* Returns true when the source format supplies display-stable board colors.
|
|
30
44
|
* @param {{ sourceFormat?: string }} options Scene options.
|
|
@@ -52,4 +66,19 @@ export class PcbScene3dBoardMaterialPalette {
|
|
|
52
66
|
static isGeneratedBodyVisible(options = {}) {
|
|
53
67
|
return true
|
|
54
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Darkens a packed RGB color while preserving its hue.
|
|
72
|
+
* @param {number} color Packed RGB color.
|
|
73
|
+
* @param {number} ratio Channel multiplier.
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
static #darkenHexColor(color, ratio) {
|
|
77
|
+
const multiplier = Math.min(Math.max(Number(ratio || 0), 0), 1)
|
|
78
|
+
|
|
79
|
+
return [16, 8, 0].reduce((output, shift) => {
|
|
80
|
+
const channel = Math.round(((color >> shift) & 255) * multiplier)
|
|
81
|
+
return output | (channel << shift)
|
|
82
|
+
}, 0)
|
|
83
|
+
}
|
|
55
84
|
}
|