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
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CircuitJsonIndexer,
|
|
4
4
|
CircuitJsonUnits
|
|
5
5
|
} from 'circuitjson-toolkit'
|
|
6
|
+
import { PcbScene3dCircuitJsonModelTransform } from './PcbScene3dCircuitJsonModelTransform.mjs'
|
|
6
7
|
|
|
7
8
|
const TOP_LAYER_ID = 1
|
|
8
9
|
const BOTTOM_LAYER_ID = 32
|
|
@@ -12,6 +13,17 @@ const DEFAULT_BOARD_THICKNESS_MM = 1.6
|
|
|
12
13
|
const DEFAULT_COMPONENT_HEIGHT_MIL = 60
|
|
13
14
|
const RECTANGULAR_PAD_SHAPE = 2
|
|
14
15
|
const CIRCULAR_PAD_SHAPE = 1
|
|
16
|
+
const CIRCLE_CUTOUT_POINTS = 32
|
|
17
|
+
const MODEL_URL_FIELDS = [
|
|
18
|
+
['model_step_url', 'step'],
|
|
19
|
+
['model_stp_url', 'step'],
|
|
20
|
+
['model_wrl_url', 'wrl'],
|
|
21
|
+
['model_vrml_url', 'wrl'],
|
|
22
|
+
['model_glb_url', 'glb'],
|
|
23
|
+
['model_gltf_url', 'gltf'],
|
|
24
|
+
['model_stl_url', 'stl'],
|
|
25
|
+
['model_obj_url', 'obj']
|
|
26
|
+
]
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* Converts serialized CircuitJSON element arrays into the viewer render model.
|
|
@@ -46,16 +58,28 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
46
58
|
/**
|
|
47
59
|
* Builds the internal render model used by the Three.js runtime.
|
|
48
60
|
* @param {object[]} circuitJson Serialized CircuitJSON model.
|
|
61
|
+
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined }} [options] Adapter options.
|
|
49
62
|
* @returns {object}
|
|
50
63
|
*/
|
|
51
|
-
static build(circuitJson) {
|
|
64
|
+
static build(circuitJson, options = {}) {
|
|
52
65
|
CircuitJsonDocument.assertModel(circuitJson)
|
|
53
66
|
const index = CircuitJsonIndexer.index(circuitJson)
|
|
54
67
|
const board = PcbScene3dCircuitJsonAdapter.#buildBoard(index)
|
|
55
68
|
const detail = PcbScene3dCircuitJsonAdapter.#buildDetail(index)
|
|
69
|
+
const externalPlacements =
|
|
70
|
+
PcbScene3dCircuitJsonAdapter.#buildExternalPlacements(
|
|
71
|
+
index,
|
|
72
|
+
board,
|
|
73
|
+
options
|
|
74
|
+
)
|
|
75
|
+
const externalModelByComponentId =
|
|
76
|
+
PcbScene3dCircuitJsonAdapter.#externalModelByComponentId(
|
|
77
|
+
externalPlacements
|
|
78
|
+
)
|
|
56
79
|
const components = PcbScene3dCircuitJsonAdapter.#buildComponents(
|
|
57
80
|
index,
|
|
58
|
-
board
|
|
81
|
+
board,
|
|
82
|
+
externalModelByComponentId
|
|
59
83
|
)
|
|
60
84
|
|
|
61
85
|
return {
|
|
@@ -63,19 +87,22 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
63
87
|
coordinateSystem: 'circuitjson-mm',
|
|
64
88
|
board,
|
|
65
89
|
components,
|
|
66
|
-
externalPlacements
|
|
90
|
+
externalPlacements,
|
|
67
91
|
boardAssemblyModel: null,
|
|
68
92
|
detail
|
|
69
93
|
}
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
/**
|
|
73
|
-
* Builds the render-model board from a `pcb_board` element.
|
|
97
|
+
* Builds the render-model board from a `pcb_panel` or `pcb_board` element.
|
|
74
98
|
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
75
99
|
* @returns {object}
|
|
76
100
|
*/
|
|
77
101
|
static #buildBoard(index) {
|
|
78
|
-
const boardElement =
|
|
102
|
+
const boardElement =
|
|
103
|
+
index.elementsByType.get('pcb_panel')?.[0] ||
|
|
104
|
+
index.elementsByType.get('pcb_board')?.[0] ||
|
|
105
|
+
{}
|
|
79
106
|
const widthMil = CircuitJsonUnits.mmToMil(
|
|
80
107
|
boardElement.width,
|
|
81
108
|
DEFAULT_BOARD_WIDTH_MM
|
|
@@ -110,6 +137,7 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
110
137
|
centerX: center.x,
|
|
111
138
|
centerY: center.y,
|
|
112
139
|
segments,
|
|
140
|
+
cutouts: PcbScene3dCircuitJsonAdapter.#buildBoardCutouts(index),
|
|
113
141
|
surfaceColor: null,
|
|
114
142
|
edgeColor: null
|
|
115
143
|
}
|
|
@@ -168,18 +196,129 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
168
196
|
})
|
|
169
197
|
}
|
|
170
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Builds explicit through-board cutout loops.
|
|
201
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
202
|
+
* @returns {{ points: { x: number, y: number }[], sourceId?: string }[]}
|
|
203
|
+
*/
|
|
204
|
+
static #buildBoardCutouts(index) {
|
|
205
|
+
return (index.elementsByType.get('pcb_cutout') || [])
|
|
206
|
+
.map((cutout) => PcbScene3dCircuitJsonAdapter.#buildCutout(cutout))
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Builds one cutout loop.
|
|
212
|
+
* @param {object} cutout Cutout element.
|
|
213
|
+
* @returns {{ points: { x: number, y: number }[], sourceId?: string } | null}
|
|
214
|
+
*/
|
|
215
|
+
static #buildCutout(cutout) {
|
|
216
|
+
const outline =
|
|
217
|
+
PcbScene3dCircuitJsonAdapter.#cutoutOutlinePoints(cutout)
|
|
218
|
+
if (outline.length < 3) {
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
points: outline,
|
|
224
|
+
sourceId: String(cutout?.pcb_cutout_id || cutout?.cutout_id || '')
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves cutout outline points from polygon, rectangle, or circle fields.
|
|
230
|
+
* @param {object} cutout Cutout element.
|
|
231
|
+
* @returns {{ x: number, y: number }[]}
|
|
232
|
+
*/
|
|
233
|
+
static #cutoutOutlinePoints(cutout) {
|
|
234
|
+
const points = PcbScene3dCircuitJsonAdapter.#array(
|
|
235
|
+
cutout?.points || cutout?.outline || cutout?.polygon || []
|
|
236
|
+
)
|
|
237
|
+
.map((point) => CircuitJsonUnits.pointMmToMil(point))
|
|
238
|
+
.filter((point) => Number.isFinite(point.x + point.y))
|
|
239
|
+
if (points.length >= 3) {
|
|
240
|
+
return points
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const shape = String(cutout?.shape || '').toLowerCase()
|
|
244
|
+
if (shape.includes('circle') || Number(cutout?.radius || 0) > 0) {
|
|
245
|
+
return PcbScene3dCircuitJsonAdapter.#circleCutoutPoints(cutout)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return PcbScene3dCircuitJsonAdapter.#rectCutoutPoints(cutout)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Builds a rectangular cutout loop.
|
|
253
|
+
* @param {object} cutout Cutout element.
|
|
254
|
+
* @returns {{ x: number, y: number }[]}
|
|
255
|
+
*/
|
|
256
|
+
static #rectCutoutPoints(cutout) {
|
|
257
|
+
const center = CircuitJsonUnits.pointMmToMil(
|
|
258
|
+
cutout?.center || {
|
|
259
|
+
x: cutout?.x,
|
|
260
|
+
y: cutout?.y
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
const width = CircuitJsonUnits.mmToMil(cutout?.width, 0)
|
|
264
|
+
const height = CircuitJsonUnits.mmToMil(cutout?.height, 0)
|
|
265
|
+
if (width <= 0 || height <= 0) {
|
|
266
|
+
return []
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const halfWidth = width / 2
|
|
270
|
+
const halfHeight = height / 2
|
|
271
|
+
return [
|
|
272
|
+
{ x: center.x - halfWidth, y: center.y - halfHeight },
|
|
273
|
+
{ x: center.x + halfWidth, y: center.y - halfHeight },
|
|
274
|
+
{ x: center.x + halfWidth, y: center.y + halfHeight },
|
|
275
|
+
{ x: center.x - halfWidth, y: center.y + halfHeight }
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Builds a circular cutout loop.
|
|
281
|
+
* @param {object} cutout Cutout element.
|
|
282
|
+
* @returns {{ x: number, y: number }[]}
|
|
283
|
+
*/
|
|
284
|
+
static #circleCutoutPoints(cutout) {
|
|
285
|
+
const center = CircuitJsonUnits.pointMmToMil(
|
|
286
|
+
cutout?.center || {
|
|
287
|
+
x: cutout?.x,
|
|
288
|
+
y: cutout?.y
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
const radius = CircuitJsonUnits.mmToMil(
|
|
292
|
+
cutout?.radius || Number(cutout?.diameter || 0) / 2,
|
|
293
|
+
0
|
|
294
|
+
)
|
|
295
|
+
if (radius <= 0) {
|
|
296
|
+
return []
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return Array.from({ length: CIRCLE_CUTOUT_POINTS }, (_entry, index) => {
|
|
300
|
+
const angle = (Math.PI * 2 * index) / CIRCLE_CUTOUT_POINTS
|
|
301
|
+
return {
|
|
302
|
+
x: center.x + Math.cos(angle) * radius,
|
|
303
|
+
y: center.y + Math.sin(angle) * radius
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
171
308
|
/**
|
|
172
309
|
* Builds component fallback bodies from `pcb_component` elements.
|
|
173
310
|
* @param {{ elementsByType: Map<string, object[]>, sourceComponentById: Map<string, object> }} index CircuitJSON index.
|
|
174
311
|
* @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
|
|
312
|
+
* @param {Map<string, object>} externalModelByComponentId External model metadata by PCB component ID.
|
|
175
313
|
* @returns {object[]}
|
|
176
314
|
*/
|
|
177
|
-
static #buildComponents(index, board) {
|
|
315
|
+
static #buildComponents(index, board, externalModelByComponentId) {
|
|
178
316
|
return (index.elementsByType.get('pcb_component') || []).map(
|
|
179
317
|
(component, componentIndex) => {
|
|
180
318
|
const sourceComponent = index.sourceComponentById.get(
|
|
181
319
|
String(component?.source_component_id || '')
|
|
182
320
|
)
|
|
321
|
+
const componentId = String(component?.pcb_component_id || '')
|
|
183
322
|
const center = CircuitJsonUnits.pointMmToMil(
|
|
184
323
|
component?.center || {
|
|
185
324
|
x: component?.x,
|
|
@@ -231,12 +370,266 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
231
370
|
height: Math.max(height, 10)
|
|
232
371
|
}
|
|
233
372
|
},
|
|
234
|
-
externalModel:
|
|
373
|
+
externalModel:
|
|
374
|
+
externalModelByComponentId.get(componentId) || null
|
|
235
375
|
}
|
|
236
376
|
}
|
|
237
377
|
)
|
|
238
378
|
}
|
|
239
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Builds external component model placements from CAD metadata.
|
|
382
|
+
* @param {{ elementsByType: Map<string, object[]>, sourceComponentById: Map<string, object> }} index CircuitJSON index.
|
|
383
|
+
* @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
|
|
384
|
+
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined }} options Adapter options.
|
|
385
|
+
* @returns {object[]}
|
|
386
|
+
*/
|
|
387
|
+
static #buildExternalPlacements(index, board, options) {
|
|
388
|
+
const componentById = PcbScene3dCircuitJsonAdapter.#elementById(
|
|
389
|
+
index,
|
|
390
|
+
'pcb_component',
|
|
391
|
+
'pcb_component_id'
|
|
392
|
+
)
|
|
393
|
+
return (index.elementsByType.get('cad_component') || [])
|
|
394
|
+
.map((cadComponent, cadIndex) => {
|
|
395
|
+
const component = componentById.get(
|
|
396
|
+
String(cadComponent?.pcb_component_id || '')
|
|
397
|
+
)
|
|
398
|
+
return PcbScene3dCircuitJsonAdapter.#buildExternalPlacement(
|
|
399
|
+
cadComponent,
|
|
400
|
+
component,
|
|
401
|
+
index,
|
|
402
|
+
board,
|
|
403
|
+
options,
|
|
404
|
+
cadIndex
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Builds one external model placement.
|
|
412
|
+
* @param {object} cadComponent CAD component element.
|
|
413
|
+
* @param {object | undefined} component PCB component element.
|
|
414
|
+
* @param {{ sourceComponentById: Map<string, object> }} index CircuitJSON index.
|
|
415
|
+
* @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
|
|
416
|
+
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined }} options Adapter options.
|
|
417
|
+
* @param {number} cadIndex CAD component index.
|
|
418
|
+
* @returns {object | null}
|
|
419
|
+
*/
|
|
420
|
+
static #buildExternalPlacement(
|
|
421
|
+
cadComponent,
|
|
422
|
+
component,
|
|
423
|
+
index,
|
|
424
|
+
board,
|
|
425
|
+
options,
|
|
426
|
+
cadIndex
|
|
427
|
+
) {
|
|
428
|
+
const sourceComponent = index.sourceComponentById.get(
|
|
429
|
+
String(component?.source_component_id || '')
|
|
430
|
+
)
|
|
431
|
+
const externalModel = PcbScene3dCircuitJsonAdapter.#externalModel(
|
|
432
|
+
cadComponent,
|
|
433
|
+
component,
|
|
434
|
+
sourceComponent,
|
|
435
|
+
options,
|
|
436
|
+
cadIndex
|
|
437
|
+
)
|
|
438
|
+
if (!externalModel) {
|
|
439
|
+
return null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const mountSide = PcbScene3dCircuitJsonAdapter.#layerSide(
|
|
443
|
+
component?.layer || cadComponent?.layer
|
|
444
|
+
)
|
|
445
|
+
return {
|
|
446
|
+
designator: PcbScene3dCircuitJsonAdapter.#componentDesignator(
|
|
447
|
+
component || cadComponent,
|
|
448
|
+
sourceComponent,
|
|
449
|
+
cadIndex
|
|
450
|
+
),
|
|
451
|
+
pcbComponentId: String(component?.pcb_component_id || ''),
|
|
452
|
+
mountSide,
|
|
453
|
+
rotationDeg: PcbScene3dCircuitJsonAdapter.#placementRotationDeg(
|
|
454
|
+
cadComponent,
|
|
455
|
+
component
|
|
456
|
+
),
|
|
457
|
+
positionMil: PcbScene3dCircuitJsonAdapter.#placementPositionMil(
|
|
458
|
+
cadComponent,
|
|
459
|
+
component,
|
|
460
|
+
board,
|
|
461
|
+
mountSide
|
|
462
|
+
),
|
|
463
|
+
modelTransform:
|
|
464
|
+
PcbScene3dCircuitJsonModelTransform.build(cadComponent),
|
|
465
|
+
...PcbScene3dCircuitJsonModelTransform.displayMetadata(
|
|
466
|
+
cadComponent
|
|
467
|
+
),
|
|
468
|
+
externalModel
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Builds an external model metadata object.
|
|
474
|
+
* @param {object} cadComponent CAD component element.
|
|
475
|
+
* @param {object | undefined} component PCB component element.
|
|
476
|
+
* @param {object | undefined} sourceComponent Source component element.
|
|
477
|
+
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined }} options Adapter options.
|
|
478
|
+
* @param {number} cadIndex CAD component index.
|
|
479
|
+
* @returns {object | null}
|
|
480
|
+
*/
|
|
481
|
+
static #externalModel(
|
|
482
|
+
cadComponent,
|
|
483
|
+
component,
|
|
484
|
+
sourceComponent,
|
|
485
|
+
options,
|
|
486
|
+
cadIndex
|
|
487
|
+
) {
|
|
488
|
+
const match = MODEL_URL_FIELDS.find(([field]) =>
|
|
489
|
+
String(cadComponent?.[field] || '').trim()
|
|
490
|
+
)
|
|
491
|
+
if (!match) {
|
|
492
|
+
return null
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const [field, format] = match
|
|
496
|
+
const sourceUrl = String(cadComponent?.[field] || '').trim()
|
|
497
|
+
const context = {
|
|
498
|
+
format,
|
|
499
|
+
field,
|
|
500
|
+
cadComponent,
|
|
501
|
+
component,
|
|
502
|
+
sourceComponent,
|
|
503
|
+
index: cadIndex
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
format,
|
|
508
|
+
name: PcbScene3dCircuitJsonAdapter.#fileNameFromUrl(sourceUrl),
|
|
509
|
+
sourceUrl,
|
|
510
|
+
...PcbScene3dCircuitJsonAdapter.#resolveModelUrl(
|
|
511
|
+
sourceUrl,
|
|
512
|
+
context,
|
|
513
|
+
options
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Applies optional caller-owned model URL resolution.
|
|
520
|
+
* @param {string} sourceUrl Source URL.
|
|
521
|
+
* @param {object} context Resolver context.
|
|
522
|
+
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined }} options Adapter options.
|
|
523
|
+
* @returns {object}
|
|
524
|
+
*/
|
|
525
|
+
static #resolveModelUrl(sourceUrl, context, options) {
|
|
526
|
+
const resolver = options?.modelUrlResolver
|
|
527
|
+
if (typeof resolver !== 'function') {
|
|
528
|
+
return {}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const resolved = resolver(sourceUrl, context)
|
|
532
|
+
if (typeof resolved === 'string') {
|
|
533
|
+
return { resolvedUrl: resolved }
|
|
534
|
+
}
|
|
535
|
+
if (resolved && typeof resolved === 'object') {
|
|
536
|
+
return { ...resolved }
|
|
537
|
+
}
|
|
538
|
+
return {}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Builds a component ID to external-model map.
|
|
543
|
+
* @param {object[]} placements External placements.
|
|
544
|
+
* @returns {Map<string, object>}
|
|
545
|
+
*/
|
|
546
|
+
static #externalModelByComponentId(placements) {
|
|
547
|
+
const map = new Map()
|
|
548
|
+
placements.forEach((placement) => {
|
|
549
|
+
const componentId = String(placement?.pcbComponentId || '')
|
|
550
|
+
if (componentId && placement?.externalModel) {
|
|
551
|
+
map.set(componentId, placement.externalModel)
|
|
552
|
+
}
|
|
553
|
+
})
|
|
554
|
+
return map
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Builds a map keyed by one element ID field.
|
|
559
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
560
|
+
* @param {string} type Element type.
|
|
561
|
+
* @param {string} idField ID field.
|
|
562
|
+
* @returns {Map<string, object>}
|
|
563
|
+
*/
|
|
564
|
+
static #elementById(index, type, idField) {
|
|
565
|
+
const map = new Map()
|
|
566
|
+
;(index.elementsByType.get(type) || []).forEach((element) => {
|
|
567
|
+
const id = String(element?.[idField] || '')
|
|
568
|
+
if (id) {
|
|
569
|
+
map.set(id, element)
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
return map
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Resolves placement position in board-local mils.
|
|
577
|
+
* @param {object} cadComponent CAD component element.
|
|
578
|
+
* @param {object | undefined} component PCB component element.
|
|
579
|
+
* @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
|
|
580
|
+
* @param {'top' | 'bottom'} mountSide Mount side.
|
|
581
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
582
|
+
*/
|
|
583
|
+
static #placementPositionMil(cadComponent, component, board, mountSide) {
|
|
584
|
+
const point = CircuitJsonUnits.pointMmToMil(
|
|
585
|
+
cadComponent?.position ||
|
|
586
|
+
component?.center || {
|
|
587
|
+
x: component?.x,
|
|
588
|
+
y: component?.y
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
const fallbackZ =
|
|
592
|
+
mountSide === 'bottom'
|
|
593
|
+
? -Number(board?.thicknessMil || 0) / 2
|
|
594
|
+
: Number(board?.thicknessMil || 0) / 2
|
|
595
|
+
const z = Number.isFinite(Number(cadComponent?.position?.z))
|
|
596
|
+
? CircuitJsonUnits.mmToMil(cadComponent.position.z, 0)
|
|
597
|
+
: fallbackZ
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
x: point.x - Number(board?.centerX || 0),
|
|
601
|
+
y: point.y - Number(board?.centerY || 0),
|
|
602
|
+
z
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Resolves placement rotation in board space.
|
|
608
|
+
* @param {object} cadComponent CAD component element.
|
|
609
|
+
* @param {object | undefined} component PCB component element.
|
|
610
|
+
* @returns {number}
|
|
611
|
+
*/
|
|
612
|
+
static #placementRotationDeg(cadComponent, component) {
|
|
613
|
+
return Number(
|
|
614
|
+
cadComponent?.rotation?.z ??
|
|
615
|
+
cadComponent?.rotationDeg ??
|
|
616
|
+
cadComponent?.rotation ??
|
|
617
|
+
component?.rotation ??
|
|
618
|
+
component?.ccw_rotation ??
|
|
619
|
+
0
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Extracts a file name from a URL-like model reference.
|
|
625
|
+
* @param {string} sourceUrl Source URL.
|
|
626
|
+
* @returns {string}
|
|
627
|
+
*/
|
|
628
|
+
static #fileNameFromUrl(sourceUrl) {
|
|
629
|
+
const cleanUrl = String(sourceUrl || '').split(/[?#]/u)[0]
|
|
630
|
+
return cleanUrl.split('/').filter(Boolean).pop() || 'model'
|
|
631
|
+
}
|
|
632
|
+
|
|
240
633
|
/**
|
|
241
634
|
* Resolves a display designator for one CircuitJSON component.
|
|
242
635
|
* @param {object} component PCB component element.
|
|
@@ -542,4 +935,13 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
542
935
|
? 'bottom'
|
|
543
936
|
: 'top'
|
|
544
937
|
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Normalizes a value to an array.
|
|
941
|
+
* @param {unknown} value Candidate value.
|
|
942
|
+
* @returns {any[]}
|
|
943
|
+
*/
|
|
944
|
+
static #array(value) {
|
|
945
|
+
return Array.isArray(value) ? value : []
|
|
946
|
+
}
|
|
545
947
|
}
|