pcb-scene3d-viewer 1.0.1
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
|
+
import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
|
|
3
|
+
import { PcbScene3dDrillPathFactory } from './PcbScene3dDrillPathFactory.mjs'
|
|
4
|
+
import { PcbScene3dOutlineBuilder } from './PcbScene3dOutlineBuilder.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds separate solder-mask face sheets for board-assembly rendering.
|
|
8
|
+
*/
|
|
9
|
+
export class PcbScene3dBoardSolderMaskFactory {
|
|
10
|
+
static #CURVE_SEGMENTS = 20
|
|
11
|
+
static #OUTER_SAMPLE_POINTS = 160
|
|
12
|
+
static #DRILL_SAMPLE_POINTS = 72
|
|
13
|
+
static #EDGE_CLEARANCE_MIL = 0
|
|
14
|
+
static #FACE_Z_OFFSET_MIL = 0
|
|
15
|
+
static #EDGE_CLIP_MAX_DEPTH = 10
|
|
16
|
+
static #EDGE_CLIP_MAX_EDGE_LENGTH_MIL = 2
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds top and bottom solder-mask face meshes for generated board bodies.
|
|
20
|
+
* @param {any} THREE
|
|
21
|
+
* @param {{ board?: any, detail?: any, boardAssemblyModel?: any }} sceneDescription
|
|
22
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
23
|
+
* @returns {any}
|
|
24
|
+
*/
|
|
25
|
+
static buildGroup(
|
|
26
|
+
THREE,
|
|
27
|
+
sceneDescription,
|
|
28
|
+
normalizeBoardPoint = (x, y) => ({ x, y })
|
|
29
|
+
) {
|
|
30
|
+
const group = new THREE.Group()
|
|
31
|
+
group.name = 'board-solder-mask'
|
|
32
|
+
const board = sceneDescription?.board || {}
|
|
33
|
+
const hasBoardAssemblyModel = Boolean(
|
|
34
|
+
sceneDescription?.boardAssemblyModel
|
|
35
|
+
)
|
|
36
|
+
const visible = PcbScene3dBoardMaterialPalette.isGeneratedBodyVisible({
|
|
37
|
+
hasBoardAssemblyModel
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!hasBoardAssemblyModel || !visible) {
|
|
41
|
+
return group
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const geometry = PcbScene3dBoardSolderMaskFactory.#buildMaskGeometry(
|
|
45
|
+
THREE,
|
|
46
|
+
board,
|
|
47
|
+
sceneDescription?.detail || {},
|
|
48
|
+
normalizeBoardPoint
|
|
49
|
+
)
|
|
50
|
+
const topMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
51
|
+
THREE,
|
|
52
|
+
board,
|
|
53
|
+
THREE.FrontSide
|
|
54
|
+
)
|
|
55
|
+
const bottomMaterial = PcbScene3dBoardSolderMaskFactory.#buildMaterial(
|
|
56
|
+
THREE,
|
|
57
|
+
board,
|
|
58
|
+
THREE.BackSide
|
|
59
|
+
)
|
|
60
|
+
const z = Number(board?.thicknessMil || 0) / 2
|
|
61
|
+
|
|
62
|
+
group.add(
|
|
63
|
+
PcbScene3dBoardSolderMaskFactory.#buildSurfaceMesh(
|
|
64
|
+
THREE,
|
|
65
|
+
geometry,
|
|
66
|
+
topMaterial,
|
|
67
|
+
z + PcbScene3dBoardSolderMaskFactory.#FACE_Z_OFFSET_MIL,
|
|
68
|
+
'board-solder-mask-top'
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
group.add(
|
|
72
|
+
PcbScene3dBoardSolderMaskFactory.#buildSurfaceMesh(
|
|
73
|
+
THREE,
|
|
74
|
+
geometry,
|
|
75
|
+
bottomMaterial,
|
|
76
|
+
-z - PcbScene3dBoardSolderMaskFactory.#FACE_Z_OFFSET_MIL,
|
|
77
|
+
'board-solder-mask-bottom'
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return group
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Builds one flat solder-mask face mesh.
|
|
86
|
+
* @param {any} THREE
|
|
87
|
+
* @param {any} geometry Board face geometry.
|
|
88
|
+
* @param {any} material Surface material.
|
|
89
|
+
* @param {number} z Face z coordinate.
|
|
90
|
+
* @param {string} name Mesh name.
|
|
91
|
+
* @returns {any}
|
|
92
|
+
*/
|
|
93
|
+
static #buildSurfaceMesh(THREE, geometry, material, z, name) {
|
|
94
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
95
|
+
mesh.name = name
|
|
96
|
+
mesh.position.z = z
|
|
97
|
+
|
|
98
|
+
return mesh
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds the solder-mask face geometry.
|
|
103
|
+
* @param {any} THREE
|
|
104
|
+
* @param {{ widthMil?: number, heightMil?: number, segments?: Array<Record<string, number | string>> }} board
|
|
105
|
+
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
106
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
107
|
+
* @returns {any}
|
|
108
|
+
*/
|
|
109
|
+
static #buildMaskGeometry(THREE, board, detail, normalizeBoardPoint) {
|
|
110
|
+
const { shape, clippingHoles } =
|
|
111
|
+
PcbScene3dBoardSolderMaskFactory.#buildMaskShape(
|
|
112
|
+
THREE,
|
|
113
|
+
board,
|
|
114
|
+
detail,
|
|
115
|
+
normalizeBoardPoint
|
|
116
|
+
)
|
|
117
|
+
const geometry = new THREE.ShapeGeometry(
|
|
118
|
+
shape,
|
|
119
|
+
PcbScene3dBoardSolderMaskFactory.#CURVE_SEGMENTS
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return PcbScene3dCutoutGeometryFilter.filter(
|
|
123
|
+
THREE,
|
|
124
|
+
geometry,
|
|
125
|
+
clippingHoles,
|
|
126
|
+
{
|
|
127
|
+
maxDepth: PcbScene3dBoardSolderMaskFactory.#EDGE_CLIP_MAX_DEPTH,
|
|
128
|
+
maxEdgeLength:
|
|
129
|
+
PcbScene3dBoardSolderMaskFactory
|
|
130
|
+
.#EDGE_CLIP_MAX_EDGE_LENGTH_MIL
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Builds the inset mask shape and drill cutouts requiring fallback clipping.
|
|
137
|
+
* @param {any} THREE
|
|
138
|
+
* @param {{ widthMil?: number, heightMil?: number, segments?: Array<Record<string, number | string>> }} board
|
|
139
|
+
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
140
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
141
|
+
* @returns {{ shape: any, clippingHoles: { x: number, y: number }[][] }}
|
|
142
|
+
*/
|
|
143
|
+
static #buildMaskShape(THREE, board, detail, normalizeBoardPoint) {
|
|
144
|
+
const shape = PcbScene3dBoardSolderMaskFactory.#buildInsetOuterShape(
|
|
145
|
+
THREE,
|
|
146
|
+
board
|
|
147
|
+
)
|
|
148
|
+
const contourPoints =
|
|
149
|
+
PcbScene3dBoardSolderMaskFactory.#resolveShapePoints(shape)
|
|
150
|
+
const drillCutouts =
|
|
151
|
+
PcbScene3dBoardSolderMaskFactory.#resolveDrillCutouts(
|
|
152
|
+
THREE,
|
|
153
|
+
detail,
|
|
154
|
+
normalizeBoardPoint
|
|
155
|
+
)
|
|
156
|
+
const { shapeHoles, clippingCutouts } =
|
|
157
|
+
PcbScene3dBoardSolderMaskFactory.#partitionDrillCutouts(
|
|
158
|
+
drillCutouts,
|
|
159
|
+
contourPoints
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
PcbScene3dBoardSolderMaskFactory.#appendShapeHoles(
|
|
163
|
+
THREE,
|
|
164
|
+
shape,
|
|
165
|
+
shapeHoles
|
|
166
|
+
)
|
|
167
|
+
return {
|
|
168
|
+
shape,
|
|
169
|
+
clippingHoles: clippingCutouts.map((cutout) => cutout.points)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Builds the solder-mask outer boundary with only perimeter clearance.
|
|
175
|
+
* @param {any} THREE
|
|
176
|
+
* @param {{ widthMil?: number, heightMil?: number, segments?: Array<Record<string, number | string>> }} board
|
|
177
|
+
* @returns {any}
|
|
178
|
+
*/
|
|
179
|
+
static #buildInsetOuterShape(THREE, board) {
|
|
180
|
+
const baseShape = PcbScene3dBoardSolderMaskFactory.#buildBaseOuterShape(
|
|
181
|
+
THREE,
|
|
182
|
+
board
|
|
183
|
+
)
|
|
184
|
+
const scale = PcbScene3dBoardSolderMaskFactory.#resolveOuterScale(board)
|
|
185
|
+
|
|
186
|
+
if (scale.x === 1 && scale.y === 1) {
|
|
187
|
+
return baseShape
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const points = PcbScene3dBoardSolderMaskFactory.#resolveShapePoints(
|
|
191
|
+
baseShape
|
|
192
|
+
).map((point) => ({
|
|
193
|
+
x: point.x * scale.x,
|
|
194
|
+
y: point.y * scale.y
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
return PcbScene3dBoardSolderMaskFactory.#buildShapeFromPoints(
|
|
198
|
+
THREE,
|
|
199
|
+
points
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Builds the original board outline without drill apertures.
|
|
205
|
+
* @param {any} THREE
|
|
206
|
+
* @param {{ widthMil?: number, heightMil?: number, segments?: Array<Record<string, number | string>> }} board
|
|
207
|
+
* @returns {any}
|
|
208
|
+
*/
|
|
209
|
+
static #buildBaseOuterShape(THREE, board) {
|
|
210
|
+
const shape = new THREE.Shape()
|
|
211
|
+
const commands = PcbScene3dOutlineBuilder.buildCommands(board)
|
|
212
|
+
|
|
213
|
+
if (!commands.length) {
|
|
214
|
+
return PcbScene3dBoardSolderMaskFactory.#buildRectangleShape(
|
|
215
|
+
THREE,
|
|
216
|
+
board
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const command of commands) {
|
|
221
|
+
if (command.type === 'move') {
|
|
222
|
+
shape.moveTo(Number(command.x || 0), Number(command.y || 0))
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (command.type === 'arc') {
|
|
227
|
+
shape.absarc(
|
|
228
|
+
Number(command.cx || 0),
|
|
229
|
+
Number(command.cy || 0),
|
|
230
|
+
Number(command.radius || 0),
|
|
231
|
+
Number(command.startAngleRad || 0),
|
|
232
|
+
Number(command.endAngleRad || 0),
|
|
233
|
+
Boolean(command.clockwise)
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
shape.lineTo(Number(command.x || 0), Number(command.y || 0))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
shape.closePath()
|
|
242
|
+
return shape
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Builds a rectangular fallback shape.
|
|
247
|
+
* @param {any} THREE
|
|
248
|
+
* @param {{ widthMil?: number, heightMil?: number }} board
|
|
249
|
+
* @returns {any}
|
|
250
|
+
*/
|
|
251
|
+
static #buildRectangleShape(THREE, board) {
|
|
252
|
+
const halfWidth = Number(board?.widthMil || 0) / 2
|
|
253
|
+
const halfHeight = Number(board?.heightMil || 0) / 2
|
|
254
|
+
const shape = new THREE.Shape()
|
|
255
|
+
|
|
256
|
+
shape.moveTo(-halfWidth, -halfHeight)
|
|
257
|
+
shape.lineTo(halfWidth, -halfHeight)
|
|
258
|
+
shape.lineTo(halfWidth, halfHeight)
|
|
259
|
+
shape.lineTo(-halfWidth, halfHeight)
|
|
260
|
+
shape.lineTo(-halfWidth, -halfHeight)
|
|
261
|
+
shape.closePath()
|
|
262
|
+
return shape
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolves sampled points from one shape outline.
|
|
267
|
+
* @param {{ getPoints?: (segments: number) => { x: number, y: number }[] }} shape
|
|
268
|
+
* @returns {{ x: number, y: number }[]}
|
|
269
|
+
*/
|
|
270
|
+
static #resolveShapePoints(shape) {
|
|
271
|
+
return (
|
|
272
|
+
shape?.getPoints?.(
|
|
273
|
+
PcbScene3dBoardSolderMaskFactory.#OUTER_SAMPLE_POINTS
|
|
274
|
+
) || []
|
|
275
|
+
).map((point) => ({
|
|
276
|
+
x: Number(point.x || 0),
|
|
277
|
+
y: Number(point.y || 0)
|
|
278
|
+
}))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Builds one closed shape from sampled outline points.
|
|
283
|
+
* @param {any} THREE
|
|
284
|
+
* @param {{ x: number, y: number }[]} points
|
|
285
|
+
* @returns {any}
|
|
286
|
+
*/
|
|
287
|
+
static #buildShapeFromPoints(THREE, points) {
|
|
288
|
+
const shape = new THREE.Shape()
|
|
289
|
+
|
|
290
|
+
if (!points.length) {
|
|
291
|
+
return shape
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
shape.moveTo(points[0].x, points[0].y)
|
|
295
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
296
|
+
shape.lineTo(points[index].x, points[index].y)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
shape.closePath()
|
|
300
|
+
return shape
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolves normalized drill cutout polygons.
|
|
305
|
+
* @param {any} THREE
|
|
306
|
+
* @param {{ pads?: any[], vias?: any[] }} detail
|
|
307
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
308
|
+
* @returns {{ path: any, points: { x: number, y: number }[] }[]}
|
|
309
|
+
*/
|
|
310
|
+
static #resolveDrillCutouts(THREE, detail, normalizeBoardPoint) {
|
|
311
|
+
return PcbScene3dDrillPathFactory.resolveBoardDrillSpecs(detail)
|
|
312
|
+
.map((drillSpec) => {
|
|
313
|
+
const point = normalizeBoardPoint(drillSpec.x, drillSpec.y)
|
|
314
|
+
const path = PcbScene3dDrillPathFactory.buildDrillPath(THREE, {
|
|
315
|
+
...drillSpec,
|
|
316
|
+
x: point.x,
|
|
317
|
+
y: point.y
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
path,
|
|
322
|
+
points: PcbScene3dBoardSolderMaskFactory.#resolveDrillCutoutPoints(
|
|
323
|
+
{
|
|
324
|
+
...drillSpec,
|
|
325
|
+
x: point.x,
|
|
326
|
+
y: point.y
|
|
327
|
+
},
|
|
328
|
+
path
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.filter((cutout) => cutout.path && cutout.points.length >= 3)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Resolves sampled points for drill cutout classification and clipping.
|
|
337
|
+
* @param {{ x: number, y: number, diameter?: number, slotLength?: number | null }} drillSpec
|
|
338
|
+
* @param {{ getPoints?: (segments: number) => { x: number, y: number }[] } | null} path
|
|
339
|
+
* @returns {{ x: number, y: number }[]}
|
|
340
|
+
*/
|
|
341
|
+
static #resolveDrillCutoutPoints(drillSpec, path) {
|
|
342
|
+
const diameter = Number(drillSpec?.diameter || 0)
|
|
343
|
+
const slotLength = Number(drillSpec?.slotLength || 0)
|
|
344
|
+
|
|
345
|
+
if (diameter > 0 && slotLength <= diameter + 0.001) {
|
|
346
|
+
return PcbScene3dBoardSolderMaskFactory.#buildCircularCutoutPoints(
|
|
347
|
+
Number(drillSpec?.x || 0),
|
|
348
|
+
Number(drillSpec?.y || 0),
|
|
349
|
+
diameter / 2
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return PcbScene3dBoardSolderMaskFactory.#resolvePathPoints(path)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Builds uniformly sampled points for a circular drill cutout.
|
|
358
|
+
* @param {number} centerX Drill center X.
|
|
359
|
+
* @param {number} centerY Drill center Y.
|
|
360
|
+
* @param {number} radius Drill radius.
|
|
361
|
+
* @returns {{ x: number, y: number }[]}
|
|
362
|
+
*/
|
|
363
|
+
static #buildCircularCutoutPoints(centerX, centerY, radius) {
|
|
364
|
+
return Array.from(
|
|
365
|
+
{
|
|
366
|
+
length: PcbScene3dBoardSolderMaskFactory.#DRILL_SAMPLE_POINTS
|
|
367
|
+
},
|
|
368
|
+
(_, index) => {
|
|
369
|
+
const angle =
|
|
370
|
+
(Math.PI * 2 * index) /
|
|
371
|
+
PcbScene3dBoardSolderMaskFactory.#DRILL_SAMPLE_POINTS
|
|
372
|
+
return {
|
|
373
|
+
x: centerX + Math.cos(angle) * radius,
|
|
374
|
+
y: centerY + Math.sin(angle) * radius
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Resolves sampled points from one path.
|
|
382
|
+
* @param {{ getPoints?: (segments: number) => { x: number, y: number }[] } | null} path
|
|
383
|
+
* @returns {{ x: number, y: number }[]}
|
|
384
|
+
*/
|
|
385
|
+
static #resolvePathPoints(path) {
|
|
386
|
+
return (
|
|
387
|
+
path?.getPoints?.(
|
|
388
|
+
PcbScene3dBoardSolderMaskFactory.#DRILL_SAMPLE_POINTS
|
|
389
|
+
) || []
|
|
390
|
+
).map((point) => ({
|
|
391
|
+
x: Number(point.x || 0),
|
|
392
|
+
y: Number(point.y || 0)
|
|
393
|
+
}))
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Splits drill cutouts into safe shape holes and fallback clip polygons.
|
|
398
|
+
* @param {{ path: any, points: { x: number, y: number }[] }[]} drillCutouts
|
|
399
|
+
* @param {{ x: number, y: number }[]} contourPoints
|
|
400
|
+
* @returns {{ shapeHoles: { path: any, points: { x: number, y: number }[] }[], clippingCutouts: { path: any, points: { x: number, y: number }[] }[] }}
|
|
401
|
+
*/
|
|
402
|
+
static #partitionDrillCutouts(drillCutouts, contourPoints) {
|
|
403
|
+
const shapeHoles = []
|
|
404
|
+
const clippingCutouts = []
|
|
405
|
+
|
|
406
|
+
for (const cutout of Array.isArray(drillCutouts) ? drillCutouts : []) {
|
|
407
|
+
if (
|
|
408
|
+
PcbScene3dBoardSolderMaskFactory.#isHoleInsideContour(
|
|
409
|
+
cutout.points,
|
|
410
|
+
contourPoints
|
|
411
|
+
)
|
|
412
|
+
) {
|
|
413
|
+
shapeHoles.push(cutout)
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
clippingCutouts.push(cutout)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { shapeHoles, clippingCutouts }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Appends normalized cutout paths to one shape fill.
|
|
425
|
+
* @param {any} THREE
|
|
426
|
+
* @param {{ holes: any[] }} shape
|
|
427
|
+
* @param {{ path: any, points: { x: number, y: number }[] }[]} holes
|
|
428
|
+
* @returns {void}
|
|
429
|
+
*/
|
|
430
|
+
static #appendShapeHoles(THREE, shape, holes) {
|
|
431
|
+
for (const hole of Array.isArray(holes) ? holes : []) {
|
|
432
|
+
shape.holes.push(
|
|
433
|
+
hole.path ||
|
|
434
|
+
PcbScene3dBoardSolderMaskFactory.#buildPathFromPoints(
|
|
435
|
+
THREE,
|
|
436
|
+
hole.points
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Builds one closed path from sampled points.
|
|
444
|
+
* @param {any} THREE
|
|
445
|
+
* @param {{ x: number, y: number }[]} points
|
|
446
|
+
* @returns {any}
|
|
447
|
+
*/
|
|
448
|
+
static #buildPathFromPoints(THREE, points) {
|
|
449
|
+
const path = new THREE.Path()
|
|
450
|
+
|
|
451
|
+
if (!points.length) {
|
|
452
|
+
return path
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
path.moveTo(points[0].x, points[0].y)
|
|
456
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
457
|
+
path.lineTo(points[index].x, points[index].y)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
path.closePath()
|
|
461
|
+
return path
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Returns true when a cutout can safely be added as a shape hole.
|
|
466
|
+
* @param {{ x: number, y: number }[]} hole
|
|
467
|
+
* @param {{ x: number, y: number }[]} contour
|
|
468
|
+
* @returns {boolean}
|
|
469
|
+
*/
|
|
470
|
+
static #isHoleInsideContour(hole, contour) {
|
|
471
|
+
return (
|
|
472
|
+
Array.isArray(hole) &&
|
|
473
|
+
Array.isArray(contour) &&
|
|
474
|
+
hole.length >= 3 &&
|
|
475
|
+
contour.length >= 3 &&
|
|
476
|
+
hole.every((point) =>
|
|
477
|
+
PcbScene3dBoardSolderMaskFactory.#isPointStrictlyInsidePolygon(
|
|
478
|
+
point,
|
|
479
|
+
contour
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Returns true when a point lies inside a polygon and away from its border.
|
|
487
|
+
* @param {{ x: number, y: number }} point
|
|
488
|
+
* @param {{ x: number, y: number }[]} polygon
|
|
489
|
+
* @returns {boolean}
|
|
490
|
+
*/
|
|
491
|
+
static #isPointStrictlyInsidePolygon(point, polygon) {
|
|
492
|
+
if (
|
|
493
|
+
PcbScene3dBoardSolderMaskFactory.#isPointOnPolygonBoundary(
|
|
494
|
+
point,
|
|
495
|
+
polygon
|
|
496
|
+
)
|
|
497
|
+
) {
|
|
498
|
+
return false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let inside = false
|
|
502
|
+
for (
|
|
503
|
+
let index = 0, previousIndex = polygon.length - 1;
|
|
504
|
+
index < polygon.length;
|
|
505
|
+
previousIndex = index, index += 1
|
|
506
|
+
) {
|
|
507
|
+
const current = polygon[index]
|
|
508
|
+
const previous = polygon[previousIndex]
|
|
509
|
+
const intersects =
|
|
510
|
+
current.y > point.y !== previous.y > point.y &&
|
|
511
|
+
point.x <
|
|
512
|
+
((previous.x - current.x) * (point.y - current.y)) /
|
|
513
|
+
(previous.y - current.y) +
|
|
514
|
+
current.x
|
|
515
|
+
|
|
516
|
+
if (intersects) {
|
|
517
|
+
inside = !inside
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return inside
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Returns true when a point lies on any polygon edge.
|
|
526
|
+
* @param {{ x: number, y: number }} point
|
|
527
|
+
* @param {{ x: number, y: number }[]} polygon
|
|
528
|
+
* @returns {boolean}
|
|
529
|
+
*/
|
|
530
|
+
static #isPointOnPolygonBoundary(point, polygon) {
|
|
531
|
+
return polygon.some((start, index) =>
|
|
532
|
+
PcbScene3dBoardSolderMaskFactory.#isPointOnSegment(
|
|
533
|
+
point,
|
|
534
|
+
start,
|
|
535
|
+
polygon[(index + 1) % polygon.length]
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Returns true when a point lies on one line segment.
|
|
542
|
+
* @param {{ x: number, y: number }} point
|
|
543
|
+
* @param {{ x: number, y: number }} start
|
|
544
|
+
* @param {{ x: number, y: number }} end
|
|
545
|
+
* @returns {boolean}
|
|
546
|
+
*/
|
|
547
|
+
static #isPointOnSegment(point, start, end) {
|
|
548
|
+
const lengthSquared =
|
|
549
|
+
(end.x - start.x) * (end.x - start.x) +
|
|
550
|
+
(end.y - start.y) * (end.y - start.y)
|
|
551
|
+
|
|
552
|
+
if (lengthSquared < 0.001) {
|
|
553
|
+
return Math.hypot(point.x - start.x, point.y - start.y) < 0.001
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const cross =
|
|
557
|
+
(point.y - start.y) * (end.x - start.x) -
|
|
558
|
+
(point.x - start.x) * (end.y - start.y)
|
|
559
|
+
if (Math.abs(cross) > 0.001) {
|
|
560
|
+
return false
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const dot =
|
|
564
|
+
(point.x - start.x) * (end.x - start.x) +
|
|
565
|
+
(point.y - start.y) * (end.y - start.y)
|
|
566
|
+
if (dot < -0.001) {
|
|
567
|
+
return false
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return dot <= lengthSquared + 0.001
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Resolves the mask outer-boundary scale for edge clearance.
|
|
575
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board dimensions.
|
|
576
|
+
* @returns {{ x: number, y: number }}
|
|
577
|
+
*/
|
|
578
|
+
static #resolveOuterScale(board) {
|
|
579
|
+
const width = Number(board?.widthMil || 0)
|
|
580
|
+
const height = Number(board?.heightMil || 0)
|
|
581
|
+
|
|
582
|
+
if (width <= 0 || height <= 0) {
|
|
583
|
+
return { x: 1, y: 1 }
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const clearance = PcbScene3dBoardSolderMaskFactory.#EDGE_CLEARANCE_MIL
|
|
587
|
+
return {
|
|
588
|
+
x: Math.max((width - 2 * clearance) / width, 0.9),
|
|
589
|
+
y: Math.max((height - 2 * clearance) / height, 0.9)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Builds the solder-mask material.
|
|
595
|
+
* @param {any} THREE
|
|
596
|
+
* @param {{ surfaceColor?: number }} board Board metadata.
|
|
597
|
+
* @param {number} side Rendered material side.
|
|
598
|
+
* @returns {any}
|
|
599
|
+
*/
|
|
600
|
+
static #buildMaterial(THREE, board, side) {
|
|
601
|
+
return new THREE.MeshStandardMaterial({
|
|
602
|
+
color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
|
|
603
|
+
hasBoardAssemblyModel: true
|
|
604
|
+
}),
|
|
605
|
+
roughness: 0.68,
|
|
606
|
+
metalness: 0.08,
|
|
607
|
+
polygonOffset: true,
|
|
608
|
+
polygonOffsetFactor: -1,
|
|
609
|
+
polygonOffsetUnits: -1,
|
|
610
|
+
side
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
}
|