pcb-scene3d-viewer 1.1.47 → 1.1.49

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.1.47",
3
+ "version": "1.1.49",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -55,6 +55,7 @@
55
55
  "circuitjson-toolkit": "^1.0.10",
56
56
  "earcut": "^3.0.2",
57
57
  "fflate": "^0.8.2",
58
+ "polygon-clipping": "^0.15.7",
58
59
  "three": "^0.183.2"
59
60
  },
60
61
  "devDependencies": {
@@ -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(board)
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 (options.hasBoardAssemblyModel) {
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.#partitionDrillCutouts(
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 drill cutouts into safe shape holes and fallback clip polygons.
399
- * @param {{ path: any, points: { x: number, y: number }[] }[]} drillCutouts
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 #partitionDrillCutouts(drillCutouts, contourPoints) {
411
+ static #partitionCutouts(cutouts, contourPoints) {
404
412
  const shapeHoles = []
405
413
  const clippingCutouts = []
406
414
 
407
- for (const cutout of Array.isArray(drillCutouts) ? drillCutouts : []) {
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 = PcbScene3dCopperDrillCutoutBuilder.resolve(
29
- sceneDescription?.detail
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,
@@ -91,6 +96,10 @@ export class PcbScene3dCopperDetailGroupBuilder {
91
96
  ),
92
97
  drillCutouts,
93
98
  drillDetail: sceneDescription?.detail,
99
+ unionCoveredLayerPrimitives:
100
+ PcbScene3dCopperDetailGroupBuilder.#usesGerberLayerUnion(
101
+ sceneDescription
102
+ ),
94
103
  occlusionCutouts:
95
104
  PcbScene3dCopperDetailGroupBuilder.#resolveCoveredCopperOcclusions(
96
105
  sceneDescription?.detail
@@ -131,6 +140,18 @@ export class PcbScene3dCopperDetailGroupBuilder {
131
140
  )
132
141
  }
133
142
 
143
+ /**
144
+ * Checks whether the source format represents copper as Gerber polarity union.
145
+ * @param {object | undefined} sceneDescription Scene description.
146
+ * @returns {boolean}
147
+ */
148
+ static #usesGerberLayerUnion(sceneDescription) {
149
+ return (
150
+ String(sceneDescription?.sourceFormat || '').toLowerCase() ===
151
+ 'gerber'
152
+ )
153
+ }
154
+
134
155
  /**
135
156
  * Builds exposed copper detail.
136
157
  * @param {any} THREE Three.js namespace.
@@ -187,6 +208,43 @@ export class PcbScene3dCopperDetailGroupBuilder {
187
208
  )
188
209
  }
189
210
 
211
+ /**
212
+ * Resolves explicit through-board cutouts that copper detail must not cover.
213
+ * @param {{ cutouts?: any[] } | undefined} board Board metadata.
214
+ * @returns {{ x: number, y: number }[][]}
215
+ */
216
+ static #resolveBoardCutouts(board) {
217
+ return (Array.isArray(board?.cutouts) ? board.cutouts : [])
218
+ .map((cutout) =>
219
+ PcbScene3dCopperDetailGroupBuilder.#resolveBoardCutoutPoints(
220
+ cutout
221
+ )
222
+ )
223
+ .filter((points) => points.length >= 3)
224
+ }
225
+
226
+ /**
227
+ * Resolves one explicit board cutout loop in source board coordinates.
228
+ * @param {any} cutout Cutout source.
229
+ * @returns {{ x: number, y: number }[]}
230
+ */
231
+ static #resolveBoardCutoutPoints(cutout) {
232
+ const points = Array.isArray(cutout?.points)
233
+ ? cutout.points
234
+ : Array.isArray(cutout?.vertices)
235
+ ? cutout.vertices
236
+ : Array.isArray(cutout)
237
+ ? cutout
238
+ : []
239
+
240
+ return points
241
+ .map((point) => ({
242
+ x: Number(point?.x ?? point?.[0]),
243
+ y: Number(point?.y ?? point?.[1])
244
+ }))
245
+ .filter((point) => Number.isFinite(point.x + point.y))
246
+ }
247
+
190
248
  /**
191
249
  * Resolves solder-mask material options for covered copper relief.
192
250
  * @param {object} sceneDescription Scene description.
@@ -199,7 +257,8 @@ export class PcbScene3dCopperDetailGroupBuilder {
199
257
  {
200
258
  hasBoardAssemblyModel: Boolean(
201
259
  sceneDescription?.boardAssemblyModel
202
- )
260
+ ),
261
+ sourceFormat: sceneDescription?.sourceFormat
203
262
  }
204
263
  )
205
264
  }