pcb-scene3d-viewer 1.1.2 → 1.1.11
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 +1 -1
- package/src/PcbScene3dArcUtils.mjs +18 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +3 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +2 -2
- package/src/PcbScene3dCameraRig.mjs +16 -1
- package/src/PcbScene3dCopperDetailFilter.mjs +73 -1
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +482 -0
- package/src/PcbScene3dCopperFactory.mjs +499 -85
- package/src/PcbScene3dCopperLayerFilter.mjs +48 -0
- package/src/PcbScene3dCopperOcclusionClipper.mjs +86 -0
- package/src/PcbScene3dCopperTextFactory.mjs +26 -6
- package/src/PcbScene3dCutoutGeometryFilter.mjs +25 -28
- package/src/PcbScene3dDrillCutoutFilter.mjs +123 -0
- package/src/PcbScene3dGeometryFaceRefiner.mjs +218 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +48 -0
- package/src/PcbScene3dMaskCoveredCopperMaterial.mjs +72 -0
- package/src/PcbScene3dMaterialFinish.mjs +49 -0
- package/src/PcbScene3dPadFactory.mjs +160 -3
- package/src/PcbScene3dPadSurfaceStack.mjs +155 -0
- package/src/PcbScene3dPolygonOverlap.mjs +283 -0
- package/src/PcbScene3dRuntime.mjs +11 -64
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +8 -3
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +212 -0
- package/src/PcbScene3dShapeHoleMerger.mjs +476 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +193 -126
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +241 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +7 -6
- package/src/PcbScene3dTerminalCutoutClassifier.mjs +92 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +16 -4
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
|
|
2
2
|
import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
|
|
3
3
|
import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
|
|
4
|
+
import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
|
|
5
|
+
import { PcbScene3dGeometryZCompressor } from './PcbScene3dGeometryZCompressor.mjs'
|
|
6
|
+
import { PcbScene3dCopperOcclusionClipper } from './PcbScene3dCopperOcclusionClipper.mjs'
|
|
7
|
+
import { PcbScene3dCopperLayerFilter } from './PcbScene3dCopperLayerFilter.mjs'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Builds copper-detail meshes for the interactive 3D PCB scene.
|
|
7
11
|
*/
|
|
8
12
|
export class PcbScene3dCopperFactory {
|
|
9
|
-
static #
|
|
10
|
-
static #BOTTOM_COPPER_LAYER_ID = 32
|
|
13
|
+
static #ARC_SEGMENT_DEGREES = 3
|
|
11
14
|
static #FULL_CIRCLE_EPSILON = 0.001
|
|
12
15
|
static #ROUND_CAP_SEGMENTS = 16
|
|
16
|
+
static #COPPER_THICKNESS_MIL = 2.2
|
|
17
|
+
static #COPPER_COLOR = 0xd9a61d
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns half the visual copper extrusion thickness.
|
|
21
|
+
* @returns {number}
|
|
22
|
+
*/
|
|
23
|
+
static visualHalfThicknessMil() {
|
|
24
|
+
return PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
|
|
25
|
+
}
|
|
13
26
|
|
|
14
27
|
/**
|
|
15
28
|
* Builds the combined top and bottom copper group.
|
|
@@ -33,11 +46,11 @@ export class PcbScene3dCopperFactory {
|
|
|
33
46
|
const topGroup = PcbScene3dCopperFactory.#buildSideGroup(
|
|
34
47
|
THREE,
|
|
35
48
|
{
|
|
36
|
-
tracks:
|
|
49
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
37
50
|
detail?.tracks,
|
|
38
51
|
'top'
|
|
39
52
|
),
|
|
40
|
-
arcs:
|
|
53
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top'),
|
|
41
54
|
pads: detail?.pads || [],
|
|
42
55
|
vias: detail?.vias || [],
|
|
43
56
|
copperTexts: detail?.copperTexts || []
|
|
@@ -50,14 +63,11 @@ export class PcbScene3dCopperFactory {
|
|
|
50
63
|
const bottomGroup = PcbScene3dCopperFactory.#buildSideGroup(
|
|
51
64
|
THREE,
|
|
52
65
|
{
|
|
53
|
-
tracks:
|
|
66
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
54
67
|
detail?.tracks,
|
|
55
68
|
'bottom'
|
|
56
69
|
),
|
|
57
|
-
arcs:
|
|
58
|
-
detail?.arcs,
|
|
59
|
-
'bottom'
|
|
60
|
-
),
|
|
70
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom'),
|
|
61
71
|
pads: detail?.pads || [],
|
|
62
72
|
vias: detail?.vias || [],
|
|
63
73
|
copperTexts: detail?.copperTexts || []
|
|
@@ -78,6 +88,78 @@ export class PcbScene3dCopperFactory {
|
|
|
78
88
|
return group
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Builds top and bottom traces that are covered by solder mask.
|
|
93
|
+
* @param {any} THREE
|
|
94
|
+
* @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
|
|
95
|
+
* @param {number} topZ
|
|
96
|
+
* @param {number} bottomZ
|
|
97
|
+
* @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]
|
|
99
|
+
* @returns {any}
|
|
100
|
+
*/
|
|
101
|
+
static buildMaskCoveredGroup(
|
|
102
|
+
THREE,
|
|
103
|
+
detail,
|
|
104
|
+
topZ,
|
|
105
|
+
bottomZ,
|
|
106
|
+
normalizeBoardPoint,
|
|
107
|
+
options = {}
|
|
108
|
+
) {
|
|
109
|
+
const group = new THREE.Group()
|
|
110
|
+
const material = PcbScene3dMaskCoveredCopperMaterial.build(
|
|
111
|
+
THREE,
|
|
112
|
+
options
|
|
113
|
+
)
|
|
114
|
+
const topGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
|
|
115
|
+
THREE,
|
|
116
|
+
{
|
|
117
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
118
|
+
detail?.tracks,
|
|
119
|
+
'top'
|
|
120
|
+
),
|
|
121
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'top')
|
|
122
|
+
},
|
|
123
|
+
Math.abs(Number(topZ || 0)),
|
|
124
|
+
normalizeBoardPoint,
|
|
125
|
+
false,
|
|
126
|
+
material,
|
|
127
|
+
PcbScene3dCopperOcclusionClipper.normalizeCutouts(
|
|
128
|
+
options?.occlusionCutouts?.top,
|
|
129
|
+
normalizeBoardPoint,
|
|
130
|
+
false
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
const bottomGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
|
|
134
|
+
THREE,
|
|
135
|
+
{
|
|
136
|
+
tracks: PcbScene3dCopperLayerFilter.tracks(
|
|
137
|
+
detail?.tracks,
|
|
138
|
+
'bottom'
|
|
139
|
+
),
|
|
140
|
+
arcs: PcbScene3dCopperLayerFilter.arcs(detail?.arcs, 'bottom')
|
|
141
|
+
},
|
|
142
|
+
Math.abs(Number(bottomZ || 0)),
|
|
143
|
+
normalizeBoardPoint,
|
|
144
|
+
true,
|
|
145
|
+
material,
|
|
146
|
+
PcbScene3dCopperOcclusionClipper.normalizeCutouts(
|
|
147
|
+
options?.occlusionCutouts?.bottom,
|
|
148
|
+
normalizeBoardPoint,
|
|
149
|
+
true
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if (topGroup.children.length) {
|
|
154
|
+
group.add(topGroup)
|
|
155
|
+
}
|
|
156
|
+
if (bottomGroup.children.length) {
|
|
157
|
+
group.add(bottomGroup)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return group
|
|
161
|
+
}
|
|
162
|
+
|
|
81
163
|
/**
|
|
82
164
|
* Builds one side-specific copper group.
|
|
83
165
|
* @param {any} THREE
|
|
@@ -152,6 +234,69 @@ export class PcbScene3dCopperFactory {
|
|
|
152
234
|
return group
|
|
153
235
|
}
|
|
154
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Builds one side of the mask-covered trace relief.
|
|
239
|
+
* @param {any} THREE
|
|
240
|
+
* @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
|
|
241
|
+
* @param {number} z
|
|
242
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
243
|
+
* @param {boolean} mirrorY
|
|
244
|
+
* @param {any} material Shared covered-trace material.
|
|
245
|
+
* @param {{ x: number, y: number }[][]} occlusionCutouts Silkscreen ink polygons covering this copper.
|
|
246
|
+
* @returns {any}
|
|
247
|
+
*/
|
|
248
|
+
static #buildMaskCoveredSideGroup(
|
|
249
|
+
THREE,
|
|
250
|
+
detail,
|
|
251
|
+
z,
|
|
252
|
+
normalizeBoardPoint,
|
|
253
|
+
mirrorY,
|
|
254
|
+
material,
|
|
255
|
+
occlusionCutouts
|
|
256
|
+
) {
|
|
257
|
+
const group = new THREE.Group()
|
|
258
|
+
const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
|
|
259
|
+
THREE,
|
|
260
|
+
detail?.tracks || [],
|
|
261
|
+
z,
|
|
262
|
+
normalizeBoardPoint,
|
|
263
|
+
mirrorY,
|
|
264
|
+
material,
|
|
265
|
+
occlusionCutouts
|
|
266
|
+
)
|
|
267
|
+
const arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
|
|
268
|
+
THREE,
|
|
269
|
+
detail?.arcs || [],
|
|
270
|
+
z,
|
|
271
|
+
normalizeBoardPoint,
|
|
272
|
+
mirrorY,
|
|
273
|
+
material,
|
|
274
|
+
occlusionCutouts
|
|
275
|
+
)
|
|
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
|
|
298
|
+
}
|
|
299
|
+
|
|
155
300
|
/**
|
|
156
301
|
* Checks whether copper text glyph strokes are already in y-up scene space.
|
|
157
302
|
* @param {{ coordinateSystem?: string } | undefined} options
|
|
@@ -164,13 +309,23 @@ export class PcbScene3dCopperFactory {
|
|
|
164
309
|
/**
|
|
165
310
|
* Builds one widened copper-track mesh for one face.
|
|
166
311
|
* @param {any} THREE
|
|
167
|
-
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number }[]} tracks
|
|
312
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number, capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }[]} tracks
|
|
168
313
|
* @param {number} z
|
|
169
314
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
170
315
|
* @param {boolean} mirrorY
|
|
316
|
+
* @param {any | null} [material] Optional material override.
|
|
317
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
171
318
|
* @returns {any | null}
|
|
172
319
|
*/
|
|
173
|
-
static #buildTrackMesh(
|
|
320
|
+
static #buildTrackMesh(
|
|
321
|
+
THREE,
|
|
322
|
+
tracks,
|
|
323
|
+
z,
|
|
324
|
+
normalizeBoardPoint,
|
|
325
|
+
mirrorY,
|
|
326
|
+
material = null,
|
|
327
|
+
cutouts = []
|
|
328
|
+
) {
|
|
174
329
|
const positions = []
|
|
175
330
|
|
|
176
331
|
for (const track of tracks) {
|
|
@@ -191,11 +346,17 @@ export class PcbScene3dCopperFactory {
|
|
|
191
346
|
start,
|
|
192
347
|
end,
|
|
193
348
|
Number(track?.width || 0),
|
|
194
|
-
z
|
|
349
|
+
z,
|
|
350
|
+
track
|
|
195
351
|
)
|
|
196
352
|
}
|
|
197
353
|
|
|
198
|
-
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
354
|
+
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
355
|
+
THREE,
|
|
356
|
+
positions,
|
|
357
|
+
material,
|
|
358
|
+
cutouts
|
|
359
|
+
)
|
|
199
360
|
}
|
|
200
361
|
|
|
201
362
|
/**
|
|
@@ -205,9 +366,19 @@ export class PcbScene3dCopperFactory {
|
|
|
205
366
|
* @param {number} z
|
|
206
367
|
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
207
368
|
* @param {boolean} mirrorY
|
|
369
|
+
* @param {any | null} [material] Optional material override.
|
|
370
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
208
371
|
* @returns {any | null}
|
|
209
372
|
*/
|
|
210
|
-
static #buildArcMesh(
|
|
373
|
+
static #buildArcMesh(
|
|
374
|
+
THREE,
|
|
375
|
+
arcs,
|
|
376
|
+
z,
|
|
377
|
+
normalizeBoardPoint,
|
|
378
|
+
mirrorY,
|
|
379
|
+
material = null,
|
|
380
|
+
cutouts = []
|
|
381
|
+
) {
|
|
211
382
|
const positions = []
|
|
212
383
|
|
|
213
384
|
for (const arc of arcs) {
|
|
@@ -226,16 +397,23 @@ export class PcbScene3dCopperFactory {
|
|
|
226
397
|
)
|
|
227
398
|
}
|
|
228
399
|
|
|
229
|
-
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
400
|
+
return PcbScene3dCopperFactory.#buildStrokeMesh(
|
|
401
|
+
THREE,
|
|
402
|
+
positions,
|
|
403
|
+
material,
|
|
404
|
+
cutouts
|
|
405
|
+
)
|
|
230
406
|
}
|
|
231
407
|
|
|
232
408
|
/**
|
|
233
409
|
* Builds one copper stroke mesh from triangle positions.
|
|
234
410
|
* @param {any} THREE
|
|
235
411
|
* @param {number[]} positions
|
|
412
|
+
* @param {any | null} [material] Optional material override.
|
|
413
|
+
* @param {{ x: number, y: number }[][]} [cutouts] Optional geometry cutouts.
|
|
236
414
|
* @returns {any | null}
|
|
237
415
|
*/
|
|
238
|
-
static #buildStrokeMesh(THREE, positions) {
|
|
416
|
+
static #buildStrokeMesh(THREE, positions, material = null, cutouts = []) {
|
|
239
417
|
if (!positions.length) {
|
|
240
418
|
return null
|
|
241
419
|
}
|
|
@@ -245,11 +423,19 @@ export class PcbScene3dCopperFactory {
|
|
|
245
423
|
'position',
|
|
246
424
|
new THREE.Float32BufferAttribute(positions, 3)
|
|
247
425
|
)
|
|
248
|
-
|
|
426
|
+
const clippedGeometry = PcbScene3dCopperOcclusionClipper.filter(
|
|
427
|
+
THREE,
|
|
428
|
+
geometry,
|
|
429
|
+
cutouts
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if (!clippedGeometry) {
|
|
433
|
+
return null
|
|
434
|
+
}
|
|
249
435
|
|
|
250
436
|
return new THREE.Mesh(
|
|
251
|
-
|
|
252
|
-
PcbScene3dCopperFactory.#buildMaterial(THREE)
|
|
437
|
+
clippedGeometry,
|
|
438
|
+
material || PcbScene3dCopperFactory.#buildMaterial(THREE)
|
|
253
439
|
)
|
|
254
440
|
}
|
|
255
441
|
|
|
@@ -260,7 +446,7 @@ export class PcbScene3dCopperFactory {
|
|
|
260
446
|
*/
|
|
261
447
|
static #buildMaterial(THREE) {
|
|
262
448
|
return new THREE.MeshStandardMaterial({
|
|
263
|
-
color:
|
|
449
|
+
color: PcbScene3dCopperFactory.#COPPER_COLOR,
|
|
264
450
|
roughness: 0.38,
|
|
265
451
|
metalness: 0.55,
|
|
266
452
|
side: THREE.DoubleSide,
|
|
@@ -270,47 +456,6 @@ export class PcbScene3dCopperFactory {
|
|
|
270
456
|
})
|
|
271
457
|
}
|
|
272
458
|
|
|
273
|
-
/**
|
|
274
|
-
* Filters one track list to one outer copper face.
|
|
275
|
-
* @param {any[] | undefined} tracks
|
|
276
|
-
* @param {'top' | 'bottom'} side
|
|
277
|
-
* @returns {any[]}
|
|
278
|
-
*/
|
|
279
|
-
static #filterTracks(tracks, side) {
|
|
280
|
-
return (tracks || []).filter((track) =>
|
|
281
|
-
PcbScene3dCopperFactory.#matchesCopperLayer(track, side)
|
|
282
|
-
)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Filters one arc list to one outer copper face.
|
|
287
|
-
* @param {any[] | undefined} arcs
|
|
288
|
-
* @param {'top' | 'bottom'} side
|
|
289
|
-
* @returns {any[]}
|
|
290
|
-
*/
|
|
291
|
-
static #filterArcs(arcs, side) {
|
|
292
|
-
return (arcs || []).filter((arc) =>
|
|
293
|
-
PcbScene3dCopperFactory.#matchesCopperLayer(arc, side)
|
|
294
|
-
)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Returns true when one primitive belongs to the requested outer copper
|
|
299
|
-
* face.
|
|
300
|
-
* @param {{ layerId?: number, layerCode?: number }} primitive
|
|
301
|
-
* @param {'top' | 'bottom'} side
|
|
302
|
-
* @returns {boolean}
|
|
303
|
-
*/
|
|
304
|
-
static #matchesCopperLayer(primitive, side) {
|
|
305
|
-
const layerId = Number(
|
|
306
|
-
primitive?.layerId ?? primitive?.layerCode ?? NaN
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
return side === 'bottom'
|
|
310
|
-
? layerId === PcbScene3dCopperFactory.#BOTTOM_COPPER_LAYER_ID
|
|
311
|
-
: layerId === PcbScene3dCopperFactory.#TOP_COPPER_LAYER_ID
|
|
312
|
-
}
|
|
313
|
-
|
|
314
459
|
/**
|
|
315
460
|
* Appends one widened track quad as two triangles.
|
|
316
461
|
* @param {number[]} positions
|
|
@@ -318,9 +463,17 @@ export class PcbScene3dCopperFactory {
|
|
|
318
463
|
* @param {{ x: number, y: number }} end
|
|
319
464
|
* @param {number} width
|
|
320
465
|
* @param {number} z
|
|
466
|
+
* @param {{ capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }} [options]
|
|
321
467
|
* @returns {void}
|
|
322
468
|
*/
|
|
323
|
-
static #appendTrackTriangles(
|
|
469
|
+
static #appendTrackTriangles(
|
|
470
|
+
positions,
|
|
471
|
+
start,
|
|
472
|
+
end,
|
|
473
|
+
width,
|
|
474
|
+
z,
|
|
475
|
+
options = {}
|
|
476
|
+
) {
|
|
324
477
|
const dx = end.x - start.x
|
|
325
478
|
const dy = end.y - start.y
|
|
326
479
|
const length = Math.hypot(dx, dy)
|
|
@@ -352,25 +505,47 @@ export class PcbScene3dCopperFactory {
|
|
|
352
505
|
{ x: start.x - normalX, y: start.y - normalY },
|
|
353
506
|
z
|
|
354
507
|
)
|
|
355
|
-
PcbScene3dCopperFactory.#
|
|
508
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
356
509
|
positions,
|
|
357
|
-
start,
|
|
358
|
-
|
|
510
|
+
{ x: start.x + normalX, y: start.y + normalY },
|
|
511
|
+
{ x: end.x + normalX, y: end.y + normalY },
|
|
359
512
|
z
|
|
360
513
|
)
|
|
361
|
-
PcbScene3dCopperFactory.#
|
|
514
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
362
515
|
positions,
|
|
363
|
-
end,
|
|
364
|
-
|
|
516
|
+
{ x: end.x - normalX, y: end.y - normalY },
|
|
517
|
+
{ x: start.x - normalX, y: start.y - normalY },
|
|
365
518
|
z
|
|
366
519
|
)
|
|
520
|
+
if (options?.capStartRound !== false) {
|
|
521
|
+
PcbScene3dCopperFactory.#appendRoundCapTriangles(
|
|
522
|
+
positions,
|
|
523
|
+
start,
|
|
524
|
+
halfWidth,
|
|
525
|
+
z,
|
|
526
|
+
-dx / length,
|
|
527
|
+
-dy / length,
|
|
528
|
+
options?.capStartSideWall !== false
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
if (options?.capEndRound !== false) {
|
|
532
|
+
PcbScene3dCopperFactory.#appendRoundCapTriangles(
|
|
533
|
+
positions,
|
|
534
|
+
end,
|
|
535
|
+
halfWidth,
|
|
536
|
+
z,
|
|
537
|
+
dx / length,
|
|
538
|
+
dy / length,
|
|
539
|
+
options?.capEndSideWall !== false
|
|
540
|
+
)
|
|
541
|
+
}
|
|
367
542
|
}
|
|
368
543
|
|
|
369
544
|
/**
|
|
370
545
|
* Appends one widened arc band as triangles.
|
|
371
546
|
* @param {number[]} positions
|
|
372
547
|
* @param {{ x: number, y: number }} center
|
|
373
|
-
* @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number }} arc
|
|
548
|
+
* @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number, sweepAngle?: number }} arc
|
|
374
549
|
* @param {number} z
|
|
375
550
|
* @param {boolean} mirrorY
|
|
376
551
|
* @returns {void}
|
|
@@ -381,10 +556,7 @@ export class PcbScene3dCopperFactory {
|
|
|
381
556
|
const outerRadius = radius + strokeWidth / 2
|
|
382
557
|
const innerRadius = Math.max(radius - strokeWidth / 2, 0)
|
|
383
558
|
const startAngleRad = (Number(arc?.startAngle || 0) * Math.PI) / 180
|
|
384
|
-
const deltaAngleDeg = PcbScene3dArcUtils.
|
|
385
|
-
Number(arc?.startAngle || 0),
|
|
386
|
-
Number(arc?.endAngle || 0)
|
|
387
|
-
)
|
|
559
|
+
const deltaAngleDeg = PcbScene3dArcUtils.resolveArcSweepDelta(arc)
|
|
388
560
|
const isFullCircle =
|
|
389
561
|
Math.abs(deltaAngleDeg) <=
|
|
390
562
|
PcbScene3dCopperFactory.#FULL_CIRCLE_EPSILON ||
|
|
@@ -395,7 +567,10 @@ export class PcbScene3dCopperFactory {
|
|
|
395
567
|
: (deltaAngleDeg * Math.PI) / 180
|
|
396
568
|
const segments = Math.max(
|
|
397
569
|
isFullCircle ? 20 : 8,
|
|
398
|
-
Math.ceil(
|
|
570
|
+
Math.ceil(
|
|
571
|
+
Math.abs(deltaAngleDeg) /
|
|
572
|
+
PcbScene3dCopperFactory.#ARC_SEGMENT_DEGREES
|
|
573
|
+
)
|
|
399
574
|
)
|
|
400
575
|
const yDirection = mirrorY ? -1 : 1
|
|
401
576
|
|
|
@@ -421,6 +596,12 @@ export class PcbScene3dCopperFactory {
|
|
|
421
596
|
outerEnd,
|
|
422
597
|
z
|
|
423
598
|
)
|
|
599
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
600
|
+
positions,
|
|
601
|
+
outerStart,
|
|
602
|
+
outerEnd,
|
|
603
|
+
z
|
|
604
|
+
)
|
|
424
605
|
continue
|
|
425
606
|
}
|
|
426
607
|
|
|
@@ -441,19 +622,45 @@ export class PcbScene3dCopperFactory {
|
|
|
441
622
|
innerStart,
|
|
442
623
|
z
|
|
443
624
|
)
|
|
625
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
626
|
+
positions,
|
|
627
|
+
outerStart,
|
|
628
|
+
outerEnd,
|
|
629
|
+
z
|
|
630
|
+
)
|
|
631
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
632
|
+
positions,
|
|
633
|
+
innerEnd,
|
|
634
|
+
innerStart,
|
|
635
|
+
z
|
|
636
|
+
)
|
|
444
637
|
}
|
|
445
638
|
|
|
446
639
|
if (!isFullCircle) {
|
|
447
|
-
|
|
640
|
+
const sweepDirection = deltaAngleRad < 0 ? -1 : 1
|
|
641
|
+
const startTangent = PcbScene3dCopperFactory.#resolveArcTangent(
|
|
642
|
+
startAngleRad,
|
|
643
|
+
yDirection,
|
|
644
|
+
sweepDirection
|
|
645
|
+
)
|
|
646
|
+
const endTangent = PcbScene3dCopperFactory.#resolveArcTangent(
|
|
647
|
+
startAngleRad + deltaAngleRad,
|
|
648
|
+
yDirection,
|
|
649
|
+
sweepDirection
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
PcbScene3dCopperFactory.#appendRoundCapTriangles(
|
|
448
653
|
positions,
|
|
449
654
|
{
|
|
450
655
|
x: center.x + Math.cos(startAngleRad) * radius,
|
|
451
656
|
y: center.y + Math.sin(startAngleRad) * radius * yDirection
|
|
452
657
|
},
|
|
453
658
|
strokeWidth / 2,
|
|
454
|
-
z
|
|
659
|
+
z,
|
|
660
|
+
-startTangent.x,
|
|
661
|
+
-startTangent.y
|
|
455
662
|
)
|
|
456
|
-
PcbScene3dCopperFactory.#
|
|
663
|
+
PcbScene3dCopperFactory.#appendRoundCapTriangles(
|
|
457
664
|
positions,
|
|
458
665
|
{
|
|
459
666
|
x:
|
|
@@ -466,7 +673,9 @@ export class PcbScene3dCopperFactory {
|
|
|
466
673
|
yDirection
|
|
467
674
|
},
|
|
468
675
|
strokeWidth / 2,
|
|
469
|
-
z
|
|
676
|
+
z,
|
|
677
|
+
endTangent.x,
|
|
678
|
+
endTangent.y
|
|
470
679
|
)
|
|
471
680
|
}
|
|
472
681
|
}
|
|
@@ -525,6 +734,153 @@ export class PcbScene3dCopperFactory {
|
|
|
525
734
|
},
|
|
526
735
|
z
|
|
527
736
|
)
|
|
737
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
738
|
+
positions,
|
|
739
|
+
{
|
|
740
|
+
x: center.x + Math.cos(startAngle) * safeRadius,
|
|
741
|
+
y: center.y + Math.sin(startAngle) * safeRadius
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
x: center.x + Math.cos(endAngle) * safeRadius,
|
|
745
|
+
y: center.y + Math.sin(endAngle) * safeRadius
|
|
746
|
+
},
|
|
747
|
+
z
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Appends only the exposed half of one rounded stroke endpoint.
|
|
754
|
+
* @param {number[]} positions Position buffer.
|
|
755
|
+
* @param {{ x: number, y: number }} center Cap center.
|
|
756
|
+
* @param {number} radius Cap radius.
|
|
757
|
+
* @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.
|
|
761
|
+
* @returns {void}
|
|
762
|
+
*/
|
|
763
|
+
static #appendRoundCapTriangles(
|
|
764
|
+
positions,
|
|
765
|
+
center,
|
|
766
|
+
radius,
|
|
767
|
+
z,
|
|
768
|
+
outwardX,
|
|
769
|
+
outwardY,
|
|
770
|
+
includeSideWall = true
|
|
771
|
+
) {
|
|
772
|
+
const safeRadius = Math.max(Number(radius || 0), 0)
|
|
773
|
+
const outwardLength = Math.hypot(outwardX, outwardY)
|
|
774
|
+
if (safeRadius <= 0) {
|
|
775
|
+
return
|
|
776
|
+
}
|
|
777
|
+
if (outwardLength <= 0.001) {
|
|
778
|
+
PcbScene3dCopperFactory.#appendDiscTriangles(
|
|
779
|
+
positions,
|
|
780
|
+
center,
|
|
781
|
+
safeRadius,
|
|
782
|
+
z
|
|
783
|
+
)
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const unitX = outwardX / outwardLength
|
|
788
|
+
const unitY = outwardY / outwardLength
|
|
789
|
+
const normalX = -unitY
|
|
790
|
+
const normalY = unitX
|
|
791
|
+
|
|
792
|
+
for (
|
|
793
|
+
let index = 0;
|
|
794
|
+
index < PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS;
|
|
795
|
+
index += 1
|
|
796
|
+
) {
|
|
797
|
+
const startAngle =
|
|
798
|
+
-Math.PI / 2 +
|
|
799
|
+
(Math.PI * index) / PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
|
|
800
|
+
const endAngle =
|
|
801
|
+
-Math.PI / 2 +
|
|
802
|
+
(Math.PI * (index + 1)) /
|
|
803
|
+
PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
|
|
804
|
+
const start = PcbScene3dCopperFactory.#resolveCapPoint(
|
|
805
|
+
center,
|
|
806
|
+
safeRadius,
|
|
807
|
+
unitX,
|
|
808
|
+
unitY,
|
|
809
|
+
normalX,
|
|
810
|
+
normalY,
|
|
811
|
+
startAngle
|
|
812
|
+
)
|
|
813
|
+
const end = PcbScene3dCopperFactory.#resolveCapPoint(
|
|
814
|
+
center,
|
|
815
|
+
safeRadius,
|
|
816
|
+
unitX,
|
|
817
|
+
unitY,
|
|
818
|
+
normalX,
|
|
819
|
+
normalY,
|
|
820
|
+
endAngle
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
PcbScene3dCopperFactory.#appendTriangle(
|
|
824
|
+
positions,
|
|
825
|
+
center,
|
|
826
|
+
start,
|
|
827
|
+
end,
|
|
828
|
+
z
|
|
829
|
+
)
|
|
830
|
+
if (includeSideWall) {
|
|
831
|
+
PcbScene3dCopperFactory.#appendBoundarySideTriangles(
|
|
832
|
+
positions,
|
|
833
|
+
start,
|
|
834
|
+
end,
|
|
835
|
+
z
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Resolves one point on an oriented round cap boundary.
|
|
843
|
+
* @param {{ x: number, y: number }} center Cap center.
|
|
844
|
+
* @param {number} radius Cap radius.
|
|
845
|
+
* @param {number} unitX Outward X direction.
|
|
846
|
+
* @param {number} unitY Outward Y direction.
|
|
847
|
+
* @param {number} normalX Cap normal X direction.
|
|
848
|
+
* @param {number} normalY Cap normal Y direction.
|
|
849
|
+
* @param {number} angle Local cap angle.
|
|
850
|
+
* @returns {{ x: number, y: number }}
|
|
851
|
+
*/
|
|
852
|
+
static #resolveCapPoint(
|
|
853
|
+
center,
|
|
854
|
+
radius,
|
|
855
|
+
unitX,
|
|
856
|
+
unitY,
|
|
857
|
+
normalX,
|
|
858
|
+
normalY,
|
|
859
|
+
angle
|
|
860
|
+
) {
|
|
861
|
+
return {
|
|
862
|
+
x:
|
|
863
|
+
center.x +
|
|
864
|
+
unitX * Math.cos(angle) * radius +
|
|
865
|
+
normalX * Math.sin(angle) * radius,
|
|
866
|
+
y:
|
|
867
|
+
center.y +
|
|
868
|
+
unitY * Math.cos(angle) * radius +
|
|
869
|
+
normalY * Math.sin(angle) * radius
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Resolves one normalized centerline tangent for an arc endpoint.
|
|
875
|
+
* @param {number} angle Endpoint angle in radians.
|
|
876
|
+
* @param {number} yDirection Mirrored Y direction.
|
|
877
|
+
* @param {number} sweepDirection Arc sweep direction.
|
|
878
|
+
* @returns {{ x: number, y: number }}
|
|
879
|
+
*/
|
|
880
|
+
static #resolveArcTangent(angle, yDirection, sweepDirection) {
|
|
881
|
+
return {
|
|
882
|
+
x: -Math.sin(angle) * sweepDirection,
|
|
883
|
+
y: Math.cos(angle) * yDirection * sweepDirection
|
|
528
884
|
}
|
|
529
885
|
}
|
|
530
886
|
|
|
@@ -548,15 +904,73 @@ export class PcbScene3dCopperFactory {
|
|
|
548
904
|
}
|
|
549
905
|
|
|
550
906
|
/**
|
|
551
|
-
* Appends one
|
|
552
|
-
* @param {number[]} positions
|
|
553
|
-
* @param {{ x: number, y: number }} a
|
|
554
|
-
* @param {{ x: number, y: number }} b
|
|
555
|
-
* @param {{ x: number, y: number }} c
|
|
556
|
-
* @param {number} z
|
|
907
|
+
* Appends one shallow triangular prism into the position buffer.
|
|
908
|
+
* @param {number[]} positions Position buffer.
|
|
909
|
+
* @param {{ x: number, y: number }} a First point.
|
|
910
|
+
* @param {{ x: number, y: number }} b Second point.
|
|
911
|
+
* @param {{ x: number, y: number }} c Third point.
|
|
912
|
+
* @param {number} z Center Z position.
|
|
557
913
|
* @returns {void}
|
|
558
914
|
*/
|
|
559
915
|
static #appendTriangle(positions, a, b, c, z) {
|
|
560
|
-
|
|
916
|
+
const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
|
|
917
|
+
const topZ = z + halfThickness
|
|
918
|
+
const bottomZ = z - halfThickness
|
|
919
|
+
|
|
920
|
+
positions.push(
|
|
921
|
+
a.x,
|
|
922
|
+
a.y,
|
|
923
|
+
topZ,
|
|
924
|
+
b.x,
|
|
925
|
+
b.y,
|
|
926
|
+
topZ,
|
|
927
|
+
c.x,
|
|
928
|
+
c.y,
|
|
929
|
+
topZ,
|
|
930
|
+
c.x,
|
|
931
|
+
c.y,
|
|
932
|
+
bottomZ,
|
|
933
|
+
b.x,
|
|
934
|
+
b.y,
|
|
935
|
+
bottomZ,
|
|
936
|
+
a.x,
|
|
937
|
+
a.y,
|
|
938
|
+
bottomZ
|
|
939
|
+
)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Appends a side wall for one actual copper boundary edge.
|
|
944
|
+
* @param {number[]} positions Position buffer.
|
|
945
|
+
* @param {{ x: number, y: number }} start Wall start point.
|
|
946
|
+
* @param {{ x: number, y: number }} end Wall end point.
|
|
947
|
+
* @param {number} z Center Z position.
|
|
948
|
+
* @returns {void}
|
|
949
|
+
*/
|
|
950
|
+
static #appendBoundarySideTriangles(positions, start, end, z) {
|
|
951
|
+
const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
|
|
952
|
+
const topZ = z + halfThickness
|
|
953
|
+
const bottomZ = z - halfThickness
|
|
954
|
+
|
|
955
|
+
positions.push(
|
|
956
|
+
start.x,
|
|
957
|
+
start.y,
|
|
958
|
+
topZ,
|
|
959
|
+
end.x,
|
|
960
|
+
end.y,
|
|
961
|
+
topZ,
|
|
962
|
+
end.x,
|
|
963
|
+
end.y,
|
|
964
|
+
bottomZ,
|
|
965
|
+
start.x,
|
|
966
|
+
start.y,
|
|
967
|
+
topZ,
|
|
968
|
+
end.x,
|
|
969
|
+
end.y,
|
|
970
|
+
bottomZ,
|
|
971
|
+
start.x,
|
|
972
|
+
start.y,
|
|
973
|
+
bottomZ
|
|
974
|
+
)
|
|
561
975
|
}
|
|
562
976
|
}
|