pcb-scene3d-viewer 1.1.49 → 1.1.50

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.49",
3
+ "version": "1.1.50",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -420,7 +420,7 @@ export class PcbScene3dBoardAssemblyPresentation {
420
420
  * @returns {number}
421
421
  */
422
422
  static #resolveSurfaceColor(board, options) {
423
- return PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
423
+ return PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {
424
424
  hasBoardAssemblyModel: true,
425
425
  sourceFormat: options?.sourceFormat
426
426
  })
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export class PcbScene3dBoardMaterialPalette {
5
5
  static #DEFAULT_SURFACE_COLOR = 0x2a5f27
6
+ static #BOARD_SURFACE_DARKEN_RATIO = 0.88
6
7
 
7
8
  /**
8
9
  * Resolves the solder-mask face color for the generated board shell.
@@ -25,6 +26,19 @@ export class PcbScene3dBoardMaterialPalette {
25
26
  : PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
26
27
  }
27
28
 
29
+ /**
30
+ * Resolves the darker display color for visible board solder-mask faces.
31
+ * @param {{ surfaceColor?: number } | undefined} board Board metadata.
32
+ * @param {{ hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options] Scene options.
33
+ * @returns {number}
34
+ */
35
+ static resolveBoardSurfaceColor(board, options = {}) {
36
+ return PcbScene3dBoardMaterialPalette.#darkenHexColor(
37
+ PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, options),
38
+ PcbScene3dBoardMaterialPalette.#BOARD_SURFACE_DARKEN_RATIO
39
+ )
40
+ }
41
+
28
42
  /**
29
43
  * Returns true when the source format supplies display-stable board colors.
30
44
  * @param {{ sourceFormat?: string }} options Scene options.
@@ -52,4 +66,19 @@ export class PcbScene3dBoardMaterialPalette {
52
66
  static isGeneratedBodyVisible(options = {}) {
53
67
  return true
54
68
  }
69
+
70
+ /**
71
+ * Darkens a packed RGB color while preserving its hue.
72
+ * @param {number} color Packed RGB color.
73
+ * @param {number} ratio Channel multiplier.
74
+ * @returns {number}
75
+ */
76
+ static #darkenHexColor(color, ratio) {
77
+ const multiplier = Math.min(Math.max(Number(ratio || 0), 0), 1)
78
+
79
+ return [16, 8, 0].reduce((output, shift) => {
80
+ const channel = Math.round(((color >> shift) & 255) * multiplier)
81
+ return output | (channel << shift)
82
+ }, 0)
83
+ }
55
84
  }
@@ -609,10 +609,13 @@ export class PcbScene3dBoardSolderMaskFactory {
609
609
  */
610
610
  static #buildMaterial(THREE, board, side, sourceFormat) {
611
611
  return new THREE.MeshStandardMaterial({
612
- color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
613
- hasBoardAssemblyModel: true,
614
- sourceFormat
615
- }),
612
+ color: PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(
613
+ board,
614
+ {
615
+ hasBoardAssemblyModel: true,
616
+ sourceFormat
617
+ }
618
+ ),
616
619
  ...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
617
620
  polygonOffset: true,
618
621
  polygonOffsetFactor: -1,
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dBoardShapeFactory } from './PcbScene3dBoardShapeFactory.mjs'
2
+ import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
2
3
  import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
3
4
 
4
5
  /**
@@ -16,7 +17,7 @@ export class PcbScene3dDrillVoidFactory {
16
17
  * @param {number} [topZ]
17
18
  * @param {number} [bottomZ]
18
19
  * @param {(x: number, y: number) => { x: number, y: number }} [normalizeBoardPoint]
19
- * @param {{ enabled?: boolean, color?: number, board?: object }} [options]
20
+ * @param {{ enabled?: boolean, color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options]
20
21
  * @returns {any}
21
22
  */
