pcb-scene3d-viewer 1.1.21 → 1.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +3 -2
  2. package/docs/api.md +27 -1
  3. package/docs/circuitjson.md +63 -18
  4. package/docs/model-format.md +13 -2
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +2 -2
  7. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
  8. package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
  9. package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
  10. package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
  12. package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
  13. package/src/PcbAssemblyGltfValidator.mjs +460 -0
  14. package/src/PcbAssemblyGltfWriter.mjs +801 -0
  15. package/src/PcbAssemblyMeshUtils.mjs +117 -0
  16. package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
  17. package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
  18. package/src/PcbAssemblyStepWriter.mjs +24 -2
  19. package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
  20. package/src/PcbModelArchiveExporter.mjs +521 -7
  21. package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
  22. package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
  23. package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
  24. package/src/PcbScene3dComponentVisibility.mjs +100 -5
  25. package/src/PcbScene3dController.mjs +25 -55
  26. package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
  27. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
  28. package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
  29. package/src/PcbScene3dCopperFactory.mjs +99 -85
  30. package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
  31. package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
  33. package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
  34. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
  35. package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
  36. package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
  37. package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
  38. package/src/PcbScene3dExternalModels.mjs +16 -7
  39. package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
  40. package/src/PcbScene3dFallbackVisibility.mjs +58 -1
  41. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
  42. package/src/PcbScene3dPadFactory.mjs +35 -2
  43. package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
  44. package/src/PcbScene3dRuntime.mjs +94 -100
  45. package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
  46. package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
  47. package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
  48. package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
  49. package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
  50. package/src/PcbScene3dSelectionStyler.mjs +52 -2
  51. package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
  52. package/src/PcbScene3dText.mjs +1 -0
  53. package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
  54. package/src/PcbScene3dViaFactory.mjs +27 -7
  55. package/src/scene3d.mjs +3 -0
@@ -4,6 +4,7 @@ import { PcbScene3dCircuitJsonAdapter } from './PcbScene3dCircuitJsonAdapter.mjs
4
4
  import { PcbScene3dInteractionHints } from './PcbScene3dInteractionHints.mjs'
5
5
  import { PcbScene3dRuntime } from './PcbScene3dRuntime.mjs'
6
6
  import { PcbScene3dSelectionInspectorRenderer } from './PcbScene3dSelectionInspectorRenderer.mjs'
7
+ import { PcbScene3dSelectionIndexBuilder } from './PcbScene3dSelectionIndexBuilder.mjs'
7
8
  import { PcbScene3dSelectionVisibilityBinder } from './PcbScene3dSelectionVisibilityBinder.mjs'
8
9
  import { PcbScene3dText } from './PcbScene3dText.mjs'
9
10
 
