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
@@ -0,0 +1,258 @@
1
+ import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
2
+
3
+ /**
4
+ * Builds normalized board drill cutouts for copper surface meshes.
5
+ */
6
+ export class PcbScene3dCopperDrillCutoutBuilder {
7
+ static #DRILL_CUTOUT_SEGMENTS = 48
8
+ static #SLOT_CAP_SEGMENTS = 12
9
+
10
+ /**
11
+ * Resolves board drill cutouts for copper fill apertures.
12
+ * @param {{ pads?: any[], vias?: any[] }} detail Copper detail.
13
+ * @param {(x: number, y: number) => { x: number, y: number }} [normalizeBoardPoint]
14
+ * @param {boolean} [mirrorY] Whether to mirror underside primitives.
15
+ * @returns {{ x: number, y: number }[][]}
16
+ */
17
+ static resolve(
18
+ detail,
19
+ normalizeBoardPoint = (x, y) => ({ x, y }),
20
+ mirrorY = false
21
+ ) {
22
+ return PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail)
23
+ .map((drillSpec) =>
24
+ PcbScene3dCopperDrillCutoutBuilder.#resolveCutoutPoints(
25
+ drillSpec,
26
+ normalizeBoardPoint,
27
+ mirrorY
28
+ )
29
+ )
30
+ .filter((points) => points.length >= 3)
31
+ }
32
+
33
+ /**
34
+ * Resolves cutouts from explicit options or fallback drill detail.
35
+ * @param {{ drillCutouts?: any[], drillDetail?: object } | undefined} options
36
+ * @param {{ pads?: any[], vias?: any[] }} fallbackDetail Fallback detail.
37
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
38
+ * @param {boolean} mirrorY Whether to mirror underside primitives.
39
+ * @returns {{ x: number, y: number }[][]}
40
+ */
41
+ static resolveFromOptions(
42
+ options,
43
+ fallbackDetail,
44
+ normalizeBoardPoint,
45
+ mirrorY
46
+ ) {
47
+ if (Array.isArray(options?.drillCutouts)) {
48
+ return PcbScene3dCopperDrillCutoutBuilder.#normalizeCutouts(
49
+ options.drillCutouts,
50
+ normalizeBoardPoint,
51
+ mirrorY
52
+ )
53
+ }
54
+
55
+ return PcbScene3dCopperDrillCutoutBuilder.resolve(
56
+ options?.drillDetail || fallbackDetail,
57
+ normalizeBoardPoint,
58
+ mirrorY
59
+ )
60
+ }
61
+
62
+ /**
63
+ * Resolves one drill cutout point loop.
64
+ * @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec
65
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
66
+ * @param {boolean} mirrorY Whether to mirror underside primitives.
67
+ * @returns {{ x: number, y: number }[]}
68
+ */
69
+ static #resolveCutoutPoints(drillSpec, normalizeBoardPoint, mirrorY) {
70
+ const points =
71
+ Number(drillSpec?.slotLength || 0) >
72
+ Number(drillSpec?.diameter || 0) + 0.001
73
+ ? PcbScene3dCopperDrillCutoutBuilder.#buildSlotPoints(drillSpec)
74
+ : PcbScene3dCopperDrillCutoutBuilder.#buildCirclePoints(
75
+ drillSpec
76
+ )
77
+
78
+ return points.map((point) =>
79
+ PcbScene3dCopperDrillCutoutBuilder.#normalizePoint(
80
+ point,
81
+ normalizeBoardPoint,
82
+ mirrorY
83
+ )
84
+ )
85
+ }
86
+
87
+ /**
88
+ * Normalizes cutout point loops.
89
+ * @param {{ x?: number, y?: number }[][]} cutouts Cutout loops.
90
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
91
+ * @param {boolean} mirrorY Whether to mirror underside primitives.
92
+ * @returns {{ x: number, y: number }[][]}
93
+ */
94
+ static #normalizeCutouts(cutouts, normalizeBoardPoint, mirrorY) {
95
+ return cutouts
96
+ .map((cutout) =>
97
+ (Array.isArray(cutout) ? cutout : []).map((point) =>
98
+ PcbScene3dCopperDrillCutoutBuilder.#normalizePoint(
99
+ point,
100
+ normalizeBoardPoint,
101
+ mirrorY
102
+ )
103
+ )
104
+ )
105
+ .filter((cutout) => cutout.length >= 3)
106
+ }
107
+
108
+ /**
109
+ * Normalizes one cutout point.
110
+ * @param {{ x?: number, y?: number }} point Source point.
111
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
112
+ * @param {boolean} mirrorY Whether to mirror underside primitives.
113
+ * @returns {{ x: number, y: number }}
114
+ */
115
+ static #normalizePoint(point, normalizeBoardPoint, mirrorY) {
116
+ const normalized = normalizeBoardPoint(
117
+ Number(point?.x || 0),
118
+ Number(point?.y || 0)
119
+ )
120
+
121
+ return {
122
+ x: Number(normalized?.x || 0),
123
+ y: mirrorY
124
+ ? -Number(normalized?.y || 0)
125
+ : Number(normalized?.y || 0)
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Builds one sampled circular drill loop.
131
+ * @param {{ x: number, y: number, diameter: number }} drillSpec Drill spec.
132
+ * @returns {{ x: number, y: number }[]}
133
+ */
134
+ static #buildCirclePoints(drillSpec) {
135
+ const radius = Math.max(Number(drillSpec?.diameter || 0) / 2, 0.6)
136
+ const centerX = Number(drillSpec?.x || 0)
137
+ const centerY = Number(drillSpec?.y || 0)
138
+
139
+ return Array.from(
140
+ {
141
+ length: PcbScene3dCopperDrillCutoutBuilder
142
+ .#DRILL_CUTOUT_SEGMENTS
143
+ },
144
+ (_, index) => {
145
+ const angle =
146
+ (Math.PI * 2 * index) /
147
+ PcbScene3dCopperDrillCutoutBuilder.#DRILL_CUTOUT_SEGMENTS
148
+
149
+ return {
150
+ x: centerX + Math.cos(angle) * radius,
151
+ y: centerY + Math.sin(angle) * radius
152
+ }
153
+ }
154
+ )
155
+ }
156
+
157
+ /**
158
+ * Builds one sampled slotted drill loop.
159
+ * @param {{ x: number, y: number, diameter: number, slotLength?: number | null, rotationDeg?: number | null }} drillSpec Drill spec.
160
+ * @returns {{ x: number, y: number }[]}
161
+ */
162
+ static #buildSlotPoints(drillSpec) {
163
+ const diameter = Math.max(Number(drillSpec?.diameter || 0), 1.2)
164
+ const radius = Math.max(diameter / 2, 0.6)
165
+ const slotLength = Math.max(
166
+ Number(drillSpec?.slotLength || 0),
167
+ diameter
168
+ )
169
+ const straightHalf = Math.max(slotLength / 2 - radius, 0)
170
+ const rotationRad =
171
+ (Number(drillSpec?.rotationDeg || 0) * Math.PI) / 180
172
+ const points = []
173
+
174
+ PcbScene3dCopperDrillCutoutBuilder.#appendArcPoints(
175
+ points,
176
+ straightHalf,
177
+ 0,
178
+ radius,
179
+ -Math.PI / 2,
180
+ Math.PI / 2
181
+ )
182
+ PcbScene3dCopperDrillCutoutBuilder.#appendArcPoints(
183
+ points,
184
+ -straightHalf,
185
+ 0,
186
+ radius,
187
+ Math.PI / 2,
188
+ (Math.PI * 3) / 2,
189
+ true
190
+ )
191
+
192
+ return points.map((point) =>
193
+ PcbScene3dCopperDrillCutoutBuilder.#rotateAndTranslatePoint(
194
+ point,
195
+ rotationRad,
196
+ Number(drillSpec?.x || 0),
197
+ Number(drillSpec?.y || 0)
198
+ )
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Appends sampled points for one slot cap arc.
204
+ * @param {{ x: number, y: number }[]} points Output point list.
205
+ * @param {number} cx Arc center X.
206
+ * @param {number} cy Arc center Y.
207
+ * @param {number} radius Arc radius.
208
+ * @param {number} startAngle Start angle in radians.
209
+ * @param {number} endAngle End angle in radians.
210
+ * @param {boolean} [skipFirst] Whether to skip the first sample.
211
+ * @returns {void}
212
+ */
213
+ static #appendArcPoints(
214
+ points,
215
+ cx,
216
+ cy,
217
+ radius,
218
+ startAngle,
219
+ endAngle,
220
+ skipFirst = false
221
+ ) {
222
+ for (
223
+ let index = 0;
224
+ index <= PcbScene3dCopperDrillCutoutBuilder.#SLOT_CAP_SEGMENTS;
225
+ index += 1
226
+ ) {
227
+ if (skipFirst && index === 0) {
228
+ continue
229
+ }
230
+
231
+ const t =
232
+ index / PcbScene3dCopperDrillCutoutBuilder.#SLOT_CAP_SEGMENTS
233
+ const angle = startAngle + (endAngle - startAngle) * t
234
+ points.push({
235
+ x: cx + Math.cos(angle) * radius,
236
+ y: cy + Math.sin(angle) * radius
237
+ })
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Rotates one local point around the origin and translates it into place.
243
+ * @param {{ x: number, y: number }} point Local point.
244
+ * @param {number} rotationRad Rotation in radians.
245
+ * @param {number} dx Translation X.
246
+ * @param {number} dy Translation Y.
247
+ * @returns {{ x: number, y: number }}
248
+ */
249
+ static #rotateAndTranslatePoint(point, rotationRad, dx, dy) {
250
+ const cos = Math.cos(rotationRad)
251
+ const sin = Math.sin(rotationRad)
252
+
253
+ return {
254
+ x: point.x * cos - point.y * sin + dx,
255
+ y: point.x * sin + point.y * cos + dy
256
+ }
257
+ }
258
+ }
@@ -2,9 +2,11 @@ import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
2
2
  import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