22
23
  static buildGroup(
@@ -113,20 +114,43 @@ export class PcbScene3dDrillVoidFactory {
113
114
  /**
114
115
  * Builds the shared drill-interior material.
115
116
  * @param {any} THREE
116
- * @param {{ color?: number }} options
117
+ * @param {{ color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} options
117
118
  * @returns {any}
118
119
  */
119
120
  static #buildInteriorMaterial(THREE, options) {
120
121
  return new THREE.MeshStandardMaterial({
121
- color: Number.isInteger(options?.color)
122
- ? options.color
123
- : PcbScene3dDrillVoidFactory.#DEFAULT_INTERIOR_COLOR,
122
+ color: PcbScene3dDrillVoidFactory.#resolveInteriorColor(options),
124
123
  roughness: 0.82,
125
124
  metalness: 0,
126
125
  side: THREE.DoubleSide
127
126
  })
128
127
  }
129
128
 
129
+ /**
130
+ * Resolves the drill-interior display color.
131
+ * @param {{ color?: number, board?: object, hasBoardAssemblyModel?: boolean, sourceFormat?: string }} options
132
+ * @returns {number}
133
+ */
134
+ static #resolveInteriorColor(options) {
135
+ if (Number.isInteger(options?.color)) {
136
+ return options.color
137
+ }
138
+
139
+ if (options?.board) {
140
+ return PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(
141
+ options.board,
142
+ {
143
+ hasBoardAssemblyModel: Boolean(
144
+ options?.hasBoardAssemblyModel
145
+ ),
146
+ sourceFormat: options?.sourceFormat
147
+ }
148
+ )
149
+ }
150
+
151
+ return PcbScene3dDrillVoidFactory.#DEFAULT_INTERIOR_COLOR
152
+ }
153
+
130
154
  /**
131
155
  * Checks whether one drill is a routed slot.
132
156
  * @param {{ diameter?: number, slotLength?: number | null }} drillSpec Drill spec.
@@ -9,7 +9,7 @@ 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
+ * @param {{ centerOffsetMil?: number, thicknessMil?: number }} [options] Compression options.
13
13
  * @returns {void}
14
14
  */
15
15
  static compressMaskCoveredCopperMesh(mesh, sourceCenterZ, options = {}) {
@@ -19,7 +19,9 @@ export class PcbScene3dGeometryZCompressor {
19
19
  sourceCenterZ +
20
20
  MASK_COVERED_CENTER_OFFSET_MIL +
21
21
  Number(options?.centerOffsetMil || 0),
22
- MASK_COVERED_THICKNESS_MIL
22
+ Number.isFinite(Number(options?.thicknessMil))
23
+ ? Math.max(Number(options.thicknessMil), 0)
24
+ : MASK_COVERED_THICKNESS_MIL
23
25
  )
24
26
  }
25
27
 
@@ -10,7 +10,8 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
10
10
  static #TRACK_COPPER_BLEND = 0.7
11
11
  static #FILL_RENDER_ORDER = 10
12
12
  static #TRACK_RENDER_ORDER = 12
13
- static #TRACK_CENTER_OFFSET_MIL = 0.16
13
+ static #TRACK_CENTER_OFFSET_MIL = 0.25
14
+ static #TRACK_THICKNESS_MIL = 1.05
14
15
 
15
16
  /**
16
17
  * Builds one side group from prepared mask-covered copper meshes.
@@ -39,6 +40,10 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
39
40
  centerOffsetMil:
40
41
  PcbScene3dMaskCoveredCopperSideGroupBuilder
41
42
  .#TRACK_CENTER_OFFSET_MIL,
43
+ thicknessMil:
44
+ PcbScene3dMaskCoveredCopperSideGroupBuilder
45
+ .#TRACK_THICKNESS_MIL,
46
+ keepSideWalls: true,
42
47
  copperBlend:
43
48
  PcbScene3dMaskCoveredCopperSideGroupBuilder
44
49
  .#TRACK_COPPER_BLEND,
@@ -56,6 +61,10 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
56
61
  centerOffsetMil:
57
62
  PcbScene3dMaskCoveredCopperSideGroupBuilder
58
63
  .#TRACK_CENTER_OFFSET_MIL,
64
+ thicknessMil:
65
+ PcbScene3dMaskCoveredCopperSideGroupBuilder
66
+ .#TRACK_THICKNESS_MIL,
67
+ keepSideWalls: true,
59
68
  copperBlend:
60
69
  PcbScene3dMaskCoveredCopperSideGroupBuilder
61
70
  .#TRACK_COPPER_BLEND,
@@ -92,7 +101,7 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
92
101
  * @param {any | null} mesh Mesh to add.
93
102
  * @param {string} name Scene object name.
94
103
  * @param {number} z Source center Z.
95
- * @param {{ centerOffsetMil?: number, copperBlend?: number, renderOrder?: number }} [options] Presentation options.
104
+ * @param {{ centerOffsetMil?: number, thicknessMil?: number, keepSideWalls?: boolean, copperBlend?: number, renderOrder?: number }} [options] Presentation options.
96
105
  * @returns {void}
97
106
  */