@@ -37,7 +38,7 @@ export class PcbScene3dController {
37
38
  /** @type {Array<{ node: EventTarget, type: string, listener: (event: any) => void }>} */
38
39
  #listeners
39
40
 
40
- /** @type {Map<string, { component: any | null, externalPlacement: any | null }>} */
41
+ /** @type {Map<string, { component: any | null, externalPlacement: any | null, staticBodyPlacement: any | null }>} */
41
42
  #selectionIndex
42
43
 
43
44
  /** @type {Map<string, { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }>} */
@@ -259,7 +260,12 @@ export class PcbScene3dController {
259
260
  * @returns {void}
260
261
  */
261
262
  setAutoSearchMissingModels(enabled) {
262
- this.#autoSearchMissingModels = enabled === true
263
+ const nextEnabled = enabled === true
264
+ if (this.#autoSearchMissingModels === nextEnabled) {
265
+ return
266
+ }
267
+
268
+ this.#autoSearchMissingModels = nextEnabled
263
269
  this.#runtime?.setToggle?.(
264
270
  'model-search-models',
265
271
  this.#autoSearchMissingModels
@@ -451,7 +457,7 @@ export class PcbScene3dController {
451
457
  PcbScene3dController.#normalizeSceneDescription(sceneDescription)
452
458
  this.#sceneDescription = renderModel
453
459
  this.#selectionIndex =
454
- PcbScene3dController.#buildSelectionIndex(renderModel)
460
+ PcbScene3dSelectionIndexBuilder.build(renderModel)
455
461
  const createRuntime =
456
462
  options.createRuntime ||
457
463
  ((nextViewportNode, nextSceneDescription, hooks) =>
@@ -716,7 +722,9 @@ export class PcbScene3dController {
716
722
  this.#selectedComponentKey
717
723
  ? {
718
724
  designator: this.#selectedComponentKey,
719
- sourceType: 'sidebar'
725
+ sourceType: this.#resolveSelectionSourceType(
726
+ this.#selectedComponentKey
727
+ )
720
728
  }
721
729
  : null
722
730
  )
@@ -783,7 +791,7 @@ export class PcbScene3dController {
783
791
 
784
792
  /**
785
793
  * Renders selected component transform controls into the external host.
786
- * @param {{ designator?: string, selection?: { sourceType?: string } | null, selectionEntry?: { component: any | null, externalPlacement: any | null } | null, adjustment?: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } } } | null} state Selection state.
794
+ * @param {{ designator?: string, selection?: { sourceType?: string } | null, selectionEntry?: { component: any | null, externalPlacement: any | null, staticBodyPlacement?: any | null } | null, adjustment?: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } } } | null} state Selection state.
787
795
  * @returns {void}
788
796
  */
789
797
  #renderAdjustmentHost(state = null) {
@@ -855,9 +863,18 @@ export class PcbScene3dController {
855
863
  * @returns {string}
856
864
  */
857
865
  #resolveSelectionSourceType(designator) {
858
- return this.#selectionIndex.get(designator)?.externalPlacement
859
- ? 'external-model'
860
- : 'component'
866
+ const selectionEntry = this.#selectionIndex.get(designator)
867
+ if (selectionEntry?.externalPlacement) {
868
+ return 'external-model'
869
+ }
870
+ if (selectionEntry?.component) {
871
+ return 'component'
872
+ }
873
+ if (selectionEntry?.staticBodyPlacement) {
874
+ return 'static-body'
875
+ }
876
+
877
+ return 'component'
861
878
  }
862
879
 
863
880
  /**
@@ -893,53 +910,6 @@ export class PcbScene3dController {
893
910
  : sceneDescription
894
911
  }
895
912
 
896
- /**
897
- * Builds one designator-keyed inspector lookup from the scene
898
- * description.
899
- * @param {{ components?: any[], externalPlacements?: any[] }} sceneDescription
900
- * @returns {Map<string, { component: any | null, externalPlacement: any | null }>}
901
- */
902
- static #buildSelectionIndex(sceneDescription) {
903
- const index = new Map()
904
- const components = Array.isArray(sceneDescription?.components)
905
- ? sceneDescription.components
906
- : []
907
- const externalPlacements = Array.isArray(
908
- sceneDescription?.externalPlacements
909
- )
910
- ? sceneDescription.externalPlacements
911
- : []
912
-
913
- components.forEach((component) => {
914
- const designator = String(component?.designator || '').trim()
915
- if (!designator) {
916
- return
917
- }
918
-
919
- index.set(designator, {
920
- component,
921
- externalPlacement:
922
- index.get(designator)?.externalPlacement || null
923
- })
924
- })
925
-
926
- externalPlacements.forEach((externalPlacement) => {
927
- const designator = String(
928
- externalPlacement?.designator || ''
929
- ).trim()
930
- if (!designator) {
931
- return
932
- }
933
-
934
- index.set(designator, {
935
- component: index.get(designator)?.component || null,
936
- externalPlacement
937
- })
938
- })
939
-
940
- return index
941
- }
942
-
943
913
  /**
944
914
  * Resolves one archive base name from the mounted document metadata.
945
915
  * @param {{ summary?: { title?: string }, fileName?: string } | null} documentModel
@@ -31,18 +31,27 @@ export class PcbScene3dCopperDetailFilter {
31
31
  arcs: PcbScene3dCopperDetailFilter.#filterMaskOpenPrimitives(
32
32
  detail.arcs
33
33
  ),
34
+ fills: PcbScene3dCopperDetailFilter.#filterMaskOpenPrimitives(
35
+ detail.fills
36
+ ),
37
+ polygons: PcbScene3dCopperDetailFilter.#filterMaskOpenPrimitives(
38
+ detail.polygons
39
+ ),
34
40
  copperTexts: PcbScene3dCopperDetailFilter.#filterMaskOpenPrimitives(
35
41
  detail.copperTexts,
36
42
  copperTextMaskMatcher
37
43
  ),
38
- vias: PcbScene3dCopperDetailFilter.#filterExposedVias(detail.vias)
44
+ vias: PcbScene3dCopperDetailFilter.#filterExposedVias(
45
+ detail.vias,
46
+ sceneDescription
47
+ )
39
48
  }
40
49
  }
41
50
 
42
51
  /**
43
52
  * Resolves trace-like copper that should be visible through solder mask.
44
53
  * @param {object} sceneDescription 3D scene description.
45
- * @returns {{ tracks: any[], arcs: any[], copperTexts: any[], vias: any[] }}
54
+ * @returns {{ tracks: any[], arcs: any[], fills: any[], polygons: any[], copperTexts: any[], vias: any[] }}
46
55
  */
47
56
  static resolveCoveredByMask(sceneDescription) {
48
57
  const detail = sceneDescription?.detail || {}
@@ -52,7 +61,14 @@ export class PcbScene3dCopperDetailFilter {
52
61
  sceneDescription
53
62
  )
54
63
  ) {
55
- return { tracks: [], arcs: [], copperTexts: [], vias: [] }
64
+ return {
65
+ tracks: [],
66
+ arcs: [],
67
+ fills: [],
68
+ polygons: [],
69
+ copperTexts: [],
70
+ vias: []
71
+ }
56
72
  }
57
73
 
58
74
  const defaultCovered =
@@ -69,6 +85,14 @@ export class PcbScene3dCopperDetailFilter {
69
85
  detail.arcs,
70
86
  defaultCovered
71
87
  ),
88
+ fills: PcbScene3dCopperDetailFilter.#filterMaskCoveredPrimitives(
89
+ detail.fills,
90
+ defaultCovered
91
+ ),
92
+ polygons: PcbScene3dCopperDetailFilter.#filterMaskCoveredPrimitives(
93
+ detail.polygons,
94
+ defaultCovered
95
+ ),
72
96
  copperTexts: [],
73
97
  vias: []
74
98
  }
@@ -104,7 +128,10 @@ export class PcbScene3dCopperDetailFilter {
104
128
  const vias = PcbScene3dCopperDetailFilter.#usesRealisticMasking(
105
129
  sceneDescription
106
130
  )
107
- ? PcbScene3dCopperDetailFilter.#filterExposedVias(detail.vias)
131
+ ? PcbScene3dCopperDetailFilter.#filterExposedVias(
132
+ detail.vias,
133
+ sceneDescription
134
+ )
108
135
  : detail.vias || []