3
3
  import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
4
4
  import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
5
- import { PcbScene3dGeometryZCompressor } from './PcbScene3dGeometryZCompressor.mjs'
6
5
  import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
7
6
  import { PcbScene3dCopperLayerFilter } from './PcbScene3dCopperLayerFilter.mjs'
7
+ import { PcbScene3dCopperFillMeshBuilder } from './PcbScene3dCopperFillMeshBuilder.mjs'
8
+ import { PcbScene3dMaskCoveredCopperSideGroupBuilder } from './PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs'
9
+ import { PcbScene3dCopperDrillCutoutBuilder } from './PcbScene3dCopperDrillCutoutBuilder.mjs'
8
10
 
9
11
  /**
10
12
  * Builds copper-detail meshes for the interactive 3D PCB scene.
@@ -27,11 +29,11 @@ export class PcbScene3dCopperFactory {
27
29
  /**
28
30
  * Builds the combined top and bottom copper group.
29
31
  * @param {any} THREE
30
- * @param {{ tracks?: any[], arcs?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
32
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[], polygons?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
31
33
  * @param {number} topZ
32
34
  * @param {number} bottomZ
33
35
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
34
- * @param {{ coordinateSystem?: string }} [options]
36
+ * @param {{ coordinateSystem?: string, drillCutouts?: any[], drillDetail?: object }} [options]
35
37
  * @returns {any}
36
38
  */
