pcb-scene3d-viewer 1.1.47 → 1.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +10 -4
- package/src/PcbScene3dBoardCutoutPathFactory.mjs +83 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +17 -2
- package/src/PcbScene3dBoardShapeFactory.mjs +16 -3
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +20 -10
- package/src/PcbScene3dController.mjs +20 -0
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +47 -4
- package/src/PcbScene3dDrillVoidFactory.mjs +15 -71
- package/src/PcbScene3dExternalModels.mjs +16 -3
- package/src/PcbScene3dGeometryZCompressor.mjs +5 -2
- package/src/PcbScene3dMaskCoveredCopperMaterial.mjs +1 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +172 -5
- package/src/PcbScene3dOutlineBuilder.mjs +54 -0
- package/src/PcbScene3dRuntime.mjs +1 -1
- package/src/PcbScene3dShellRenderer.mjs +38 -4
package/package.json
CHANGED
|
@@ -19,9 +19,10 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
19
19
|
* PCB-derived substrate, copper, and silkscreen remain the visual source.
|
|
20
20
|
* @param {any} modelGroup Loaded board assembly group.
|
|
21
21
|
* @param {{ widthMil?: number, heightMil?: number, thicknessMil?: number } | null | undefined} board Board dimensions.
|
|
22
|
+
* @param {{ sourceFormat?: string }} [options] Presentation options.
|
|
22
23
|
* @returns {void}
|
|
23
24
|
*/
|
|
24
|
-
static apply(modelGroup, board) {
|
|
25
|
+
static apply(modelGroup, board, options = {}) {
|
|
25
26
|
const meshRecords =
|
|
26
27
|
PcbScene3dBoardAssemblyPresentation.#collectMeshRecords(modelGroup)
|
|
27
28
|
const boardBounds =
|
|
@@ -30,7 +31,10 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
30
31
|
board
|
|
31
32
|
)
|
|
32
33
|
const surfaceColor =
|
|
33
|
-
PcbScene3dBoardAssemblyPresentation.#resolveSurfaceColor(
|
|
34
|
+
PcbScene3dBoardAssemblyPresentation.#resolveSurfaceColor(
|
|
35
|
+
board,
|
|
36
|
+
options
|
|
37
|
+
)
|
|
34
38
|
const edgeColor =
|
|
35
39
|
PcbScene3dBoardAssemblyPresentation.#resolveEdgeColor(board)
|
|
36
40
|
const importedSurfaceColor =
|
|
@@ -412,11 +416,13 @@ export class PcbScene3dBoardAssemblyPresentation {
|
|
|
412
416
|
/**
|
|
413
417
|
* Resolves the app-level substrate color for board assembly meshes.
|
|
414
418
|
* @param {{ surfaceColor?: number } | null | undefined} board Board dimensions.
|
|
419
|
+
* @param {{ sourceFormat?: string }} options Presentation options.
|
|
415
420
|
* @returns {number}
|
|
416
421
|
*/
|
|
417
|
-
static #resolveSurfaceColor(board) {
|
|
422
|
+
static #resolveSurfaceColor(board, options) {
|
|
418
423
|
return PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
|
|
419
|
-
hasBoardAssemblyModel: true
|
|
424
|
+
hasBoardAssemblyModel: true,
|
|
425
|
+
sourceFormat: options?.sourceFormat
|
|
420
426
|
})
|
|
421
427
|
}
|
|
422
428
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds normalized Three.js paths for explicit board cutouts.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dBoardCutoutPathFactory {
|
|
5
|
+
/**
|
|
6
|
+
* Resolves explicit board cutout paths.
|
|
7
|
+
* @param {any} THREE Three.js namespace.
|
|
8
|
+
* @param {{ cutouts?: any[] }} board Board metadata.
|
|
9
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
10
|
+
* @returns {{ path: any, points: { x: number, y: number }[] }[]}
|
|
11
|
+
*/
|
|
12
|
+
static resolve(THREE, board, normalizeBoardPoint) {
|
|
13
|
+
return (Array.isArray(board?.cutouts) ? board.cutouts : [])
|
|
14
|
+
.map((cutout) =>
|
|
15
|
+
PcbScene3dBoardCutoutPathFactory.#buildPath(
|
|
16
|
+
THREE,
|
|
17
|
+
cutout,
|
|
18
|
+
normalizeBoardPoint
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds one explicit board cutout path.
|
|
26
|
+
* @param {any} THREE Three.js namespace.
|
|
27
|
+
* @param {any} cutout Source cutout.
|
|
28
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
29
|
+
* @returns {{ path: any, points: { x: number, y: number }[] } | null}
|
|
30
|
+
*/
|
|
31
|
+
static #buildPath(THREE, cutout, normalizeBoardPoint) {
|
|
32
|
+
const points = PcbScene3dBoardCutoutPathFactory.#sourcePoints(cutout)
|
|
33
|
+
.map((point) =>
|
|
34
|
+
PcbScene3dBoardCutoutPathFactory.#normalizePoint(
|
|
35
|
+
point,
|
|
36
|
+
normalizeBoardPoint
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
|
|
41
|
+
if (points.length < 3) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const path = new THREE.Path()
|
|
46
|
+
path.moveTo(points[0].x, points[0].y)
|
|
47
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
48
|
+
path.lineTo(points[index].x, points[index].y)
|
|
49
|
+
}
|
|
50
|
+
path.closePath()
|
|
51
|
+
|
|
52
|
+
return { path, points }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves one cutout's point list.
|
|
57
|
+
* @param {any} cutout Source cutout.
|
|
58
|
+
* @returns {any[]}
|
|
59
|
+
*/
|
|
60
|
+
static #sourcePoints(cutout) {
|
|
61
|
+
if (Array.isArray(cutout?.points)) {
|
|
62
|
+
return cutout.points
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (Array.isArray(cutout?.vertices)) {
|
|
66
|
+
return cutout.vertices
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Array.isArray(cutout) ? cutout : []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalizes one source point into local board shape coordinates.
|
|
74
|
+
* @param {any} point Source point.
|
|
75
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
76
|
+
* @returns {{ x: number, y: number } | null}
|
|
77
|
+
*/
|
|
78
|
+
static #normalizePoint(point, normalizeBoardPoint) {
|
|
79
|
+
const x = Number(point?.x ?? point?.[0])
|
|
80
|
+
const y = Number(point?.y ?? point?.[1])
|
|
81
|
+
return Number.isFinite(x + y) ? normalizeBoardPoint(x, y) : null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -7,11 +7,16 @@ export class PcbScene3dBoardMaterialPalette {
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolves the solder-mask face color for the generated board shell.
|
|
9
9
|
* @param {{ surfaceColor?: number } | undefined} board Board metadata.
|
|
10
|
-
* @param {{ hasBoardAssemblyModel?: boolean }} [options] Scene options.
|
|
10
|
+
* @param {{ hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options] Scene options.
|
|
11
11
|
* @returns {number}
|
|
12
12
|
*/
|
|
13
13
|
static resolveSurfaceColor(board, options = {}) {
|
|
14
|
-
if (
|
|
14
|
+
if (
|
|
15
|
+
options.hasBoardAssemblyModel &&
|
|
16
|
+
!PcbScene3dBoardMaterialPalette.#shouldPreserveAuthoredSurfaceColor(
|
|
17
|
+
options
|
|
18
|
+
)
|
|
19
|
+
) {
|
|
15
20
|
return PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -20,6 +25,16 @@ export class PcbScene3dBoardMaterialPalette {
|
|
|
20
25
|
: PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Returns true when the source format supplies display-stable board colors.
|
|
30
|
+
* @param {{ sourceFormat?: string }} options Scene options.
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
static #shouldPreserveAuthoredSurfaceColor(options) {
|
|
34
|
+
const sourceFormat = String(options?.sourceFormat || '').toLowerCase()
|
|
35
|
+
return sourceFormat === 'altium' || sourceFormat.startsWith('altium-')
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/**
|
|
24
39
|
* Resolves whether the generated board face should render.
|
|
25
40
|
* @param {{ hasBoardAssemblyModel?: boolean }} [options] Scene options.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PcbScene3dBoardEdgeCutoutBuilder } from './PcbScene3dBoardEdgeCutoutBuilder.mjs'
|
|
2
|
+
import { PcbScene3dBoardCutoutPathFactory } from './PcbScene3dBoardCutoutPathFactory.mjs'
|
|
2
3
|
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
3
4
|
import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
4
5
|
|
|
@@ -36,6 +37,11 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
36
37
|
)
|
|
37
38
|
const contourPoints =
|
|
38
39
|
PcbScene3dBoardEdgeCutoutBuilder.resolveShapePoints(baseShape)
|
|
40
|
+
const boardCutouts = PcbScene3dBoardCutoutPathFactory.resolve(
|
|
41
|
+
THREE,
|
|
42
|
+
board,
|
|
43
|
+
normalizeBoardPoint
|
|
44
|
+
)
|
|
39
45
|
const drillCutouts = PcbScene3dBoardShapeFactory.#resolveDrillCutouts(
|
|
40
46
|
THREE,
|
|
41
47
|
detail,
|
|
@@ -61,12 +67,20 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
61
67
|
const finalContourPoints = edgeCutouts.length
|
|
62
68
|
? PcbScene3dBoardEdgeCutoutBuilder.resolveShapePoints(shape)
|
|
63
69
|
: contourPoints
|
|
64
|
-
|
|
70
|
+
for (const cutout of boardCutouts) {
|
|
71
|
+
if (
|
|
72
|
+
PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
|
|
73
|
+
cutout.points,
|
|
74
|
+
finalContourPoints
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
shape.holes.push(cutout.path)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
for (const cutout of drillCutouts) {
|
|
66
81
|
if (edgeCutouts.includes(cutout)) {
|
|
67
82
|
continue
|
|
68
83
|
}
|
|
69
|
-
|
|
70
84
|
if (
|
|
71
85
|
!cutout.isCircular ||
|
|
72
86
|
PcbScene3dBoardEdgeCutoutBuilder.isHoleInsideContour(
|
|
@@ -77,7 +91,6 @@ export class PcbScene3dBoardShapeFactory {
|
|
|
77
91
|
shape.holes.push(cutout.path)
|
|
78
92
|
}
|
|
79
93
|
}
|
|
80
|
-
|
|
81
94
|
return shape
|
|
82
95
|
}
|
|
83
96
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
|
+
import { PcbScene3dBoardCutoutPathFactory } from './PcbScene3dBoardCutoutPathFactory.mjs'
|
|
2
3
|
import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
|
|
3
4
|
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
4
5
|
import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
|
|
@@ -51,12 +52,14 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
51
52
|
const topMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
52
53
|
THREE,
|
|
53
54
|
board,
|
|
54
|
-
THREE.FrontSide
|
|
55
|
+
THREE.FrontSide,
|
|
56
|
+
sceneDescription?.sourceFormat
|
|
55
57
|
)
|
|
56
58
|
const bottomMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
57
59
|
THREE,
|
|
58
60
|
board,
|
|
59
|
-
THREE.BackSide
|
|
61
|
+
THREE.BackSide,
|
|
62
|
+
sceneDescription?.sourceFormat
|
|
60
63
|
)
|
|
61
64
|
const z = Number(board?.thicknessMil || 0) / 2
|
|
62
65
|
|
|
@@ -148,6 +151,11 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
148
151
|
)
|
|
149
152
|
const contourPoints =
|
|
150
153
|
PcbScene3dBoardSolderMaskFactory.#resolveShapePoints(shape)
|
|
154
|
+
const boardCutouts = PcbScene3dBoardCutoutPathFactory.resolve(
|
|
155
|
+
THREE,
|
|
156
|
+
board,
|
|
157
|
+
normalizeBoardPoint
|
|
158
|
+
)
|
|
151
159
|
const drillCutouts =
|
|
152
160
|
PcbScene3dBoardSolderMaskFactory.#resolveDrillCutouts(
|
|
153
161
|
THREE,
|
|
@@ -155,8 +163,8 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
155
163
|
normalizeBoardPoint
|
|
156
164
|
)
|
|
157
165
|
const { shapeHoles, clippingCutouts } =
|
|
158
|
-
PcbScene3dBoardSolderMaskFactory.#
|
|
159
|
-
drillCutouts,
|
|
166
|
+
PcbScene3dBoardSolderMaskFactory.#partitionCutouts(
|
|
167
|
+
[...boardCutouts, ...drillCutouts],
|
|
160
168
|
contourPoints
|
|
161
169
|
)
|
|
162
170
|
|
|
@@ -395,16 +403,16 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
395
403
|
}
|
|
396
404
|
|
|
397
405
|
/**
|
|
398
|
-
* Splits
|
|
399
|
-
* @param {{ path: any, points: { x: number, y: number }[] }[]}
|
|
406
|
+
* Splits cutouts into safe shape holes and fallback clip polygons.
|
|
407
|
+
* @param {{ path: any, points: { x: number, y: number }[] }[]} cutouts
|
|
400
408
|
* @param {{ x: number, y: number }[]} contourPoints
|
|
401
409
|
* @returns {{ shapeHoles: { path: any, points: { x: number, y: number }[] }[], clippingCutouts: { path: any, points: { x: number, y: number }[] }[] }}
|
|
402
410
|
*/
|
|
403
|
-
static #
|
|
411
|
+
static #partitionCutouts(cutouts, contourPoints) {
|
|
404
412
|
const shapeHoles = []
|
|
405
413
|
const clippingCutouts = []
|
|
406
414
|
|
|
407
|
-
for (const cutout of Array.isArray(
|
|
415
|
+
for (const cutout of Array.isArray(cutouts) ? cutouts : []) {
|
|
408
416
|
if (
|
|
409
417
|
PcbScene3dBoardSolderMaskFactory.#isHoleInsideContour(
|
|
410
418
|
cutout.points,
|
|
@@ -596,12 +604,14 @@ export class PcbScene3dBoardSolderMaskFactory {
|
|
|
596
604
|
* @param {any} THREE
|
|
597
605
|
* @param {{ surfaceColor?: number }} board Board metadata.
|
|
598
606
|
* @param {number} side Rendered material side.
|
|
607
|
+
* @param {string | undefined} sourceFormat Scene source format.
|
|
599
608
|
* @returns {any}
|
|
600
609
|
*/
|
|
601
|
-
static #buildMaterial(THREE, board, side) {
|
|
610
|
+
static #buildMaterial(THREE, board, side, sourceFormat) {
|
|
602
611
|
return new THREE.MeshStandardMaterial({
|
|
603
612
|
color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
|
|
604
|
-
hasBoardAssemblyModel: true
|
|
613
|
+
hasBoardAssemblyModel: true,
|
|
614
|
+
sourceFormat
|
|
605
615
|
}),
|
|
606
616
|
...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
|
|
607
617
|
polygonOffset: true,
|
|
@@ -474,6 +474,7 @@ export class PcbScene3dController {
|
|
|
474
474
|
this.#handleRuntimeSelection(selection),
|
|
475
475
|
translate: this.#translate
|
|
476
476
|
})
|
|
477
|
+
this.#applyInitialToggles()
|
|
477
478
|
if (!this.#autoSearchMissingModels) {
|
|
478
479
|
this.#runtime?.setToggle?.('model-search-models', false)
|
|
479
480
|
}
|
|
@@ -562,6 +563,25 @@ export class PcbScene3dController {
|
|
|
562
563
|
})
|
|
563
564
|
}
|
|
564
565
|
|
|
566
|
+
/**
|
|
567
|
+
* Synchronizes runtime visibility with the shell's initial checkbox state.
|
|
568
|
+
* @returns {void}
|
|
569
|
+
*/
|
|
570
|
+
#applyInitialToggles() {
|
|
571
|
+
const toggles =
|
|
572
|
+
this.#rootNode?.querySelectorAll('[data-scene-3d-toggle]') || []
|
|
573
|
+
|
|
574
|
+
toggles.forEach((toggle) => {
|
|
575
|
+
const toggleName =
|
|
576
|
+
toggle?.getAttribute?.('data-scene-3d-toggle') || ''
|
|
577
|
+
if (!toggleName) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.#runtime?.setToggle?.(toggleName, Boolean(toggle.checked))
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
565
585
|
/**
|
|
566
586
|
* Binds the model archive export action.
|
|
567
587
|
* @returns {void}
|
|
@@ -25,9 +25,14 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
25
25
|
*/
|
|
26
26
|
static build(THREE, sceneDescription, topZ, normalizePoint) {
|
|
27
27
|
const group = new THREE.Group()
|
|
28
|
-
const drillCutouts =
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const drillCutouts = [
|
|
29
|
+
...PcbScene3dCopperDrillCutoutBuilder.resolve(
|
|
30
|
+
sceneDescription?.detail
|
|
31
|
+
),
|
|
32
|
+
...PcbScene3dCopperDetailGroupBuilder.#resolveBoardCutouts(
|
|
33
|
+
sceneDescription?.board
|
|
34
|
+
)
|
|
35
|
+
]
|
|
31
36
|
const coveredGroup =
|
|
32
37
|
PcbScene3dCopperDetailGroupBuilder.#buildCoveredGroup(
|
|
33
38
|
THREE,
|
|
@@ -187,6 +192,43 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
187
192
|
)
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Resolves explicit through-board cutouts that copper detail must not cover.
|
|
197
|
+
* @param {{ cutouts?: any[] } | undefined} board Board metadata.
|
|
198
|
+
* @returns {{ x: number, y: number }[][]}
|
|
199
|
+
*/
|
|
200
|
+
static #resolveBoardCutouts(board) {
|
|
201
|
+
return (Array.isArray(board?.cutouts) ? board.cutouts : [])
|
|
202
|
+
.map((cutout) =>
|
|
203
|
+
PcbScene3dCopperDetailGroupBuilder.#resolveBoardCutoutPoints(
|
|
204
|
+
cutout
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
.filter((points) => points.length >= 3)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolves one explicit board cutout loop in source board coordinates.
|
|
212
|
+
* @param {any} cutout Cutout source.
|
|
213
|
+
* @returns {{ x: number, y: number }[]}
|
|
214
|
+
*/
|
|
215
|
+
static #resolveBoardCutoutPoints(cutout) {
|
|
216
|
+
const points = Array.isArray(cutout?.points)
|
|
217
|
+
? cutout.points
|
|
218
|
+
: Array.isArray(cutout?.vertices)
|
|
219
|
+
? cutout.vertices
|
|
220
|
+
: Array.isArray(cutout)
|
|
221
|
+
? cutout
|
|
222
|
+
: []
|
|
223
|
+
|
|
224
|
+
return points
|
|
225
|
+
.map((point) => ({
|
|
226
|
+
x: Number(point?.x ?? point?.[0]),
|
|
227
|
+
y: Number(point?.y ?? point?.[1])
|
|
228
|
+
}))
|
|
229
|
+
.filter((point) => Number.isFinite(point.x + point.y))
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
/**
|
|
191
233
|
* Resolves solder-mask material options for covered copper relief.
|
|
192
234
|
* @param {object} sceneDescription Scene description.
|
|
@@ -199,7 +241,8 @@ export class PcbScene3dCopperDetailGroupBuilder {
|
|
|
199
241
|
{
|
|
200
242
|
hasBoardAssemblyModel: Boolean(
|
|
201
243
|
sceneDescription?.boardAssemblyModel
|
|
202
|
-
)
|
|
244
|
+
),
|
|
245
|
+
sourceFormat: sceneDescription?.sourceFormat
|
|
203
246
|
}
|
|
204
247
|
)
|
|
205
248
|
}
|
|
@@ -33,15 +33,13 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
33
33
|
return group
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const material = PcbScene3dDrillVoidFactory.#
|
|
36
|
+
const material = PcbScene3dDrillVoidFactory.#buildInteriorMaterial(
|
|
37
37
|
THREE,
|
|
38
38
|
options
|
|
39
39
|
)
|
|
40
40
|
const geometryCache = new Map()
|
|
41
41
|
const depth = Math.max(Math.abs(Number(topZ) - Number(bottomZ)), 1)
|
|
42
42
|
const centerZ = (Number(topZ || 0) + Number(bottomZ || 0)) / 2
|
|
43
|
-
const platedDrillKeys =
|
|
44
|
-
PcbScene3dDrillVoidFactory.#buildPlatedDrillKeySet(detail)
|
|
45
43
|
const edgeDrillKeys = PcbScene3dDrillVoidFactory.#buildEdgeDrillKeySet(
|
|
46
44
|
THREE,
|
|
47
45
|
detail,
|
|
@@ -52,9 +50,7 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
52
50
|
PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail).forEach(
|
|
53
51
|
(drillSpec) => {
|
|
54
52
|
if (
|
|
55
|
-
|
|
56
|
-
PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
|
|
57
|
-
) ||
|
|
53
|
+
PcbScene3dDrillVoidFactory.#isSlottedDrill(drillSpec) ||
|
|
58
54
|
edgeDrillKeys.has(
|
|
59
55
|
PcbScene3dDrillVoidFactory.#drillKey(drillSpec)
|
|
60
56
|
)
|
|
@@ -101,63 +97,6 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
101
97
|
)
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
/**
|
|
105
|
-
* Builds a lookup for drill holes that already have copper barrel geometry.
|
|
106
|
-
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
107
|
-
* @returns {Set<string>}
|
|
108
|
-
*/
|
|
109
|
-
static #buildPlatedDrillKeySet(detail) {
|
|
110
|
-
const platedDrills = new Set()
|
|
111
|
-
|
|
112
|
-
for (const via of detail?.vias || []) {
|
|
113
|
-
if (PcbScene3dDrillVoidFactory.#hasViaCopperAnnulus(via)) {
|
|
114
|
-
platedDrills.add(PcbScene3dDrillVoidFactory.#drillKey(via))
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (const pad of detail?.pads || []) {
|
|
119
|
-
if (PcbScene3dDrillVoidFactory.#hasPadCopperAnnulus(pad)) {
|
|
120
|
-
platedDrills.add(PcbScene3dDrillVoidFactory.#drillKey(pad))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return platedDrills
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Checks whether one via has a copper annulus around its drill.
|
|
129
|
-
* @param {any} via Via primitive.
|
|
130
|
-
* @returns {boolean}
|
|
131
|
-
*/
|
|
132
|
-
static #hasViaCopperAnnulus(via) {
|
|
133
|
-
const holeDiameter = Number(via?.holeDiameter || 0)
|
|
134
|
-
return (
|
|
135
|
-
holeDiameter > 0 &&
|
|
136
|
-
Number(via?.diameter || 0) > holeDiameter + 0.001
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Checks whether one pad has copper larger than its drill aperture.
|
|
142
|
-
* @param {any} pad Pad primitive.
|
|
143
|
-
* @returns {boolean}
|
|
144
|
-
*/
|
|
145
|
-
static #hasPadCopperAnnulus(pad) {
|
|
146
|
-
const holeDiameter = Number(pad?.holeDiameter || 0)
|
|
147
|
-
if (holeDiameter <= 0) {
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return [
|
|
152
|
-
pad?.sizeTopX,
|
|
153
|
-
pad?.sizeTopY,
|
|
154
|
-
pad?.sizeMidX,
|
|
155
|
-
pad?.sizeMidY,
|
|
156
|
-
pad?.sizeBottomX,
|
|
157
|
-
pad?.sizeBottomY
|
|
158
|
-
].some((size) => Number(size || 0) > holeDiameter + 0.001)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
100
|
/**
|
|
162
101
|
* Builds a stable lookup key for one circular drill.
|
|
163
102
|
* @param {{ x?: number, y?: number, holeDiameter?: number, diameter?: number }} drill
|
|
@@ -177,7 +116,7 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
177
116
|
* @param {{ color?: number }} options
|
|
178
117
|
* @returns {any}
|
|
179
118
|
*/
|
|
180
|
-
static #
|
|
119
|
+
static #buildInteriorMaterial(THREE, options) {
|
|
181
120
|
return new THREE.MeshStandardMaterial({
|
|
182
121
|
color: Number.isInteger(options?.color)
|
|
183
122
|
? options.color
|
|
@@ -188,6 +127,18 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
188
127
|
})
|
|
189
128
|
}
|
|
190
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Checks whether one drill is a routed slot.
|
|
132
|
+
* @param {{ diameter?: number, slotLength?: number | null }} drillSpec Drill spec.
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
static #isSlottedDrill(drillSpec) {
|
|
136
|
+
return (
|
|
137
|
+
Number(drillSpec?.slotLength || 0) >
|
|
138
|
+
Number(drillSpec?.diameter || 0) + 0.001
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
191
142
|
/**
|
|
192
143
|
* Builds one open circular drill-interior mesh.
|
|
193
144
|
* @param {any} THREE
|
|
@@ -208,13 +159,6 @@ export class PcbScene3dDrillVoidFactory {
|
|
|
208
159
|
material,
|
|
209
160
|
normalizeBoardPoint
|
|
210
161
|
) {
|
|
211
|
-
if (
|
|
212
|
-
Number(drillSpec?.slotLength || 0) >
|
|
213
|
-
Number(drillSpec?.diameter || 0) + 0.001
|
|
214
|
-
) {
|
|
215
|
-
return null
|
|
216
|
-
}
|
|
217
|
-
|
|
218
162
|
const point = normalizeBoardPoint(
|
|
219
163
|
Number(drillSpec?.x || 0),
|
|
220
164
|
Number(drillSpec?.y || 0)
|
|
@@ -281,7 +281,8 @@ export class PcbScene3dExternalModels {
|
|
|
281
281
|
return PcbScene3dExternalModels.#buildBoardAssemblyWrapper(
|
|
282
282
|
THREE,
|
|
283
283
|
placement,
|
|
284
|
-
modelGroup
|
|
284
|
+
modelGroup,
|
|
285
|
+
sceneDescription
|
|
285
286
|
)
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -371,13 +372,25 @@ export class PcbScene3dExternalModels {
|
|
|
371
372
|
* @param {any} THREE
|
|
372
373
|
* @param {{ positionMil?: { x?: number, y?: number, z?: number }, board?: { widthMil?: number, heightMil?: number, thicknessMil?: number }, sourceFrameScale?: { y?: number } }} placement
|
|
373
374
|
* @param {any} modelGroup
|
|
375
|
+
* @param {{ sourceFormat?: string } | null | undefined} sceneDescription Scene description.
|
|
374
376
|
* @returns {any}
|
|
375
377
|
*/
|
|
376
|
-
static #buildBoardAssemblyWrapper(
|
|
378
|
+
static #buildBoardAssemblyWrapper(
|
|
379
|
+
THREE,
|
|
380
|
+
placement,
|
|
381
|
+
modelGroup,
|
|
382
|
+
sceneDescription
|
|
383
|
+
) {
|
|
377
384
|
const wrapperGroup = new THREE.Group()
|
|
378
385
|
const positionMil = placement?.positionMil || {}
|
|
379
386
|
|
|
380
|
-
PcbScene3dBoardAssemblyPresentation.apply(
|
|
387
|
+
PcbScene3dBoardAssemblyPresentation.apply(
|
|
388
|
+
modelGroup,
|
|
389
|
+
placement?.board,
|
|
390
|
+
{
|
|
391
|
+
sourceFormat: sceneDescription?.sourceFormat
|
|
392
|
+
}
|
|
393
|
+
)
|
|
381
394
|
PcbScene3dBoardAssemblyTransform.apply(modelGroup, placement)
|
|
382
395
|
wrapperGroup.position.set(
|
|
383
396
|
Number(positionMil.x || 0),
|
|
@@ -9,13 +9,16 @@ export class PcbScene3dGeometryZCompressor {
|
|
|
9
9
|
* Rewrites mask-covered copper relief below exposed copper height.
|
|
10
10
|
* @param {any | null} mesh Mesh with a position attribute.
|
|
11
11
|
* @param {number} sourceCenterZ Original geometry center Z.
|
|
12
|
+
* @param {{ centerOffsetMil?: number }} [options] Compression options.
|
|
12
13
|
* @returns {void}
|
|
13
14
|
*/
|
|
14
|
-
static compressMaskCoveredCopperMesh(mesh, sourceCenterZ) {
|
|
15
|
+
static compressMaskCoveredCopperMesh(mesh, sourceCenterZ, options = {}) {
|
|
15
16
|
PcbScene3dGeometryZCompressor.compressMesh(
|
|
16
17
|
mesh,
|
|
17
18
|
sourceCenterZ,
|
|
18
|
-
sourceCenterZ +
|
|
19
|
+
sourceCenterZ +
|
|
20
|
+
MASK_COVERED_CENTER_OFFSET_MIL +
|
|
21
|
+
Number(options?.centerOffsetMil || 0),
|
|
19
22
|
MASK_COVERED_THICKNESS_MIL
|
|
20
23
|
)
|
|
21
24
|
}
|
|
@@ -6,7 +6,7 @@ import { PcbScene3dMaterialFinish } from './PcbScene3dMaterialFinish.mjs'
|
|
|
6
6
|
export class PcbScene3dMaskCoveredCopperMaterial {
|
|
7
7
|
static #COPPER_COLOR = 0xd9a61d
|
|
8
8
|
static #DEFAULT_SOLDER_MASK_COLOR = 0x2a5f27
|
|
9
|
-
static #COPPER_BLEND = 0.
|
|
9
|
+
static #COPPER_BLEND = 0.04
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Builds a mask-covered copper material.
|
|
@@ -4,6 +4,13 @@ import { PcbScene3dGeometryZCompressor } from './PcbScene3dGeometryZCompressor.m
|
|
|
4
4
|
* Builds side-specific copper relief groups that sit below solder mask.
|
|
5
5
|
*/
|
|
6
6
|
export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
|
|
7
|
+
static #GREEN_MASKED_COPPER_TINT = 0x1f7a34
|
|
8
|
+
static #FILL_COPPER_BLEND = 0.45
|
|
9
|
+
static #TRACK_COPPER_BLEND = 0.7
|
|
10
|
+
static #FILL_RENDER_ORDER = 10
|
|
11
|
+
static #TRACK_RENDER_ORDER = 12
|
|
12
|
+
static #TRACK_CENTER_OFFSET_MIL = 0.16
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Builds one side group from prepared mask-covered copper meshes.
|
|
9
16
|
* @param {any} THREE Three.js namespace.
|
|
@@ -26,19 +33,49 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
|
|
|
26
33
|
group,
|
|
27
34
|
trackMesh,
|
|
28
35
|
'mask-covered-copper-tracks',
|
|
29
|
-
z
|
|
36
|
+
z,
|
|
37
|
+
{
|
|
38
|
+
centerOffsetMil:
|
|
39
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
40
|
+
.#TRACK_CENTER_OFFSET_MIL,
|
|
41
|
+
copperBlend:
|
|
42
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
43
|
+
.#TRACK_COPPER_BLEND,
|
|
44
|
+
renderOrder:
|
|
45
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
46
|
+
.#TRACK_RENDER_ORDER
|
|
47
|
+
}
|
|
30
48
|
)
|
|
31
49
|
PcbScene3dMaskCoveredCopperSideGroupBuilder.#addCompressedMesh(
|
|
32
50
|
group,
|
|
33
51
|
arcMesh,
|
|
34
52
|
'mask-covered-copper-arcs',
|
|
35
|
-
z
|
|
53
|
+
z,
|
|
54
|
+
{
|
|
55
|
+
centerOffsetMil:
|
|
56
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
57
|
+
.#TRACK_CENTER_OFFSET_MIL,
|
|
58
|
+
copperBlend:
|
|
59
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
60
|
+
.#TRACK_COPPER_BLEND,
|
|
61
|
+
renderOrder:
|
|
62
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
63
|
+
.#TRACK_RENDER_ORDER
|
|
64
|
+
}
|
|
36
65
|
)
|
|
37
66
|
PcbScene3dMaskCoveredCopperSideGroupBuilder.#addCompressedMesh(
|
|
38
67
|
group,
|
|
39
68
|
fillMesh,
|
|
40
69
|
'mask-covered-copper-fills',
|
|
41
|
-
z
|
|
70
|
+
z,
|
|
71
|
+
{
|
|
72
|
+
copperBlend:
|
|
73
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
74
|
+
.#FILL_COPPER_BLEND,
|
|
75
|
+
renderOrder:
|
|
76
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
77
|
+
.#FILL_RENDER_ORDER
|
|
78
|
+
}
|
|
42
79
|
)
|
|
43
80
|
|
|
44
81
|
if (mirrorY && group.children.length) {
|
|
@@ -54,15 +91,145 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
|
|
|
54
91
|
* @param {any | null} mesh Mesh to add.
|
|
55
92
|
* @param {string} name Scene object name.
|
|
56
93
|
* @param {number} z Source center Z.
|
|
94
|
+
* @param {{ centerOffsetMil?: number, copperBlend?: number, renderOrder?: number }} [options] Presentation options.
|
|
57
95
|
* @returns {void}
|
|
58
96
|
*/
|
|
59
|
-
static #addCompressedMesh(group, mesh, name, z) {
|
|
97
|
+
static #addCompressedMesh(group, mesh, name, z, options = {}) {
|
|
60
98
|
if (!mesh) {
|
|
61
99
|
return
|
|
62
100
|
}
|
|
63
101
|
|
|
64
|
-
PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(mesh, z
|
|
102
|
+
PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(mesh, z, {
|
|
103
|
+
centerOffsetMil: options.centerOffsetMil
|
|
104
|
+
})
|
|
65
105
|
mesh.name = name
|
|
106
|
+
mesh.renderOrder = Number(options.renderOrder || 0)
|
|
107
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder.#applyCopperTint(
|
|
108
|
+
mesh,
|
|
109
|
+
options.copperBlend
|
|
110
|
+
)
|
|
66
111
|
group.add(mesh)
|
|
67
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Applies a role-specific covered-copper tint without mutating sibling meshes.
|
|
116
|
+
* @param {any} mesh Source mesh.
|
|
117
|
+
* @param {number | undefined} copperBlend Blend ratio.
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
120
|
+
static #applyCopperTint(mesh, copperBlend) {
|
|
121
|
+
const material = mesh?.material?.clone?.() || mesh?.material
|
|
122
|
+
const baseColor = material?.color?.getHex?.()
|
|
123
|
+
if (!material || !Number.isInteger(baseColor)) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
material.color.setHex(
|
|
128
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder.#blendHexColor(
|
|
129
|
+
baseColor,
|
|
130
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder.#resolveMaskedCopperTint(
|
|
131
|
+
baseColor
|
|
132
|
+
),
|
|
133
|
+
copperBlend
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
mesh.material = material
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolves a mask-covered copper tint that stays in the solder-mask hue.
|
|
141
|
+
* @param {number} baseColor Base mask-covered copper color.
|
|
142
|
+
* @returns {number}
|
|
143
|
+
*/
|
|
144
|
+
static #resolveMaskedCopperTint(baseColor) {
|
|
145
|
+
const channels =
|
|
146
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder.#rgbChannels(baseColor)
|
|
147
|
+
const dominant =
|
|
148
|
+
PcbScene3dMaskCoveredCopperSideGroupBuilder.#dominantChannel(
|
|
149
|
+
channels
|
|
150
|
+
)
|
|
151
|
+
if (dominant === 'green') {
|
|
152
|
+
return PcbScene3dMaskCoveredCopperSideGroupBuilder
|
|
153
|
+
.#GREEN_MASKED_COPPER_TINT
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return PcbScene3dMaskCoveredCopperSideGroupBuilder.#rgbToHex({
|
|
157
|
+
red: PcbScene3dMaskCoveredCopperSideGroupBuilder.#tintChannel(
|
|
158
|
+
channels.red,
|
|
159
|
+
dominant === 'red'
|
|
160
|
+
),
|
|
161
|
+
green: PcbScene3dMaskCoveredCopperSideGroupBuilder.#tintChannel(
|
|
162
|
+
channels.green,
|
|
163
|
+
dominant === 'green'
|
|
164
|
+
),
|
|
165
|
+
blue: PcbScene3dMaskCoveredCopperSideGroupBuilder.#tintChannel(
|
|
166
|
+
channels.blue,
|
|
167
|
+
dominant === 'blue'
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolves one RGB channel from a packed color.
|
|
174
|
+
* @param {number} color Packed hex color.
|
|
175
|
+
* @returns {{ red: number, green: number, blue: number }}
|
|
176
|
+
*/
|
|
177
|
+
static #rgbChannels(color) {
|
|
178
|
+
return {
|
|
179
|
+
red: (color >> 16) & 255,
|
|
180
|
+
green: (color >> 8) & 255,
|
|
181
|
+
blue: color & 255
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolves the dominant color channel.
|
|
187
|
+
* @param {{ red: number, green: number, blue: number }} channels RGB channels.
|
|
188
|
+
* @returns {'red' | 'green' | 'blue'}
|
|
189
|
+
*/
|
|
190
|
+
static #dominantChannel(channels) {
|
|
191
|
+
return Object.entries(channels).sort(
|
|
192
|
+
(left, right) => right[1] - left[1]
|
|
193
|
+
)[0][0]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Builds a tint channel that preserves the source mask hue.
|
|
198
|
+
* @param {number} channel Source channel value.
|
|
199
|
+
* @param {boolean} dominant Whether this channel is dominant.
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
static #tintChannel(channel, dominant) {
|
|
203
|
+
const value = dominant ? channel * 1.25 + 16 : channel * 0.84
|
|
204
|
+
return Math.min(255, Math.max(0, Math.round(value)))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Packs RGB channels into a hex color.
|
|
209
|
+
* @param {{ red: number, green: number, blue: number }} channels RGB channels.
|
|
210
|
+
* @returns {number}
|
|
211
|
+
*/
|
|
212
|
+
static #rgbToHex(channels) {
|
|
213
|
+
return (channels.red << 16) | (channels.green << 8) | channels.blue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Blends two integer RGB colors.
|
|
218
|
+
* @param {number} baseColor Base color.
|
|
219
|
+
* @param {number} overlayColor Overlay color.
|
|
220
|
+
* @param {number | undefined} overlayRatio Overlay ratio.
|
|
221
|
+
* @returns {number}
|
|
222
|
+
*/
|
|
223
|
+
static #blendHexColor(baseColor, overlayColor, overlayRatio) {
|
|
224
|
+
const ratio = Math.min(Math.max(Number(overlayRatio || 0), 0), 1)
|
|
225
|
+
const inverse = 1 - ratio
|
|
226
|
+
|
|
227
|
+
return [16, 8, 0].reduce((output, shift) => {
|
|
228
|
+
const channel = Math.round(
|
|
229
|
+
((baseColor >> shift) & 255) * inverse +
|
|
230
|
+
((overlayColor >> shift) & 255) * ratio
|
|
231
|
+
)
|
|
232
|
+
return output | (channel << shift)
|
|
233
|
+
}, 0)
|
|
234
|
+
}
|
|
68
235
|
}
|
|
@@ -30,8 +30,29 @@ export class PcbScene3dOutlineBuilder {
|
|
|
30
30
|
y: firstPoint.y
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
|
+
let currentPoint = firstPoint
|
|
33
34
|
|
|
34
35
|
for (const segment of segments) {
|
|
36
|
+
const startPoint = PcbScene3dOutlineBuilder.#segmentStartPoint(
|
|
37
|
+
segment,
|
|
38
|
+
centerX,
|
|
39
|
+
centerY
|
|
40
|
+
)
|
|
41
|
+
if (
|
|
42
|
+
startPoint &&
|
|
43
|
+
PcbScene3dOutlineBuilder.#distanceBetween(
|
|
44
|
+
currentPoint,
|
|
45
|
+
startPoint
|
|
46
|
+
) >= PcbScene3dOutlineBuilder.#DEGENERATE_LINE_EPSILON
|
|
47
|
+
) {
|
|
48
|
+
commands.push({
|
|
49
|
+
type: 'move',
|
|
50
|
+
x: startPoint.x,
|
|
51
|
+
y: startPoint.y
|
|
52
|
+
})
|
|
53
|
+
currentPoint = startPoint
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
if (segment.type === 'arc') {
|
|
36
57
|
const arcCommand = PcbScene3dOutlineBuilder.#buildArcCommand(
|
|
37
58
|
segment,
|
|
@@ -40,6 +61,8 @@ export class PcbScene3dOutlineBuilder {
|
|
|
40
61
|
)
|
|
41
62
|
if (arcCommand) {
|
|
42
63
|
commands.push(arcCommand)
|
|
64
|
+
currentPoint =
|
|
65
|
+
PcbScene3dOutlineBuilder.#commandEndPoint(arcCommand)
|
|
43
66
|
}
|
|
44
67
|
continue
|
|
45
68
|
}
|
|
@@ -51,12 +74,43 @@ export class PcbScene3dOutlineBuilder {
|
|
|
51
74
|
)
|
|
52
75
|
if (lineCommand) {
|
|
53
76
|
commands.push(lineCommand)
|
|
77
|
+
currentPoint =
|
|
78
|
+
PcbScene3dOutlineBuilder.#commandEndPoint(lineCommand)
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
return commands
|
|
58
83
|
}
|
|
59
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Converts one segment start into local centered scene coordinates.
|
|
87
|
+
* @param {Record<string, number | string>} segment Source segment.
|
|
88
|
+
* @param {number} centerX Board center X.
|
|
89
|
+
* @param {number} centerY Board center Y.
|
|
90
|
+
* @returns {{ x: number, y: number } | null}
|
|
91
|
+
*/
|
|
92
|
+
static #segmentStartPoint(segment, centerX, centerY) {
|
|
93
|
+
const x = Number(segment?.x1)
|
|
94
|
+
const y = Number(segment?.y1)
|
|
95
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return PcbScene3dOutlineBuilder.#toLocalPoint(x, y, centerX, centerY)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolves one generated command's endpoint.
|
|
104
|
+
* @param {Record<string, number | boolean>} command Outline command.
|
|
105
|
+
* @returns {{ x: number, y: number }}
|
|
106
|
+
*/
|
|
107
|
+
static #commandEndPoint(command) {
|
|
108
|
+
return {
|
|
109
|
+
x: Number(command.endX ?? command.x ?? 0),
|
|
110
|
+
y: Number(command.endY ?? command.y ?? 0)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
60
114
|
/**
|
|
61
115
|
* Converts one outline line segment into a local line command.
|
|
62
116
|
* @param {Record<string, number | string>} segment
|
|
@@ -8,9 +8,10 @@ export class PcbScene3dShellRenderer {
|
|
|
8
8
|
* Renders the interactive 3D scene shell.
|
|
9
9
|
* @param {{ pcb?: { boardOutline: { widthMil: number, heightMil: number }, components: { designator: string }[] }, bom: { quantity: number }[] }} documentModel
|
|
10
10
|
* @param {((key: string) => string) | null} [translate] Translation lookup.
|
|
11
|
+
* @param {{ initialToggles?: { 'external-models'?: boolean, 'fallback-bodies'?: boolean, copper?: boolean } }} [options] Rendering options.
|
|
11
12
|
* @returns {string}
|
|
12
13
|
*/
|
|
13
|
-
static render(documentModel, translate = null) {
|
|
14
|
+
static render(documentModel, translate = null, options = {}) {
|
|
14
15
|
const t = PcbScene3dText.createTranslator(translate)
|
|
15
16
|
const pcb = documentModel?.pcb
|
|
16
17
|
if (!pcb) {
|
|
@@ -71,13 +72,27 @@ export class PcbScene3dShellRenderer {
|
|
|
71
72
|
'<aside class="scene-3d__controls" aria-label="' +
|
|
72
73
|
PcbScene3dShellRenderer.#escapeHtml(t('scene3d.controlsAria')) +
|
|
73
74
|
'">' +
|
|
74
|
-
'<label class="scene-3d__toggle"><input type="checkbox"
|
|
75
|
+
'<label class="scene-3d__toggle"><input type="checkbox"' +
|
|
76
|
+
PcbScene3dShellRenderer.#checkedAttribute(
|
|
77
|
+
options,
|
|
78
|
+
'external-models',
|
|
79
|
+
true
|
|
80
|
+
) +
|
|
81
|
+
' data-scene-3d-toggle="external-models" />' +
|
|
75
82
|
PcbScene3dShellRenderer.#escapeHtml(t('scene3d.externalModels')) +
|
|
76
83
|
'</label>' +
|
|
77
|
-
'<label class="scene-3d__toggle"><input type="checkbox"
|
|
84
|
+
'<label class="scene-3d__toggle"><input type="checkbox"' +
|
|
85
|
+
PcbScene3dShellRenderer.#checkedAttribute(
|
|
86
|
+
options,
|
|
87
|
+
'fallback-bodies',
|
|
88
|
+
false
|
|
89
|
+
) +
|
|
90
|
+
' data-scene-3d-toggle="fallback-bodies" />' +
|
|
78
91
|
PcbScene3dShellRenderer.#escapeHtml(t('scene3d.fallbackBodies')) +
|
|
79
92
|
'</label>' +
|
|
80
|
-
'<label class="scene-3d__toggle"><input type="checkbox"
|
|
93
|
+
'<label class="scene-3d__toggle"><input type="checkbox"' +
|
|
94
|
+
PcbScene3dShellRenderer.#checkedAttribute(options, 'copper', true) +
|
|
95
|
+
' data-scene-3d-toggle="copper" />' +
|
|
81
96
|
PcbScene3dShellRenderer.#escapeHtml(t('scene3d.copperDetail')) +
|
|
82
97
|
'</label>' +
|
|
83
98
|
'<section class="scene-3d__selection" aria-live="polite"><h4 class="scene-3d__selection-title">' +
|
|
@@ -114,6 +129,25 @@ export class PcbScene3dShellRenderer {
|
|
|
114
129
|
)
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Returns the checkbox checked attribute for one scene toggle.
|
|
134
|
+
* @param {{ initialToggles?: Record<string, boolean> }} options Renderer options.
|
|
135
|
+
* @param {string} toggleName Toggle identifier.
|
|
136
|
+
* @param {boolean} defaultChecked Package default state.
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
static #checkedAttribute(options, toggleName, defaultChecked) {
|
|
140
|
+
const initialToggles = options?.initialToggles || {}
|
|
141
|
+
const checked = Object.prototype.hasOwnProperty.call(
|
|
142
|
+
initialToggles,
|
|
143
|
+
toggleName
|
|
144
|
+
)
|
|
145
|
+
? initialToggles[toggleName] === true
|
|
146
|
+
: defaultChecked
|
|
147
|
+
|
|
148
|
+
return checked ? ' checked' : ''
|
|
149
|
+
}
|
|
150
|
+
|
|
117
151
|
/**
|
|
118
152
|
* Escapes markup text.
|
|
119
153
|
* @param {string} value Raw text.
|