109
136
 
110
137
  return PcbScene3dCopperDetailFilter.#appendPadBarrelSpecs(
@@ -113,6 +140,28 @@ export class PcbScene3dCopperDetailFilter {
113
140
  )
114
141
  }
115
142
 
143
+ /**
144
+ * Resolves via annuli that should be visible through solder mask.
145
+ * @param {object} sceneDescription 3D scene description.
146
+ * @returns {any[]}
147
+ */
148
+ static resolveCoveredStandaloneVias(sceneDescription) {
149
+ const detail = sceneDescription?.detail || {}
150
+
151
+ if (
152
+ !PcbScene3dCopperDetailFilter.#usesRealisticMasking(
153
+ sceneDescription
154
+ )
155
+ ) {
156
+ return []
157
+ }
158
+
159
+ return PcbScene3dCopperDetailFilter.#filterMaskCoveredVias(
160
+ detail.vias,
161
+ sceneDescription
162
+ )
163
+ }
164
+
116
165
  /**
117
166
  * Checks whether one scene should use solder-mask visibility.
118
167
  * @param {object} sceneDescription 3D scene description.
@@ -163,6 +212,8 @@ export class PcbScene3dCopperDetailFilter {
163
212
  return [
164
213
  detail?.tracks,
165
214
  detail?.arcs,
215
+ detail?.fills,
216
+ detail?.polygons,
166
217
  detail?.copperTexts,
167
218
  detail?.vias,
168
219
  detail?.pads
@@ -205,14 +256,40 @@ export class PcbScene3dCopperDetailFilter {
205
256
  }
206
257
 
207
258
  /**
208
- * Keeps vias only when they are explicitly not tented.
259
+ * Keeps vias that should expose copper annuli in the 3D view.
209
260
  * @param {any[] | undefined} vias Via list.
261
+ * @param {object} sceneDescription 3D scene description.
210
262
  * @returns {any[]}
211
263
  */