37
39
  static buildGroup(
@@ -51,6 +53,10 @@ export class PcbScene3dCopperFactory {
51
53
  'top'
52
54
  ),
53
55
  arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top'),
56
+ fills: PcbScene3dCopperLayerFilter.fills(
57
+ [...(detail?.fills || []), ...(detail?.polygons || [])],
58
+ 'top'
59
+ ),
54
60
  pads: detail?.pads || [],
55
61
  vias: detail?.vias || [],
56
62
  copperTexts: detail?.copperTexts || []
@@ -68,6 +74,10 @@ export class PcbScene3dCopperFactory {
68
74
  'bottom'
69
75
  ),
70
76
  arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom'),
77
+ fills: PcbScene3dCopperLayerFilter.fills(
78
+ [...(detail?.fills || []), ...(detail?.polygons || [])],
79
+ 'bottom'
80
+ ),
71
81
  pads: detail?.pads || [],
72
82
  vias: detail?.vias || [],
73
83
  copperTexts: detail?.copperTexts || []
@@ -78,24 +88,20 @@ export class PcbScene3dCopperFactory {
78
88
  options
79
89
  )
80
90
 
81
- if (topGroup.children.length) {
82
- group.add(topGroup)
83
- }
84
- if (bottomGroup.children.length) {
85
- group.add(bottomGroup)
86
- }
91
+ if (topGroup.children.length) group.add(topGroup)
92
+ if (bottomGroup.children.length) group.add(bottomGroup)
87
93
 
88
94
  return group
89
95
  }