98
107
  static #addCompressedMesh(group, mesh, name, z, options = {}) {
@@ -100,9 +109,14 @@ export class PcbScene3dMaskCoveredCopperSideGroupBuilder {
100
109
  return
101
110
  }
102
111
  PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(mesh, z, {
103
- centerOffsetMil: options.centerOffsetMil
112
+ centerOffsetMil: options.centerOffsetMil,
113
+ thicknessMil: options.thicknessMil
104
114
  })
105
- PcbScene3dMaskCoveredCopperSurfaceFilter.keepOuterSurface(mesh)
115
+ if (options.keepSideWalls === true) {
116
+ PcbScene3dMaskCoveredCopperSurfaceFilter.keepOuterRelief(mesh)
117
+ } else {
118
+ PcbScene3dMaskCoveredCopperSurfaceFilter.keepOuterSurface(mesh)
119
+ }
106
120
  mesh.name = name
107
121
  mesh.renderOrder = Number(options.renderOrder || 0)
108
122
  PcbScene3dMaskCoveredCopperSideGroupBuilder.#applyCopperTint(
@@ -10,6 +10,29 @@ export class PcbScene3dMaskCoveredCopperSurfaceFilter {
10
10
  * @returns {void}
11
11
  */
12
12
  static keepOuterSurface(mesh) {
13
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#filterOuterTriangles(mesh, {
14
+ keepSideWalls: false
15
+ })
16
+ }
17
+
18
+ /**
19
+ * Removes the hidden underside while keeping shallow edge walls.
20
+ * @param {any | null} mesh Copper relief mesh.
21
+ * @returns {void}
22
+ */
23
+ static keepOuterRelief(mesh) {
24
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#filterOuterTriangles(mesh, {
25
+ keepSideWalls: true
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Keeps only triangles that are visible above the solder mask.
31
+ * @param {any | null} mesh Copper relief mesh.
32
+ * @param {{ keepSideWalls?: boolean }} options Filter options.
33
+ * @returns {void}
34
+ */
35
+ static #filterOuterTriangles(mesh, options) {
13
36
  const geometry = mesh?.geometry
14
37
  const position = geometry?.getAttribute?.('position')
15
38
  const source = position?.array
@@ -17,14 +40,16 @@ export class PcbScene3dMaskCoveredCopperSurfaceFilter {
17
40
  return
18
41
  }
19
42
 
20
- const maxZ = PcbScene3dMaskCoveredCopperSurfaceFilter.#maxZ(source)
43
+ const zBounds =
44
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#zBounds(source)
21
45
  const filtered = []
22
46
  for (let index = 0; index + 8 < source.length; index += 9) {
23
47
  if (
24
- [2, 5, 8].every(
25
- (offset) =>
26
- Math.abs(source[index + offset] - maxZ) <=
27
- PcbScene3dMaskCoveredCopperSurfaceFilter.#Z_EPSILON
48
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#keepsTriangle(
49
+ source,
50
+ index,
51
+ zBounds,
52
+ options
28
53
  )
29
54
  ) {
30
55
  filtered.push(...source.slice(index, index + 9))
@@ -50,15 +75,55 @@ export class PcbScene3dMaskCoveredCopperSurfaceFilter {
50
75
  }
51
76
 
52
77
  /**
53
- * Resolves the highest Z plane in one packed XYZ buffer.
78
+ * Checks whether one triangle should stay in the visible relief mesh.
79
+ * @param {ArrayLike<number>} source Position buffer.
80
+ * @param {number} index Triangle start index.
81
+ * @param {{ minZ: number, maxZ: number }} zBounds Geometry Z bounds.
82
+ * @param {{ keepSideWalls?: boolean }} options Filter options.
83
+ * @returns {boolean}
84
+ */
85
+ static #keepsTriangle(source, index, zBounds, options) {
86
+ const zValues = [2, 5, 8].map((offset) => source[index + offset])
87
+ const hasTop = zValues.some((z) =>
88
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#matchesZ(z, zBounds.maxZ)
89
+ )
90
+ const hasBottom = zValues.some((z) =>
91
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#matchesZ(z, zBounds.minZ)
92
+ )
93
+
94
+ if (hasTop && !hasBottom) {
95
+ return true
96
+ }
97
+
98
+ return options?.keepSideWalls === true && hasTop && hasBottom
99
+ }
100
+
101
+ /**
102
+ * Checks whether a Z value matches one target plane.
103
+ * @param {number} value Candidate Z.
104
+ * @param {number} target Target Z.
105
+ * @returns {boolean}
106
+ */
107
+ static #matchesZ(value, target) {
108
+ return (
109
+ Math.abs(Number(value) - Number(target)) <=
110
+ PcbScene3dMaskCoveredCopperSurfaceFilter.#Z_EPSILON
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Resolves the lowest and highest Z planes in one packed XYZ buffer.
54
116
  * @param {ArrayLike<number>} source Position buffer.
55
- * @returns {number}
117
+ * @returns {{ minZ: number, maxZ: number }}
56
118
  */
57
- static #maxZ(source) {
119
+ static #zBounds(source) {
120
+ let minZ = Infinity
58
121
  let maxZ = -Infinity
59
122
  for (let index = 2; index < source.length; index += 3) {
60
- maxZ = Math.max(maxZ, Number(source[index]))
123
+ const z = Number(source[index])
124
+ minZ = Math.min(minZ, z)
125
+ maxZ = Math.max(maxZ, z)
61
126
  }
62
- return maxZ
127
+ return { minZ, maxZ }
63
128
  }
64
129
  }
@@ -264,7 +264,6 @@ export class PcbScene3dRuntime {
264
264
  this.#settleReady()
265
265
  return
266
266
  }
267
-
268
267
  try {
269
268
  const { THREE, OrbitControls } =
270
269
  (await this.#hooks.loadRuntimeModules?.()) ||
@@ -274,7 +273,6 @@ export class PcbScene3dRuntime {
274
273
  if (this.#isDisposed || !this.#viewportNode) {
275
274
  return
276
275
  }
277
-
278
276
  this.#createRenderer()
279
277
  this.#createSceneGraph()
280
278
  this.#createControls()
@@ -299,7 +297,6 @@ export class PcbScene3dRuntime {
299
297
  #createRenderer() {
300
298
  const THREE = this.#three
301
299
  const size = PcbScene3dViewportResize.resolveSize(this.#viewportNode)
302
-
303
300
  this.#renderer = new THREE.WebGLRenderer({
304
301
  antialias: true,
305
302
  alpha: true,
@@ -310,7 +307,6 @@ export class PcbScene3dRuntime {
310
307
  this.#renderer.domElement.className = 'scene-3d__canvas'
311
308
  this.#renderer.domElement.style.width = '100%'
312
309
  this.#renderer.domElement.style.height = '100%'
313
-
314
310
  this.#scene = new THREE.Scene()
315
311
  this.#camera = new THREE.PerspectiveCamera(
316
312
  38,
@@ -368,7 +364,11 @@ export class PcbScene3dRuntime {
368
364
  (x, y) => this.#normalizeDetailPoint(x, y),
369
365
  {
370
366
  enabled: true,
371
- board
367
+ board,
368
+ hasBoardAssemblyModel: Boolean(
369
+ this.#sceneDescription.boardAssemblyModel
370
+ ),
371
+ sourceFormat: this.#sceneDescription.sourceFormat
372
372
  }
373
373
  )
374
374
  )
@@ -97,7 +97,7 @@ export class PcbScene3dRuntimeBoardMeshes {
97
97
  : 0xc9ca78
98
98
  const surfaceColor = hasBoardAssemblyModel
99
99
  ? resolvedEdgeColor
100
- : PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
100
+ : PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {
101
101
  hasBoardAssemblyModel
102
102
  })
103
103