212
- static #filterExposedVias(vias) {
213
- return (vias || []).filter((via) => {
214
- return via?.isTentingTop === false || via?.isTentingBottom === false
215
- })
264
+ static #filterExposedVias(vias, sceneDescription) {
265
+ return (vias || []).filter((via) =>
266
+ PcbScene3dCopperDetailFilter.#isViaExplicitlyOpen(via)
267
+ )
268
+ }
269
+
270
+ /**
271
+ * Keeps KiCad via annuli that are covered by solder mask.
272
+ * @param {any[] | undefined} vias Via list.
273
+ * @param {object} sceneDescription 3D scene description.
274
+ * @returns {any[]}
275
+ */
276
+ static #filterMaskCoveredVias(vias, sceneDescription) {
277
+ if (!PcbScene3dCopperDetailFilter.#isKiCadScene(sceneDescription)) {
278
+ return []
279
+ }
280
+
281
+ return (vias || []).filter(
282
+ (via) => !PcbScene3dCopperDetailFilter.#isViaExplicitlyOpen(via)
283
+ )
284
+ }
285
+
286
+ /**
287
+ * Checks whether one via explicitly exposes copper on either side.
288
+ * @param {object} via Via primitive.
289
+ * @returns {boolean}
290
+ */
291
+ static #isViaExplicitlyOpen(via) {
292
+ return via?.isTentingTop === false || via?.isTentingBottom === false
216
293
  }
217
294
 
218
295
  /**
@@ -1,6 +1,8 @@
1
1
  import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
2
2
  import { PcbScene3dCopperDetailFilter } from './PcbScene3dCopperDetailFilter.mjs'
3
+ import { PcbScene3dCopperDrillCutoutBuilder } from './PcbScene3dCopperDrillCutoutBuilder.mjs'
3
4
  import { PcbScene3dCopperFactory } from './PcbScene3dCopperFactory.mjs'
5
+ import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
4
6
  import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
5
7
  import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
6
8
 
@@ -9,6 +11,8 @@ import { PcbScene3dViaFactory } from './PcbScene3dViaFactory.mjs'
9
11
  */
10
12
  export class PcbScene3dCopperDetailGroupBuilder {
11
13
  static #CIRCLE_SEGMENTS = 64
14
+ static #COVERED_COPPER_UNDERLAP_MIL = 6
15
+ static #MAX_SILKSCREEN_OCCLUSION_SPAN_MIL = 500
12
16
  static #ROUNDED_CORNER_SEGMENTS = 12
13
17
 
14
18
  /**
@@ -21,11 +25,21 @@ export class PcbScene3dCopperDetailGroupBuilder {
21
25
  */
22
26
  static build(THREE, sceneDescription, topZ, normalizePoint) {
23
27
  const group = new THREE.Group()
28
+ const drillCutouts = PcbScene3dCopperDrillCutoutBuilder.resolve(
29
+ sceneDescription?.detail
30
+ )
24
31
  const coveredGroup =
25
32
  PcbScene3dCopperDetailGroupBuilder.#buildCoveredGroup(
26
33
  THREE,
27
34
  sceneDescription,
28
35
  topZ,
36
+ normalizePoint,
37
+ drillCutouts
38
+ )
39
+ const coveredViaGroup =
40
+ PcbScene3dCopperDetailGroupBuilder.#buildCoveredViaGroup(
41
+ THREE,
42
+ sceneDescription,
29
43
  normalizePoint
30
44
  )