90
96
 
91
97
  /**
92
- * Builds top and bottom traces that are covered by solder mask.
98
+ * Builds top and bottom copper detail that is covered by solder mask.
93
99
  * @param {any} THREE
94
- * @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
100
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[], polygons?: any[] }} detail Mask-covered detail.
95
101
  * @param {number} topZ
96
102
  * @param {number} bottomZ
97
103
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
98
- * @param {{ solderMaskColor?: number, occlusionCutouts?: { top?: { x: number, y: number }[][], bottom?: { x: number, y: number }[][] } }} [options]
104
+ * @param {{ solderMaskColor?: number, occlusionCutouts?: object, drillCutouts?: any[], drillDetail?: object }} [options]
99
105
  * @returns {any}
100
106
  */
101
107
  static buildMaskCoveredGroup(
@@ -111,6 +117,21 @@ export class PcbScene3dCopperFactory {
111
117
  THREE,
112
118
  options
113
119
  )
120
+ const drillCutouts = Array.isArray(options?.drillCutouts)
121
+ ? options.drillCutouts
122
+ : PcbScene3dCopperDrillCutoutBuilder.resolve(
123
+ options?.drillDetail || detail
124
+ )
125
+ const topCutouts = PcbScene3dCopperOcclusionClipper.normalizeCutouts(
126
+ [...(options?.occlusionCutouts?.top || []), ...drillCutouts],
127
+ normalizeBoardPoint,
128
+ false
129
+ )
130
+ const bottomCutouts = PcbScene3dCopperOcclusionClipper.normalizeCutouts(
131
+ [...(options?.occlusionCutouts?.bottom || []), ...drillCutouts],
132
+ normalizeBoardPoint,
133
+ true
134
+ )
114
135
  const topGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
115
136
  THREE,
116
137
  {
@@ -118,17 +139,17 @@ export class PcbScene3dCopperFactory {
118
139
  detail?.tracks,
119
140
  'top'
120
141
  ),
121
- arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top')
142
+ arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top'),
143
+ fills: PcbScene3dCopperLayerFilter.fills(
144
+ [...(detail?.fills || []), ...(detail?.polygons || [])],
145
+ 'top'
146
+ )
122
147
  },
123
148
  Math.abs(Number(topZ || 0)),
124
149
  normalizeBoardPoint,
125
150
  false,
126
151
  material,
127
- PcbScene3dCopperOcclusionClipper.normalizeCutouts(
128
- options?.occlusionCutouts?.top,
129
- normalizeBoardPoint,
130
- false
131
- )
152
+ topCutouts
132
153
  )
133
154
  const bottomGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
134
155
  THREE,
@@ -137,25 +158,21 @@ export class PcbScene3dCopperFactory {
137
158
  detail?.tracks,
138
159
  'bottom'
139
160
  ),
140
- arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom')
161
+ arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom'),
162
+ fills: PcbScene3dCopperLayerFilter.fills(
163
+ [...(detail?.fills || []), ...(detail?.polygons || [])],
164
+ 'bottom'
165
+ )
141
166
  },
142
167
  Math.abs(Number(bottomZ || 0)),
143
168
  normalizeBoardPoint,
144
169
  true,
145
170
  material,
146
- PcbScene3dCopperOcclusionClipper.normalizeCutouts(
147
- options?.occlusionCutouts?.bottom,
148
- normalizeBoardPoint,
149
- true
150
- )
171
+ bottomCutouts
151
172
  )
152
173
 
