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
|
@@ -2,13 +2,13 @@ import { PcbScene3dBoardEdgeCutoutBuilder } from './PcbScene3dBoardEdgeCutoutBui
|
|
|
2
2
|
import { PcbScene3dBoardCutoutPathFactory } from './PcbScene3dBoardCutoutPathFactory.mjs'
|
|
3
3
|
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
4
4
|
import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
5
|
+
import { PcbScene3dPlatedDrillSpecResolver } from './PcbScene3dPlatedDrillSpecResolver.mjs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Builds the board solid profile, including drilled holes.
|
|
8
9
|
*/
|
|
9
10
|
export class PcbScene3dBoardShapeFactory {
|
|
10
11
|
static #CURVE_SEGMENTS = 8
|
|
11
|
-
static #PAD_HOLE_SHAPE_SLOT = 2
|
|
12
12
|
static #PLATED_WALL_MATERIAL_INDEX = 2
|
|
13
13
|
static #EDGE_WALL_MATERIAL_INDEX = 1
|
|
14
14
|
static #CONTOUR_SAMPLE_POINTS = 64
|
|
@@ -235,7 +235,9 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
235
235
|
const diameter = Number(normalizedSpec.diameter || 0)
|
|
236
236
|
const slotLength = Number(normalizedSpec.slotLength || 0)
|
|
237
237
|
const isCircular =
|
|
238
|
-
|
|
238
|
+
PcbScene3dBoardShapeFactory.#isCircularDrillSpec(
|
|
239
|
+
normalizedSpec
|
|
240
|
+
)
|
|
239
241
|
const points = isCircular
|
|
240
242
|
? PcbScene3dBoardEdgeCutoutBuilder.buildCircularCutoutPoints(
|
|
241
243
|
Number(normalizedSpec.x || 0),
|
|
@@ -338,7 +340,7 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
338
340
|
* @returns {{ points: { x: number, y: number }[], segments: { start: { x: number, y: number }, end: { x: number, y: number }, dx: number, dy: number, lengthSquared: number, bounds: { minX: number, maxX: number, minY: number, maxY: number } }[], bounds: { minX: number, maxX: number, minY: number, maxY: number }, isCircular?: boolean, centerX?: number, centerY?: number, radius?: number }[]}
|
|
339
341
|
*/
|
|
340
342
|
static #resolvePlatedContours(THREE, detail, normalizeBoardPoint) {
|
|
341
|
-
return
|
|
343
|
+
return PcbScene3dPlatedDrillSpecResolver.resolve(detail)
|
|
342
344
|
.map((drillSpec) => {
|
|
343
345
|
const point = normalizeBoardPoint(drillSpec.x, drillSpec.y)
|
|
344
346
|
const normalizedSpec = {
|
|
@@ -373,14 +375,18 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
373
375
|
|
|
374
376
|
/**
|
|
375
377
|
* Returns true when a drill aperture can be matched as a circle.
|
|
376
|
-
* @param {{ diameter?: number, slotLength?: number | null }} drillSpec
|
|
378
|
+
* @param {{ diameter?: number, shape?: string, slotLength?: number | null }} drillSpec
|
|
377
379
|
* @returns {boolean}
|
|
378
380
|
*/
|
|
379
381
|
static #isCircularDrillSpec(drillSpec) {
|
|
380
382
|
const diameter = Number(drillSpec?.diameter || 0)
|
|
381
383
|
const slotLength = Number(drillSpec?.slotLength || 0)
|
|
382
384
|
|
|
383
|
-
return
|
|
385
|
+
return (
|
|
386
|
+
diameter > 0 &&
|
|
387
|
+
drillSpec?.shape !== 'rect' &&
|
|
388
|
+
slotLength <= diameter + 0.001
|
|
389
|
+
)
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
/**
|
|
@@ -416,94 +422,6 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
416
422
|
}
|
|
417
423
|
}
|
|
418
424
|
|
|
419
|
-
/**
|
|
420
|
-
* Resolves deduped drill specs that represent plated holes.
|
|
421
|
-
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
422
|
-
* @returns {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }[]}
|
|
423
|
-
*/
|
|
424
|
-
static #resolvePlatedDrillSpecs(detail) {
|
|
425
|
-
const platedKeys = new Set()
|
|
426
|
-
|
|
427
|
-
for (const via of detail?.vias || []) {
|
|
428
|
-
const diameter = Number(via?.holeDiameter || 0)
|
|
429
|
-
if (diameter <= 0) {
|
|
430
|
-
continue
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
platedKeys.add(
|
|
434
|
-
PcbScene3dBoardShapeFactory.#buildDrillSpecKey({
|
|
435
|
-
x: Number(via?.x || 0),
|
|
436
|
-
y: Number(via?.y || 0),
|
|
437
|
-
diameter,
|
|
438
|
-
slotLength: null,
|
|
439
|
-
rotationDeg: 0
|
|
440
|
-
})
|
|
441
|
-
)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
for (const pad of detail?.pads || []) {
|
|
445
|
-
const diameter = Number(pad?.holeDiameter || 0)
|
|
446
|
-
if (
|
|
447
|
-
diameter <= 0 ||
|
|
448
|
-
!PcbScene3dBoardShapeFactory.#hasPadCopperAnnulus(pad, diameter)
|
|
449
|
-
) {
|
|
450
|
-
continue
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const slotLength =
|
|
454
|
-
Number(pad?.holeShape) ===
|
|
455
|
-
PcbScene3dBoardShapeFactory.#PAD_HOLE_SHAPE_SLOT &&
|
|
456
|
-
Number(pad?.holeSlotLength || 0) > diameter
|
|
457
|
-
? Number(pad?.holeSlotLength || 0)
|
|
458
|
-
: null
|
|
459
|
-
platedKeys.add(
|
|
460
|
-
PcbScene3dBoardShapeFactory.#buildDrillSpecKey({
|
|
461
|
-
x: Number(pad?.x || 0),
|
|
462
|
-
y: Number(pad?.y || 0),
|
|
463
|
-
diameter,
|
|
464
|
-
slotLength,
|
|
465
|
-
rotationDeg:
|
|
466
|
-
slotLength === null
|
|
467
|
-
? 0
|
|
468
|
-
: PcbScene3dBoardShapeFactory.#normalizeAngle(
|
|
469
|
-
Number(pad?.rotation || 0) +
|
|
470
|
-
Number(pad?.holeRotation || 0)
|
|
471
|
-
)
|
|
472
|
-
})
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail).filter(
|
|
477
|
-
(drillSpec) =>
|
|
478
|
-
platedKeys.has(
|
|
479
|
-
PcbScene3dBoardShapeFactory.#buildDrillSpecKey(drillSpec)
|
|
480
|
-
)
|
|
481
|
-
)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Checks whether one pad has copper larger than its drill aperture.
|
|
486
|
-
* @param {any} pad
|
|
487
|
-
* @param {number} diameter
|
|
488
|
-
* @returns {boolean}
|
|
489
|
-
*/
|
|
490
|
-
static #hasPadCopperAnnulus(pad, diameter) {
|
|
491
|
-
const drillSpan = Math.max(diameter, Number(pad?.holeSlotLength || 0))
|
|
492
|
-
|
|
493
|
-
return [
|
|
494
|
-
pad?.sizeTopX,
|
|
495
|
-
pad?.sizeTopY,
|
|
496
|
-
pad?.sizeMidX,
|
|
497
|
-
pad?.sizeMidY,
|
|
498
|
-
pad?.sizeBottomX,
|
|
499
|
-
pad?.sizeBottomY
|
|
500
|
-
].some(
|
|
501
|
-
(size) =>
|
|
502
|
-
Number(size || 0) >
|
|
503
|
-
drillSpan + PcbScene3dBoardShapeFactory.#GEOMETRY_EPSILON
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
425
|
/**
|
|
508
426
|
* Builds one closed contour and bounds from sampled path points.
|
|
509
427
|
* @param {any[]} points
|
|
@@ -970,29 +888,4 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
970
888
|
|
|
971
889
|
return dx * dx + dy * dy
|
|
972
890
|
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Builds a stable drill spec key.
|
|
976
|
-
* @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
|
|
977
|
-
* @returns {string}
|
|
978
|
-
*/
|
|
979
|
-
static #buildDrillSpecKey(drillSpec) {
|
|
980
|
-
return [
|
|
981
|
-
Number(drillSpec.x || 0).toFixed(4),
|
|
982
|
-
Number(drillSpec.y || 0).toFixed(4),
|
|
983
|
-
Number(drillSpec.diameter || 0).toFixed(4),
|
|
984
|
-
Number(drillSpec.slotLength || 0).toFixed(4),
|
|
985
|
-
Number(drillSpec.rotationDeg || 0).toFixed(4)
|
|
986
|
-
].join(':')
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Normalizes one angle into the inclusive `[0, 360)` range.
|
|
991
|
-
* @param {number} angleDeg
|
|
992
|
-
* @returns {number}
|
|
993
|
-
*/
|
|
994
|
-
static #normalizeAngle(angleDeg) {
|
|
995
|
-
const normalized = Number(angleDeg || 0) % 360
|
|
996
|
-
return normalized < 0 ? normalized + 360 : normalized
|
|
997
|
-
}
|
|
998
891
|
}
|
|
@@ -43,48 +43,71 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
43
43
|
return group
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sceneDescription?.sourceFormat
|
|
57
|
-
)
|
|
58
|
-
const bottomMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
59
|
-
THREE,
|
|
60
|
-
board,
|
|
61
|
-
THREE.BackSide,
|
|
62
|
-
sceneDescription?.sourceFormat
|
|
63
|
-
)
|
|
64
|
-
const z = Number(board?.thicknessMil || 0) / 2
|
|
65
|
-
|
|
66
|
-
group.add(
|
|
67
|
-
PcbScene3dBoardSolderMaskFactory.#buildSurfaceMesh(
|
|
46
|
+
const contours = PcbScene3dBoardSolderMaskFactory.#boardContours(board)
|
|
47
|
+
contours.forEach((contour, index) => {
|
|
48
|
+
const geometry =
|
|
49
|
+
PcbScene3dBoardSolderMaskFactory.#buildMaskGeometry(
|
|
50
|
+
THREE,
|
|
51
|
+
contour,
|
|
52
|
+
sceneDescription?.detail || {},
|
|
53
|
+
normalizeBoardPoint
|
|
54
|
+
)
|
|
55
|
+
const topMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
68
56
|
THREE,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'board-solder-mask-top'
|
|
57
|
+
contour,
|
|
58
|
+
THREE.FrontSide,
|
|
59
|
+
sceneDescription?.sourceFormat
|
|
73
60
|
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
const bottomMaterial =
|
|
62
|
+
PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
63
|
+
THREE,
|
|
64
|
+
contour,
|
|
65
|
+
THREE.BackSide,
|
|
66
|
+
sceneDescription?.sourceFormat
|
|
67
|
+
)
|
|
68
|
+
const z = Number(contour?.thicknessMil || 0) / 2
|
|
69
|
+
const suffix = contours.length === 1 ? '' : '-' + (index + 1)
|
|
70
|
+
|
|
71
|
+
group.add(
|
|
72
|
+
PcbScene3dBoardSolderMaskFactory.#buildSurfaceMesh(
|
|
73
|
+
THREE,
|
|
74
|
+
geometry,
|
|
75
|
+
topMaterial,
|
|
76
|
+
z + PcbScene3dBoardSolderMaskFactory.#FACE_Z_OFFSET_MIL,
|
|
77
|
+
'board-solder-mask-top' + suffix
|
|
78
|
+
)
|
|
82
79
|
)
|
|
83
|
-
|
|
80
|
+
group.add(
|
|
81
|
+
PcbScene3dBoardSolderMaskFactory.#buildSurfaceMesh(
|
|
82
|
+
THREE,
|
|
83
|
+
geometry,
|
|
84
|
+
bottomMaterial,
|
|
85
|
+
-z - PcbScene3dBoardSolderMaskFactory.#FACE_Z_OFFSET_MIL,
|
|
86
|
+
'board-solder-mask-bottom' + suffix
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
})
|
|
84
90
|
|
|
85
91
|
return group
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Resolves independently renderable solder-mask contours.
|
|
96
|
+
* @param {object} board Aggregate board metadata.
|
|
97
|
+
* @returns {object[]}
|
|
98
|
+
*/
|
|
99
|
+
static #boardContours(board) {
|
|
100
|
+
const contours = Array.isArray(board?.contours)
|
|
101
|
+
? board.contours.filter(Boolean)
|
|
102
|
+
: []
|
|
103
|
+
if (!contours.length) return [board]
|
|
104
|
+
return contours.map((contour) => ({
|
|
105
|
+
...board,
|
|
106
|
+
...contour,
|
|
107
|
+
contours: []
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
/**
|
|
89
112
|
* Builds one flat solder-mask face mesh.
|
|
90
113
|
* @param {any} THREE
|
|
@@ -609,10 +632,13 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
609
632
|
*/
|
|
610
633
|
static #buildMaterial(THREE, board, side, sourceFormat) {
|
|
611
634
|
return new THREE.MeshStandardMaterial({
|
|
612
|
-
color: PcbScene3dBoardMaterialPalette.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
635
|
+
color: PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(
|
|
636
|
+
board,
|
|
637
|
+
{
|
|
638
|
+
hasBoardAssemblyModel: true,
|
|
639
|
+
sourceFormat
|
|
640
|
+
}
|
|
641
|
+
),
|
|
616
642
|
...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
|
|
617
643
|
polygonOffset: true,
|
|
618
644
|
polygonOffsetFactor: -1,
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
CircuitJsonIndexer,
|
|
2
|
+
CircuitJsonDocumentContext,
|
|
4
3
|
CircuitJsonUnits
|
|
5
4
|
} from 'circuitjson-toolkit'
|
|
5
|
+
import { CircuitJsonPcbHolePrimitiveModel } from 'circuitjson-toolkit/extensions'
|
|
6
6
|
import { PcbScene3dCircuitJsonGeometry } from './PcbScene3dCircuitJsonGeometry.mjs'
|
|
7
|
+
import { PcbScene3dCircuitJsonDrillDetail } from './PcbScene3dCircuitJsonDrillDetail.mjs'
|
|
7
8
|
import { PcbScene3dCircuitJsonModelTransform } from './PcbScene3dCircuitJsonModelTransform.mjs'
|
|
8
9
|
import { PcbScene3dCircuitJsonCopperPourBuilder } from './PcbScene3dCircuitJsonCopperPourBuilder.mjs'
|
|
9
10
|
import { PcbScene3dCircuitJsonSilkscreenBuilder } from './PcbScene3dCircuitJsonSilkscreenBuilder.mjs'
|
|
10
11
|
import { PcbScene3dFootprintBodyBuilder } from './PcbScene3dFootprintBodyBuilder.mjs'
|
|
11
12
|
import { PcbScene3dCircuitJsonLayer } from './PcbScene3dCircuitJsonLayer.mjs'
|
|
13
|
+
import { PcbScene3dCircuitJsonInput } from './PcbScene3dCircuitJsonInput.mjs'
|
|
12
14
|
import { PcbScene3dCircuitJsonTraceRouteBuilder } from './PcbScene3dCircuitJsonTraceRouteBuilder.mjs'
|
|
13
15
|
import { PcbScene3dCircuitJsonThermalSpokeBuilder } from './PcbScene3dCircuitJsonThermalSpokeBuilder.mjs'
|
|
14
16
|
import { PcbScene3dCircuitJsonModelUrlResolver } from './PcbScene3dCircuitJsonModelUrlResolver.mjs'
|
|
15
17
|
import { PcbScene3dCircuitJsonSolderPasteBuilder } from './PcbScene3dCircuitJsonSolderPasteBuilder.mjs'
|
|
18
|
+
import { CircuitJsonCadModelAssetResolver } from './CircuitJsonCadModelAssetResolver.mjs'
|
|
19
|
+
import { PcbScene3dDescriptorSafeRecord } from './PcbScene3dDescriptorSafeRecord.mjs'
|
|
20
|
+
import { PcbScene3dCircuitJsonModelAsset } from './PcbScene3dCircuitJsonModelAsset.mjs'
|
|
16
21
|
|
|
17
22
|
const DEFAULT_COMPONENT_HEIGHT_MIL = 60
|
|
18
23
|
const RECTANGULAR_PAD_SHAPE = 2
|
|
@@ -39,7 +44,7 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
39
44
|
* @returns {boolean}
|
|
40
45
|
*/
|
|
41
46
|
static isCircuitJsonModel(value) {
|
|
42
|
-
return
|
|
47
|
+
return PcbScene3dCircuitJsonInput.isModel(value)
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
@@ -52,29 +57,61 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
52
57
|
return false
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
if (
|
|
60
|
+
if (!Array.isArray(value)) return true
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
String(
|
|
64
|
+
PcbScene3dCircuitJsonAdapter.#ownData(value, 'sourceFormat') ||
|
|
65
|
+
''
|
|
66
|
+
) === 'circuitjson'
|
|
67
|
+
) {
|
|
56
68
|
return true
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
return !PcbScene3dCircuitJsonAdapter.#hasCompatibilityModel(value)
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Normalizes and validates one candidate through the shared CircuitJSON
|
|
76
|
+
* context while reusing an existing proof and element index.
|
|
77
|
+
* @param {unknown} circuitJson CircuitJSON model, document, or context.
|
|
78
|
+
* @returns {CircuitJsonDocumentContext} Prepared shared context.
|
|
79
|
+
*/
|
|
80
|
+
static prepare(circuitJson) {
|
|
81
|
+
return CircuitJsonDocumentContext.prepare(circuitJson, {
|
|
82
|
+
indexes: ['elements']
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
/**
|
|
63
87
|
* Builds the internal render model used by the Three.js runtime.
|
|
64
|
-
* @param {
|
|
88
|
+
* @param {unknown} circuitJson CircuitJSON model, document envelope, or prepared context.
|
|
65
89
|
* @param {{ modelUrlResolver?: (url: string, context: object) => string | object | null | undefined, projectBaseUrl?: string, boardDrillQuality?: string, showPcbNotes?: boolean, showPcbPaste?: boolean }} [options] Adapter options.
|
|
66
90
|
* @returns {object}
|
|
67
91
|
*/
|
|
68
92
|
static build(circuitJson, options = {}) {
|
|
69
|
-
|
|
70
|
-
const index =
|
|
71
|
-
const
|
|
72
|
-
|
|
93
|
+
const context = PcbScene3dCircuitJsonAdapter.prepare(circuitJson)
|
|
94
|
+
const index = context.getIndex('elements')
|
|
95
|
+
const resolvedOptions =
|
|
96
|
+
PcbScene3dCircuitJsonAdapter.#hasExternalModelReferences(index)
|
|
97
|
+
? CircuitJsonCadModelAssetResolver.withContextAssetResolver(
|
|
98
|
+
options,
|
|
99
|
+
context
|
|
100
|
+
)
|
|
101
|
+
: options
|
|
102
|
+
const board = PcbScene3dCircuitJsonGeometry.buildBoard(
|
|
103
|
+
index,
|
|
104
|
+
resolvedOptions
|
|
105
|
+
)
|
|
106
|
+
const detail = PcbScene3dCircuitJsonAdapter.#buildDetail(
|
|
107
|
+
index,
|
|
108
|
+
resolvedOptions
|
|
109
|
+
)
|
|
73
110
|
const externalPlacements =
|
|
74
111
|
PcbScene3dCircuitJsonAdapter.#buildExternalPlacements(
|
|
75
112
|
index,
|
|
76
113
|
board,
|
|
77
|
-
|
|
114
|
+
resolvedOptions
|
|
78
115
|
)
|
|
79
116
|
const externalPlacementByComponentId =
|
|
80
117
|
PcbScene3dCircuitJsonAdapter.#externalPlacementByComponentId(
|
|
@@ -104,6 +141,33 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
104
141
|
}
|
|
105
142
|
}
|
|
106
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Returns whether CAD rows reference models that may need asset lookup.
|
|
146
|
+
* @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
static #hasExternalModelReferences(index) {
|
|
150
|
+
return (index.elementsByType.get('cad_component') || []).some(
|
|
151
|
+
(component) =>
|
|
152
|
+
MODEL_URL_FIELDS.some(([field]) =>
|
|
153
|
+
String(
|
|
154
|
+
PcbScene3dCircuitJsonAdapter.#ownData(
|
|
155
|
+
component,
|
|
156
|
+
field
|
|
157
|
+
) || ''
|
|
158
|
+
).trim()
|
|
159
|
+
) ||
|
|
160
|
+
Boolean(
|
|
161
|
+
PcbScene3dCircuitJsonModelAsset.reference(
|
|
162
|
+
PcbScene3dCircuitJsonAdapter.#ownData(
|
|
163
|
+
component,
|
|
164
|
+
'model_asset'
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
107
171
|
/**
|
|
108
172
|
* Returns true for parser-produced arrays that already carry a legacy
|
|
109
173
|
* renderer model and must keep using their format-specific scene builder.
|
|
@@ -111,7 +175,27 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
111
175
|
* @returns {boolean}
|
|
112
176
|
*/
|
|
113
177
|
static #hasCompatibilityModel(value) {
|
|
114
|
-
return
|
|
178
|
+
return ['pcb', 'schematic', 'bom'].some((name) =>
|
|
179
|
+
Boolean(PcbScene3dCircuitJsonAdapter.#ownData(value, name))
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Reads one own data property without invoking caller accessors.
|
|
185
|
+
* @param {unknown} value Record candidate.
|
|
186
|
+
* @param {string} name Property name.
|
|
187
|
+
* @returns {unknown} Own data value or undefined.
|
|
188
|
+
*/
|
|
189
|
+
static #ownData(value, name) {
|
|
190
|
+
if (!value || typeof value !== 'object') return undefined
|
|
191
|
+
try {
|
|
192
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name)
|
|
193
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
194
|
+
? descriptor.value
|
|
195
|
+
: undefined
|
|
196
|
+
} catch {
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
115
199
|
}
|
|
116
200
|
|
|
117
201
|
/**
|
|
@@ -405,12 +489,16 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
405
489
|
const match = MODEL_URL_FIELDS.find(([field]) =>
|
|
406
490
|
String(cadComponent?.[field] || '').trim()
|
|
407
491
|
)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
492
|
+
const reference = match
|
|
493
|
+
? {
|
|
494
|
+
field: match[0],
|
|
495
|
+
format: match[1],
|
|
496
|
+
sourceUrl: String(cadComponent?.[match[0]] || '').trim()
|
|
497
|
+
}
|
|
498
|
+
: PcbScene3dCircuitJsonAdapter.#modelAssetReference(cadComponent)
|
|
499
|
+
if (!reference) return null
|
|
500
|
+
|
|
501
|
+
const { field, format, sourceUrl } = reference
|
|
414
502
|
const context = {
|
|
415
503
|
format,
|
|
416
504
|
field,
|
|
@@ -432,6 +520,18 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
432
520
|
}
|
|
433
521
|
}
|
|
434
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Resolves a canonical `model_asset` fallback reference.
|
|
525
|
+
* @param {object} cadComponent CAD component element.
|
|
526
|
+
* @returns {{ field: string, format: string, sourceUrl: string } | null}
|
|
527
|
+
*/
|
|
528
|
+
static #modelAssetReference(cadComponent) {
|
|
529
|
+
const reference = PcbScene3dCircuitJsonModelAsset.reference(
|
|
530
|
+
cadComponent?.model_asset
|
|
531
|
+
)
|
|
532
|
+
return reference ? { field: 'model_asset', ...reference } : null
|
|
533
|
+
}
|
|
534
|
+
|
|
435
535
|
/**
|
|
436
536
|
* Applies optional caller-owned model URL resolution.
|
|
437
537
|
* @param {string} sourceUrl Source URL.
|
|
@@ -447,7 +547,7 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
447
547
|
return { resolvedUrl: resolved }
|
|
448
548
|
}
|
|
449
549
|
if (resolved && typeof resolved === 'object') {
|
|
450
|
-
return
|
|
550
|
+
return PcbScene3dDescriptorSafeRecord.copy(resolved)
|
|
451
551
|
}
|
|
452
552
|
}
|
|
453
553
|
|
|
@@ -663,13 +763,20 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
663
763
|
static #buildPlatedHoles(index) {
|
|
664
764
|
return (index.elementsByType.get('pcb_plated_hole') || []).map(
|
|
665
765
|
(hole) => {
|
|
766
|
+
const geometry = CircuitJsonPcbHolePrimitiveModel.build(hole, {
|
|
767
|
+
x: Number(hole?.x || 0),
|
|
768
|
+
y: Number(hole?.y || 0)
|
|
769
|
+
})
|
|
666
770
|
const size =
|
|
667
|
-
PcbScene3dCircuitJsonAdapter.#platedHoleOuterSize(
|
|
771
|
+
PcbScene3dCircuitJsonAdapter.#platedHoleOuterSize(geometry)
|
|
668
772
|
const center = CircuitJsonUnits.pointMmToMil({
|
|
669
773
|
x: hole?.x,
|
|
670
774
|
y: hole?.y
|
|
671
775
|
})
|
|
672
|
-
const drill = PcbScene3dCircuitJsonGeometry.holeDrillSpec(
|
|
776
|
+
const drill = PcbScene3dCircuitJsonGeometry.holeDrillSpec(
|
|
777
|
+
hole,
|
|
778
|
+
geometry
|
|
779
|
+
)
|
|
673
780
|
const exposesCopper =
|
|
674
781
|
PcbScene3dCircuitJsonAdapter.#exposesCopperThroughMask(
|
|
675
782
|
hole,
|
|
@@ -678,19 +785,18 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
678
785
|
return {
|
|
679
786
|
x: center.x,
|
|
680
787
|
y: center.y,
|
|
681
|
-
rotation: Number(
|
|
682
|
-
shapeTop: PcbScene3dCircuitJsonAdapter.#padShape(
|
|
683
|
-
shapeMid: PcbScene3dCircuitJsonAdapter.#padShape(
|
|
684
|
-
shapeBottom:
|
|
788
|
+
rotation: Number(geometry.rotation || 0),
|
|
789
|
+
shapeTop: PcbScene3dCircuitJsonAdapter.#padShape(geometry),
|
|
790
|
+
shapeMid: PcbScene3dCircuitJsonAdapter.#padShape(geometry),
|
|
791
|
+
shapeBottom:
|
|
792
|
+
PcbScene3dCircuitJsonAdapter.#padShape(geometry),
|
|
685
793
|
sizeTopX: size.width,
|
|
686
794
|
sizeTopY: size.height,
|
|
687
795
|
sizeMidX: size.width,
|
|
688
796
|
sizeMidY: size.height,
|
|
689
797
|
sizeBottomX: size.width,
|
|
690
798
|
sizeBottomY: size.height,
|
|
691
|
-
|
|
692
|
-
holeSlotLength: drill.slotLength,
|
|
693
|
-
holeRotation: drill.rotationDeg,
|
|
799
|
+
...PcbScene3dCircuitJsonDrillDetail.fields(drill),
|
|
694
800
|
holeOffsetX: drill.center.x - center.x,
|
|
695
801
|
holeOffsetY: drill.center.y - center.y,
|
|
696
802
|
hasTopSolderMaskOpening: exposesCopper,
|
|
@@ -721,9 +827,7 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
721
827
|
sizeMidY: 0,
|
|
722
828
|
sizeBottomX: 0,
|
|
723
829
|
sizeBottomY: 0,
|
|
724
|
-
|
|
725
|
-
holeSlotLength: drill.slotLength,
|
|
726
|
-
holeRotation: drill.rotationDeg,
|
|
830
|
+
...PcbScene3dCircuitJsonDrillDetail.fields(drill),
|
|
727
831
|
hasTopSolderMaskOpening: false,
|
|
728
832
|
hasBottomSolderMaskOpening: false
|
|
729
833
|
}
|
|
@@ -782,10 +886,17 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
782
886
|
* @returns {number}
|
|
783
887
|
*/
|
|
784
888
|
static #roundedPadCornerRadiusPercent(pad, size) {
|
|
889
|
+
const width = CircuitJsonUnits.optionalLength(pad?.width)
|
|
890
|
+
const height = CircuitJsonUnits.optionalLength(pad?.height)
|
|
891
|
+
const radius = CircuitJsonUnits.optionalLength(pad?.radius)
|
|
892
|
+
if (width > 0 && height > 0 && radius > 0) {
|
|
893
|
+
return Math.min((radius / Math.min(width, height)) * 100, 50)
|
|
894
|
+
}
|
|
895
|
+
|
|
785
896
|
const shortestSide = Math.min(Number(size.width), Number(size.height))
|
|
786
|
-
const
|
|
787
|
-
if (shortestSide > 0 &&
|
|
788
|
-
return Math.min((
|
|
897
|
+
const radiusMil = CircuitJsonUnits.mmToMil(pad?.radius, 0)
|
|
898
|
+
if (shortestSide > 0 && radiusMil > 0) {
|
|
899
|
+
return Math.min((radiusMil / shortestSide) * 100, 50)
|
|
789
900
|
}
|
|
790
901
|
|
|
791
902
|
return 50
|
|
@@ -793,23 +904,13 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
793
904
|
|
|
794
905
|
/**
|
|
795
906
|
* Resolves plated-hole copper size from CircuitJSON fields.
|
|
796
|
-
* @param {object}
|
|
907
|
+
* @param {object} geometry Shared drilled-primitive geometry.
|
|
797
908
|
* @returns {{ width: number, height: number }}
|
|
798
909
|
*/
|
|
799
|
-
static #platedHoleOuterSize(
|
|
800
|
-
if (Number(hole?.outer_diameter || 0) > 0) {
|
|
801
|
-
const diameter = CircuitJsonUnits.mmToMil(hole.outer_diameter, 1)
|
|
802
|
-
return { width: diameter, height: diameter }
|
|
803
|
-
}
|
|
910
|
+
static #platedHoleOuterSize(geometry) {
|
|
804
911
|
return {
|
|
805
|
-
width: CircuitJsonUnits.mmToMil(
|
|
806
|
-
|
|
807
|
-
1
|
|
808
|
-
),
|
|
809
|
-
height: CircuitJsonUnits.mmToMil(
|
|
810
|
-
hole?.outer_height || hole?.rect_pad_height,
|
|
811
|
-
1
|
|
812
|
-
)
|
|
912
|
+
width: CircuitJsonUnits.mmToMil(geometry?.width, 1),
|
|
913
|
+
height: CircuitJsonUnits.mmToMil(geometry?.height, 1)
|
|
813
914
|
}
|
|
814
915
|
}
|
|
815
916
|
|
|
@@ -820,7 +921,9 @@ export class PcbScene3dCircuitJsonAdapter {
|
|
|
820
921
|
*/
|
|
821
922
|
static #padShape(pad) {
|
|
822
923
|
const shape = String(pad?.shape || '')
|
|
823
|
-
return shape.includes('rect') ||
|
|
924
|
+
return shape.includes('rect') ||
|
|
925
|
+
shape.includes('polygon') ||
|
|
926
|
+
shape.endsWith('pill')
|
|
824
927
|
? RECTANGULAR_PAD_SHAPE
|
|
825
928
|
: CIRCULAR_PAD_SHAPE
|
|
826
929
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps canonical drill descriptors onto the viewer's compact pad-detail shape.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dCircuitJsonDrillDetail {
|
|
5
|
+
static #RECTANGULAR_HOLE_SHAPE = 1
|
|
6
|
+
static #SLOTTED_HOLE_SHAPE = 2
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds common viewer drill fields without losing canonical aperture shape.
|
|
10
|
+
* @param {{ diameter: number, width: number, height: number, shape: 'circle' | 'pill' | 'rect', slotLength: number, rotationDeg: number }} drill Canonical drill descriptor.
|
|
11
|
+
* @returns {{ holeDiameter: number, holeSlotLength: number, holeWidth: number, holeHeight: number, holeShape: number | null, holeRotation: number }} Viewer drill fields.
|
|
12
|
+
*/
|
|
13
|
+
static fields(drill) {
|
|
14
|
+
return {
|
|
15
|
+
holeDiameter: drill.diameter,
|
|
16
|
+
holeSlotLength: drill.slotLength,
|
|
17
|
+
holeWidth: drill.width,
|
|
18
|
+
holeHeight: drill.height,
|
|
19
|
+
holeShape:
|
|
20
|
+
drill.shape === 'rect'
|
|
21
|
+
? PcbScene3dCircuitJsonDrillDetail.#RECTANGULAR_HOLE_SHAPE
|
|
22
|
+
: drill.slotLength > drill.diameter
|
|
23
|
+
? PcbScene3dCircuitJsonDrillDetail.#SLOTTED_HOLE_SHAPE
|
|
24
|
+
: null,
|
|
25
|
+
holeRotation: drill.rotationDeg
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Object.freeze(PcbScene3dCircuitJsonDrillDetail.prototype)
|
|
31
|
+
Object.freeze(PcbScene3dCircuitJsonDrillDetail)
|