31
45
  const exposedGroup =
@@ -33,7 +47,8 @@ export class PcbScene3dCopperDetailGroupBuilder {
33
47
  THREE,
34
48
  sceneDescription,
35
49
  topZ,
36
- normalizePoint
50
+ normalizePoint,
51
+ drillCutouts
37
52
  )
38
53
  const viaGroup = PcbScene3dCopperDetailGroupBuilder.#buildViaGroup(
39
54
  THREE,
@@ -41,7 +56,7 @@ export class PcbScene3dCopperDetailGroupBuilder {
41
56
  normalizePoint
42
57
  )
43
58
 
44
- ;[coveredGroup, exposedGroup, viaGroup]
59
+ ;[coveredGroup, coveredViaGroup, exposedGroup, viaGroup]
45
60
  .filter((child) => child.children.length)
46
61
  .forEach((child) => group.add(child))
47
62
 
@@ -54,9 +69,16 @@ export class PcbScene3dCopperDetailGroupBuilder {
54
69
  * @param {object} sceneDescription Scene description.
55
70
  * @param {number} topZ Top copper center Z.
56
71
  * @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
72
+ * @param {{ x: number, y: number }[][]} drillCutouts Board drill cutouts.
57
73
  * @returns {any}
58
74
  */
59
- static #buildCoveredGroup(THREE, sceneDescription, topZ, normalizePoint) {
75
+ static #buildCoveredGroup(
76
+ THREE,
77
+ sceneDescription,
78
+ topZ,
79
+ normalizePoint,
80
+ drillCutouts
81
+ ) {
60
82
  return PcbScene3dCopperFactory.buildMaskCoveredGroup(
61
83
  THREE,
62
84
  PcbScene3dCopperDetailFilter.resolveCoveredByMask(sceneDescription),
@@ -64,15 +86,11 @@ export class PcbScene3dCopperDetailGroupBuilder {
64
86
  -topZ,
65
87
  normalizePoint,
66
88
  {
67
- solderMaskColor:
68
- PcbScene3dBoardMaterialPalette.resolveSurfaceColor(
69
- sceneDescription?.board,
70
- {
71
- hasBoardAssemblyModel: Boolean(
72
- sceneDescription?.boardAssemblyModel
73
- )
74
- }
75
- ),
89
+ ...PcbScene3dCopperDetailGroupBuilder.#coveredCopperMaterialOptions(
90
+ sceneDescription
91
+ ),
92
+ drillCutouts,
93
+ drillDetail: sceneDescription?.detail,
76
94
  occlusionCutouts:
77
95
  PcbScene3dCopperDetailGroupBuilder.#resolveCoveredCopperOcclusions(
78
96
  sceneDescription?.detail
@@ -81,22 +99,65 @@ export class PcbScene3dCopperDetailGroupBuilder {
81
99
  )
82
100
  }
83
101
 
102
+ /**
103
+ * Builds solder-mask-covered via annuli.
104
+ * @param {any} THREE Three.js namespace.
105
+ * @param {object} sceneDescription Scene description.
106
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
107
+ * @returns {any}
108
+ */
109
+ static #buildCoveredViaGroup(THREE, sceneDescription, normalizePoint) {
110
+ const vias =
111
+ PcbScene3dCopperDetailFilter.resolveCoveredStandaloneVias(
112
+ sceneDescription
113
+ )
114
+ if (!vias.length) {
115
+ return new THREE.Group()
116
+ }
117
+
118
+ return PcbScene3dViaFactory.buildGroup(
119
+ THREE,
120
+ vias,
121
+ sceneDescription?.board?.thicknessMil,
122
+ normalizePoint,
123
+ {
124
+ material: PcbScene3dMaskCoveredCopperMaterial.build(
125
+ THREE,
126
+ PcbScene3dCopperDetailGroupBuilder.#coveredCopperMaterialOptions(
127
+ sceneDescription
128
+ )
129
+ )
130
+ }
131
+ )
132
+ }
133
+
84
134
  /**
85
135
  * Builds exposed copper detail.
86
136
  * @param {any} THREE Three.js namespace.
87
137
  * @param {object} sceneDescription Scene description.
88
138
  * @param {number} topZ Top copper center Z.
89
139
  * @param {(x: number, y: number) => { x: number, y: number }} normalizePoint
140
+ * @param {{ x: number, y: number }[][]} drillCutouts Board drill cutouts.
90
141
  * @returns {any}
91
142
  */