153
- if (topGroup.children.length) {
154
- group.add(topGroup)
155
- }
156
- if (bottomGroup.children.length) {
157
- group.add(bottomGroup)
158
- }
174
+ if (topGroup.children.length) group.add(topGroup)
175
+ if (bottomGroup.children.length) group.add(bottomGroup)
159
176
 
160
177
  return group
161
178
  }
@@ -163,11 +180,11 @@ export class PcbScene3dCopperFactory {
163
180
  /**
164
181
  * Builds one side-specific copper group.
165
182
  * @param {any} THREE
166
- * @param {{ tracks?: any[], arcs?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
183
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
167
184
  * @param {number} z
168
185
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
169
186
  * @param {boolean} mirrorY
170
- * @param {{ coordinateSystem?: string }} [options]
187
+ * @param {{ coordinateSystem?: string, drillCutouts?: any[], drillDetail?: object }} [options]
171
188
  * @returns {any}
172
189
  */
173
190
  static #buildSideGroup(
@@ -179,6 +196,13 @@ export class PcbScene3dCopperFactory {
179
196
  options = {}
180
197
  ) {
181
198
  const group = new THREE.Group()
199
+ const drillCutouts =
200
+ PcbScene3dCopperDrillCutoutBuilder.resolveFromOptions(
201
+ options,
202
+ detail,
203
+ normalizeBoardPoint,
204
+ mirrorY
205
+ )
182
206
  const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
183
207
  THREE,
184
208
  detail?.tracks || [],
@@ -193,6 +217,16 @@ export class PcbScene3dCopperFactory {
193
217
  normalizeBoardPoint,
194
218
  mirrorY
195
219
  )
220
+ const fillMesh = PcbScene3dCopperFillMeshBuilder.build(
221
+ THREE,
222
+ detail?.fills || [],
223
+ z,
224
+ PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL,
225
+ normalizeBoardPoint,
226
+ mirrorY,
227
+ PcbScene3dCopperFactory.#buildMaterial(THREE),
228
+ drillCutouts
229
+ )
196
230
  const padGroup = PcbScene3dPadFactory.buildGroup(
197
231
  THREE,
198
232
  detail?.pads || [],
@@ -215,29 +249,20 @@ export class PcbScene3dCopperFactory {
215
249
  }
216
250
  )
217
251
 
218
- if (trackMesh) {
219
- group.add(trackMesh)
220
- }
221
- if (arcMesh) {
222
- group.add(arcMesh)
223
- }
224
- if (padGroup.children.length) {
225
- group.add(padGroup)
226
- }
227
- if (textGroup.children.length) {
228
- group.add(textGroup)
229
- }
230
- if (mirrorY && group.children.length) {
231
- group.rotation.x = Math.PI
232
- }
252
+ if (trackMesh) group.add(trackMesh)
253
+ if (arcMesh) group.add(arcMesh)
254
+ if (fillMesh) group.add(fillMesh)
255
+ if (padGroup.children.length) group.add(padGroup)
256
+ if (textGroup.children.length) group.add(textGroup)
257
+ if (mirrorY && group.children.length) group.rotation.x = Math.PI
233
258
 
234
259
  return group
235
260
  }
236
261
 
237
262
  /**
238
- * Builds one side of the mask-covered trace relief.
263
+ * Builds one side of the mask-covered copper relief.
239
264
  * @param {any} THREE
240
- * @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
265
+ * @param {{ tracks?: any[], arcs?: any[], fills?: any[] }} detail Mask-covered detail.
241
266
  * @param {number} z
242
267
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
243
268
  * @param {boolean} mirrorY
@@ -254,7 +279,6 @@ export class PcbScene3dCopperFactory {
254
279
  material,
255
280
  occlusionCutouts
256
281
  ) {
257
- const group = new THREE.Group()
258
282
  const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
259
283
  THREE,
260
284
  detail?.tracks || [],
@@ -273,28 +297,23 @@ export class PcbScene3dCopperFactory {
273
297
  material,
274
298
  occlusionCutouts
275
299
  )
276
-
277
- if (trackMesh) {
278
- PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(
279
- trackMesh,
280
- z
281
- )
282
- trackMesh.name = 'mask-covered-copper-tracks'
283
- group.add(trackMesh)
284
- }
285
- if (arcMesh) {
286
- PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(
287
- arcMesh,
288
- z
289
- )
290
- arcMesh.name = 'mask-covered-copper-arcs'
291
- group.add(arcMesh)
292
- }
293
- if (mirrorY && group.children.length) {
294
- group.rotation.x = Math.PI
295
- }
296
-
297
- return group
300
+ const fillMesh = PcbScene3dCopperFillMeshBuilder.build(
301
+ THREE,
302
+ detail?.fills || [],
303
+ z,
304
+ PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL,
305
+ normalizeBoardPoint,
306
+ mirrorY,
307
+ material,
308
+ occlusionCutouts
309
+ )
310
+ return PcbScene3dMaskCoveredCopperSideGroupBuilder.build(THREE, {
311
+ trackMesh,
312
+ arcMesh,
313
+ fillMesh,
314
+ z,
315
+ mirrorY
316
+ })
298
317
  }
299
318
 
300
319
  /**
@@ -309,7 +328,7 @@ export class PcbScene3dCopperFactory {
309
328
  /**
310
329
  * Builds one widened copper-track mesh for one face.
311
330
  * @param {any} THREE
312
- * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number, capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }[]} tracks
331
+ * @param {any[]} tracks
313
332
  * @param {number} z
314
333
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
315
334
  * @param {boolean} mirrorY
@@ -362,7 +381,7 @@ export class PcbScene3dCopperFactory {
362
381
  /**
363
382
  * Builds one widened copper-arc mesh for one face.
364
383
  * @param {any} THREE
365
- * @param {{ x?: number, y?: number, radius?: number, startAngle?: number, endAngle?: number, width?: number }[]} arcs
384
+ * @param {any[]} arcs
366
385
  * @param {number} z
367
386
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
368
387
  * @param {boolean} mirrorY
@@ -463,7 +482,7 @@ export class PcbScene3dCopperFactory {
463
482
  * @param {{ x: number, y: number }} end
464
483
  * @param {number} width
465
484
  * @param {number} z
466
- * @param {{ capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }} [options]
485
+ * @param {object} [options]
467
486
  * @returns {void}
468
487
  */
469
488
  static #appendTrackTriangles(
@@ -705,9 +724,7 @@ export class PcbScene3dCopperFactory {
705
724
  */
706
725
  static #appendDiscTriangles(positions, center, radius, z) {
707
726
  const safeRadius = Math.max(Number(radius || 0), 0)
708
- if (safeRadius <= 0) {
709
- return
710
- }
727
+ if (safeRadius <= 0) return
711
728
 
712
729
  for (
713
730
  let index = 0;
@@ -755,9 +772,9 @@ export class PcbScene3dCopperFactory {
755
772
  * @param {{ x: number, y: number }} center Cap center.
756
773
  * @param {number} radius Cap radius.
757
774
  * @param {number} z Center Z position.
758
- * @param {number} outwardX Unit X direction pointing out of the stroke.
759
- * @param {number} outwardY Unit Y direction pointing out of the stroke.
760
- * @param {boolean} [includeSideWall] Whether to emit the cap perimeter wall.
775
+ * @param {number} outwardX Outward unit X.
776
+ * @param {number} outwardY Outward unit Y.
777
+ * @param {boolean} [includeSideWall] Emits cap perimeter wall.
761
778
  * @returns {void}
762
779
  */
763
780
  static #appendRoundCapTriangles(
@@ -771,9 +788,7 @@ export class PcbScene3dCopperFactory {
771
788
  ) {
772
789
  const safeRadius = Math.max(Number(radius || 0), 0)
773
790
  const outwardLength = Math.hypot(outwardX, outwardY)
774
- if (safeRadius <= 0) {
775
- return
776
- }
791
+ if (safeRadius <= 0) return
777
792
  if (outwardLength <= 0.001) {
778
793
  PcbScene3dCopperFactory.#appendDiscTriangles(
779
794
  positions,
@@ -916,7 +931,6 @@ export class PcbScene3dCopperFactory {
916
931
  const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
917
932
  const topZ = z + halfThickness
918
933
  const bottomZ = z - halfThickness
919
-
920
934
  positions.push(
921
935
  a.x,
922
936
  a.y,