92
- static #buildExposedGroup(THREE, sceneDescription, topZ, normalizePoint) {
143
+ static #buildExposedGroup(
144
+ THREE,
145
+ sceneDescription,
146
+ topZ,
147
+ normalizePoint,
148
+ drillCutouts
149
+ ) {
93
150
  return PcbScene3dCopperFactory.buildGroup(
94
151
  THREE,
95
152
  PcbScene3dCopperDetailFilter.resolve(sceneDescription),
96
153
  topZ,
97
154
  -topZ,
98
155
  normalizePoint,
99
- { coordinateSystem: sceneDescription?.coordinateSystem }
156
+ {
157
+ coordinateSystem: sceneDescription?.coordinateSystem,
158
+ drillCutouts,
159
+ drillDetail: sceneDescription?.detail
160
+ }
100
161
  )
101
162
  }
102
163
 
@@ -126,6 +187,24 @@ export class PcbScene3dCopperDetailGroupBuilder {
126
187
  )
127
188
  }
128
189
 
190
+ /**
191
+ * Resolves solder-mask material options for covered copper relief.
192
+ * @param {object} sceneDescription Scene description.
193
+ * @returns {{ solderMaskColor: number }}
194
+ */
195
+ static #coveredCopperMaterialOptions(sceneDescription) {
196
+ return {
197
+ solderMaskColor: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(
198
+ sceneDescription?.board,
199
+ {
200
+ hasBoardAssemblyModel: Boolean(
201
+ sceneDescription?.boardAssemblyModel
202
+ )
203
+ }
204
+ )
205
+ }
206
+ }
207
+
129
208
  /**
130
209
  * Resolves opaque silkscreen fill polygons that should hide covered copper.
131
210
  * @param {{ top?: object, bottom?: object } | undefined} silkscreen Scene silkscreen detail.
@@ -168,6 +247,11 @@ export class PcbScene3dCopperDetailGroupBuilder {
168
247
  )
169
248
  )
170
249
  .filter((points) => points.length >= 3)
250
+ .filter((points) =>
251
+ PcbScene3dCopperDetailGroupBuilder.#isBoundedSilkscreenOcclusion(
252
+ points
253
+ )
254
+ )
171
255
  }
172
256
 
173
257
  /**
@@ -198,6 +282,44 @@ export class PcbScene3dCopperDetailGroupBuilder {
198
282
  ]
199
283
  }
200
284
 
285
+ /**
286
+ * Checks whether a silkscreen fill is small enough for copper clipping.
287
+ * @param {{ x: number, y: number }[]} points Fill contour points.
288
+ * @returns {boolean}
289
+ */
290
+ static #isBoundedSilkscreenOcclusion(points) {
291
+ const bounds =
292
+ PcbScene3dCopperDetailGroupBuilder.#resolveSilkscreenBounds(points)
293
+ const span = Math.max(
294
+ bounds.maxX - bounds.minX,
295
+ bounds.maxY - bounds.minY
296
+ )
297
+
298
+ return (
299
+ Number.isFinite(span) &&
300
+ span <=
301
+ PcbScene3dCopperDetailGroupBuilder
302
+ .#MAX_SILKSCREEN_OCCLUSION_SPAN_MIL
303
+ )
304
+ }
305
+
306
+ /**
307
+ * Resolves the bounds for one silkscreen fill contour.
308
+ * @param {{ x: number, y: number }[]} points Fill contour points.
309
+ * @returns {{ minX: number, maxX: number, minY: number, maxY: number }}
310
+ */
311
+ static #resolveSilkscreenBounds(points) {
312
+ return points.reduce(
313
+ (bounds, point) => ({
314
+ minX: Math.min(bounds.minX, point.x),
315
+ maxX: Math.max(bounds.maxX, point.x),
316
+ minY: Math.min(bounds.minY, point.y),
317
+ maxY: Math.max(bounds.maxY, point.y)
318
+ }),
319
+ { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity }
320
+ )
321
+ }
322
+
201
323
  /**
202
324
  * Resolves opaque geometry that should hide mask-covered copper.
203
325
  * @param {object | undefined} detail Scene detail.
@@ -318,18 +440,67 @@ export class PcbScene3dCopperDetailGroupBuilder {
318
440
  */
319
441
  static #resolvePadSurfacePolygon(pad, side) {
320
442
  const spec = PcbScene3dPadFactory.resolvePadSurfaceSpec(pad, side)
443
+ const occlusionSpec =
444
+ PcbScene3dCopperDetailGroupBuilder.#insetPadSurfaceSpec(spec)
321
445
  const center = {
322
446
  x: Number(pad?.x || 0) + spec.offsetX,
323
447
  y: Number(pad?.y || 0) + spec.offsetY
324
448
  }
325
449
 
450
+ if (!occlusionSpec) {
451
+ return []
452
+ }
453
+
326
454
  return PcbScene3dCopperDetailGroupBuilder.#transformPoints(
327
- PcbScene3dCopperDetailGroupBuilder.#buildPadLocalPoints(spec),
455
+ PcbScene3dCopperDetailGroupBuilder.#buildPadLocalPoints(
456
+ occlusionSpec
457
+ ),
328
458
  center,
329
459
  Number(pad?.rotation || 0)
330
460
  )
331
461
  }
332
462
 
463
+ /**
464
+ * Insets a pad occlusion so mask-covered traces can visually tuck below
465
+ * exposed copper sidewalls instead of ending at the same vertical plane.
466
+ * @param {{ width: number, height: number, kind: string, cornerRadius: number }} spec Pad surface spec.
467
+ * @returns {{ width: number, height: number, kind: string, cornerRadius: number } | null}
468
+ */
469
+ static #insetPadSurfaceSpec(spec) {
470
+ const inset = PcbScene3dCopperDetailGroupBuilder.#resolvePadInset(spec)
471
+ const width = Math.max(Number(spec?.width || 0) - inset * 2, 0)
472
+ const height = Math.max(Number(spec?.height || 0) - inset * 2, 0)
473
+
474
+ if (width <= 0 || height <= 0) {
475
+ return null
476
+ }
477
+
478
+ return {
479
+ ...spec,
480
+ width,
481
+ height,
482
+ radius: Math.max(width, height) / 2,
483
+ cornerRadius: Math.max(Number(spec?.cornerRadius || 0) - inset, 0)
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Resolves a bounded pad occlusion inset.
489
+ * @param {{ width: number, height: number }} spec Pad surface spec.
490
+ * @returns {number}
491
+ */
492
+ static #resolvePadInset(spec) {
493
+ return Math.min(
494
+ PcbScene3dCopperDetailGroupBuilder.#COVERED_COPPER_UNDERLAP_MIL,
495
+ Math.max(
496
+ Math.min(Number(spec?.width || 0), Number(spec?.height || 0)) /
497
+ 2 -
498
+ 0.001,
499
+ 0
500
+ )
501
+ )
502
+ }
503
+
333
504
  /**
334
505
  * Builds local points for one pad face.
335
506
  * @param {{ width: number, height: number, kind: string, cornerRadius: number }} spec Pad surface spec.