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,534 @@
|
|
|
1
|
+
import { PcbScene3dStrokeFont } from './PcbScene3dStrokeFont.mjs'
|
|
2
|
+
import { PcbScene3dCutoutGeometryFilter } from './PcbScene3dCutoutGeometryFilter.mjs'
|
|
3
|
+
import { PcbScene3dStrokeGeometryBuilder } from './PcbScene3dStrokeGeometryBuilder.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds KiCad copper text as widened stroke meshes for the 3D PCB scene.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dCopperTextFactory {
|
|
9
|
+
static #DEFAULT_MATERIAL_COLOR = 0xd9a61d
|
|
10
|
+
static #TEXT_LINE_SPACING_RATIO = 1.61
|
|
11
|
+
static #FIRST_LINE_HEIGHT_RATIO = 1.17
|
|
12
|
+
static #STROKE_BASELINE_FUDGE_RATIO = 0.052
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds one side-specific copper text group.
|
|
16
|
+
* @param {any} THREE
|
|
17
|
+
* @param {any[]} texts
|
|
18
|
+
* @param {number} z
|
|
19
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
20
|
+
* @param {{ side?: 'top' | 'bottom', mirrorY?: boolean, materialColor?: number, filterSide?: boolean, glyphYUp?: boolean, drillCutouts?: { x: number, y: number }[][] }} [options]
|
|
21
|
+
* @returns {any}
|
|
22
|
+
*/
|
|
23
|
+
static buildGroup(THREE, texts, z, normalizeBoardPoint, options = {}) {
|
|
24
|
+
const group = new THREE.Group()
|
|
25
|
+
const positions = []
|
|
26
|
+
const side = PcbScene3dCopperTextFactory.#normalizeSide(options?.side)
|
|
27
|
+
const mirrorY = Boolean(options?.mirrorY)
|
|
28
|
+
const glyphYUp = Boolean(options?.glyphYUp)
|
|
29
|
+
const shouldFilterSide = options?.filterSide !== false
|
|
30
|
+
group.name = 'copper-texts'
|
|
31
|
+
;(texts || [])
|
|
32
|
+
.filter((text) =>
|
|
33
|
+
shouldFilterSide
|
|
34
|
+
? PcbScene3dCopperTextFactory.#matchesSide(text, side)
|
|
35
|
+
: true
|
|
36
|
+
)
|
|
37
|
+
.forEach((text) => {
|
|
38
|
+
PcbScene3dCopperTextFactory.#appendTextTriangles(
|
|
39
|
+
positions,
|
|
40
|
+
text,
|
|
41
|
+
z,
|
|
42
|
+
normalizeBoardPoint,
|
|
43
|
+
mirrorY,
|
|
44
|
+
glyphYUp
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (!positions.length) {
|
|
49
|
+
return group
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const geometry = new THREE.BufferGeometry()
|
|
53
|
+
geometry.setAttribute(
|
|
54
|
+
'position',
|
|
55
|
+
new THREE.Float32BufferAttribute(positions, 3)
|
|
56
|
+
)
|
|
57
|
+
geometry.computeVertexNormals?.()
|
|
58
|
+
const filteredGeometry =
|
|
59
|
+
PcbScene3dCopperTextFactory.#filterDrillCutouts(
|
|
60
|
+
THREE,
|
|
61
|
+
geometry,
|
|
62
|
+
options?.drillCutouts
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!PcbScene3dCopperTextFactory.#hasGeometryPositions(filteredGeometry)
|
|
67
|
+
) {
|
|
68
|
+
return group
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mesh = new THREE.Mesh(
|
|
72
|
+
filteredGeometry,
|
|
73
|
+
PcbScene3dCopperTextFactory.#buildMaterial(
|
|
74
|
+
THREE,
|
|
75
|
+
PcbScene3dCopperTextFactory.#resolveMaterialColor(
|
|
76
|
+
options?.materialColor
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
mesh.name = 'copper-text'
|
|
81
|
+
group.add(mesh)
|
|
82
|
+
return group
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Removes text stroke triangles that overlap drill cutouts.
|
|
87
|
+
* @param {any} THREE
|
|
88
|
+
* @param {any} geometry
|
|
89
|
+
* @param {{ x: number, y: number }[][] | undefined} drillCutouts
|
|
90
|
+
* @returns {any}
|
|
91
|
+
*/
|
|
92
|
+
static #filterDrillCutouts(THREE, geometry, drillCutouts) {
|
|
93
|
+
return Array.isArray(drillCutouts) && drillCutouts.length
|
|
94
|
+
? PcbScene3dCutoutGeometryFilter.filter(
|
|
95
|
+
THREE,
|
|
96
|
+
geometry,
|
|
97
|
+
drillCutouts
|
|
98
|
+
)
|
|
99
|
+
: geometry
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true when a geometry still has triangle positions.
|
|
104
|
+
* @param {any} geometry
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
static #hasGeometryPositions(geometry) {
|
|
108
|
+
const position =
|
|
109
|
+
geometry?.getAttribute?.('position') ??
|
|
110
|
+
geometry?.attributes?.get?.('position')
|
|
111
|
+
|
|
112
|
+
return Boolean(position?.count || position?.array?.length)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Appends all widened stroke segments for one text primitive.
|
|
117
|
+
* @param {number[]} positions
|
|
118
|
+
* @param {object} text
|
|
119
|
+
* @param {number} z
|
|
120
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
121
|
+
* @param {boolean} mirrorY
|
|
122
|
+
* @param {boolean} glyphYUp
|
|
123
|
+
* @returns {void}
|
|
124
|
+
*/
|
|
125
|
+
static #appendTextTriangles(
|
|
126
|
+
positions,
|
|
127
|
+
text,
|
|
128
|
+
z,
|
|
129
|
+
normalizeBoardPoint,
|
|
130
|
+
mirrorY,
|
|
131
|
+
glyphYUp
|
|
132
|
+
) {
|
|
133
|
+
const width = PcbScene3dCopperTextFactory.#textStrokeWidth(text)
|
|
134
|
+
|
|
135
|
+
PcbScene3dCopperTextFactory.#textStrokes(text, glyphYUp).forEach(
|
|
136
|
+
(stroke) => {
|
|
137
|
+
for (let index = 1; index < stroke.length; index += 1) {
|
|
138
|
+
const start = PcbScene3dCopperTextFactory.#normalizePoint(
|
|
139
|
+
normalizeBoardPoint,
|
|
140
|
+
stroke[index - 1],
|
|
141
|
+
mirrorY
|
|
142
|
+
)
|
|
143
|
+
const end = PcbScene3dCopperTextFactory.#normalizePoint(
|
|
144
|
+
normalizeBoardPoint,
|
|
145
|
+
stroke[index],
|
|
146
|
+
mirrorY
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
PcbScene3dCopperTextFactory.#appendTrackTriangles(
|
|
150
|
+
positions,
|
|
151
|
+
start,
|
|
152
|
+
end,
|
|
153
|
+
width,
|
|
154
|
+
z
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds all KiCad stroke-font point lists for one text primitive.
|
|
163
|
+
* @param {object} text
|
|
164
|
+
* @param {boolean} glyphYUp
|
|
165
|
+
* @returns {{ x: number, y: number }[][]}
|
|
166
|
+
*/
|
|
167
|
+
static #textStrokes(text, glyphYUp) {
|
|
168
|
+
const lines = String(text?.value ?? text?.text ?? '').split('\n')
|
|
169
|
+
const lineSpacing = PcbScene3dCopperTextFactory.#textLineSpacing(text)
|
|
170
|
+
|
|
171
|
+
return lines.flatMap((line, index) =>
|
|
172
|
+
PcbScene3dCopperTextFactory.#textLineStrokes(
|
|
173
|
+
text,
|
|
174
|
+
line,
|
|
175
|
+
index,
|
|
176
|
+
lines.length,
|
|
177
|
+
lineSpacing,
|
|
178
|
+
glyphYUp
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds transformed stroke point lists for one line.
|
|
185
|
+
* @param {object} text
|
|
186
|
+
* @param {string} line
|
|
187
|
+
* @param {number} index
|
|
188
|
+
* @param {number} lineCount
|
|
189
|
+
* @param {number} lineSpacing
|
|
190
|
+
* @param {boolean} glyphYUp
|
|
191
|
+
* @returns {{ x: number, y: number }[][]}
|
|
192
|
+
*/
|
|
193
|
+
static #textLineStrokes(
|
|
194
|
+
text,
|
|
195
|
+
line,
|
|
196
|
+
index,
|
|
197
|
+
lineCount,
|
|
198
|
+
lineSpacing,
|
|
199
|
+
glyphYUp
|
|
200
|
+
) {
|
|
201
|
+
const sizeX = PcbScene3dCopperTextFactory.#textWidth(text)
|
|
202
|
+
const sizeY = PcbScene3dCopperTextFactory.#textHeight(text)
|
|
203
|
+
const layout = PcbScene3dStrokeFont.layoutLine(line, {
|
|
204
|
+
x: 0,
|
|
205
|
+
y: 0,
|
|
206
|
+
sizeX,
|
|
207
|
+
sizeY
|
|
208
|
+
})
|
|
209
|
+
const x = PcbScene3dCopperTextFactory.#textLineX(text, layout.width)
|
|
210
|
+
const y = PcbScene3dCopperTextFactory.#textLineY(
|
|
211
|
+
text,
|
|
212
|
+
index,
|
|
213
|
+
lineCount,
|
|
214
|
+
lineSpacing
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return layout.strokes.map((stroke) =>
|
|
218
|
+
stroke.map((point) =>
|
|
219
|
+
PcbScene3dCopperTextFactory.#transformTextPoint(
|
|
220
|
+
text,
|
|
221
|
+
{ x: point.x + x, y: point.y + y },
|
|
222
|
+
glyphYUp
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolves KiCad-like baseline spacing for multiline text.
|
|
230
|
+
* @param {object} text
|
|
231
|
+
* @returns {number}
|
|
232
|
+
*/
|
|
233
|
+
static #textLineSpacing(text) {
|
|
234
|
+
return (
|
|
235
|
+
PcbScene3dCopperTextFactory.#textHeight(text) *
|
|
236
|
+
PcbScene3dCopperTextFactory.#TEXT_LINE_SPACING_RATIO
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolves vertical text size.
|
|
242
|
+
* @param {object} text
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
static #textHeight(text) {
|
|
246
|
+
return PcbScene3dCopperTextFactory.#positiveTextSize(
|
|
247
|
+
text?.sizeX,
|
|
248
|
+
text?.sizeY,
|
|
249
|
+
text?.height
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolves horizontal text size.
|
|
255
|
+
* @param {object} text
|
|
256
|
+
* @returns {number}
|
|
257
|
+
*/
|
|
258
|
+
static #textWidth(text) {
|
|
259
|
+
return PcbScene3dCopperTextFactory.#positiveTextSize(
|
|
260
|
+
text?.sizeY,
|
|
261
|
+
text?.sizeX,
|
|
262
|
+
text?.height
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolves a positive text metric.
|
|
268
|
+
* @param {number | undefined} primary
|
|
269
|
+
* @param {number | undefined} secondary
|
|
270
|
+
* @param {number | undefined} tertiary
|
|
271
|
+
* @returns {number}
|
|
272
|
+
*/
|
|
273
|
+
static #positiveTextSize(primary, secondary, tertiary) {
|
|
274
|
+
return Math.max(
|
|
275
|
+
Number(primary) ||
|
|
276
|
+
Number(secondary) ||
|
|
277
|
+
Number(tertiary) ||
|
|
278
|
+
39.37007874,
|
|
279
|
+
0.001
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolves line origin from KiCad horizontal justification.
|
|
285
|
+
* @param {object} text
|
|
286
|
+
* @param {number} lineWidth
|
|
287
|
+
* @returns {number}
|
|
288
|
+
*/
|
|
289
|
+
static #textLineX(text, lineWidth) {
|
|
290
|
+
const fudge =
|
|
291
|
+
PcbScene3dCopperTextFactory.#textStrokeHorizontalFudge(text)
|
|
292
|
+
|
|
293
|
+
if (text?.hAlign === 'left') {
|
|
294
|
+
return Number(text?.x || 0) + fudge
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (text?.hAlign === 'right') {
|
|
298
|
+
return Number(text?.x || 0) - lineWidth - fudge
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return Number(text?.x || 0) - lineWidth / 2
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolves one line baseline from KiCad vertical justification.
|
|
306
|
+
* @param {object} text
|
|
307
|
+
* @param {number} index
|
|
308
|
+
* @param {number} lineCount
|
|
309
|
+
* @param {number} lineSpacing
|
|
310
|
+
* @returns {number}
|
|
311
|
+
*/
|
|
312
|
+
static #textLineY(text, index, lineCount, lineSpacing) {
|
|
313
|
+
const height = PcbScene3dCopperTextFactory.#textHeight(text)
|
|
314
|
+
const blockHeight =
|
|
315
|
+
height * PcbScene3dCopperTextFactory.#FIRST_LINE_HEIGHT_RATIO +
|
|
316
|
+
lineSpacing * (lineCount - 1)
|
|
317
|
+
let baseline =
|
|
318
|
+
Number(text?.y || 0) +
|
|
319
|
+
height -
|
|
320
|
+
PcbScene3dCopperTextFactory.#textStrokeBaselineFudge(text)
|
|
321
|
+
|
|
322
|
+
if (text?.vAlign === 'bottom') {
|
|
323
|
+
baseline -= blockHeight
|
|
324
|
+
} else if (text?.vAlign === 'center') {
|
|
325
|
+
baseline -= blockHeight / 2
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return baseline + lineSpacing * index
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resolves KiCad text stroke width.
|
|
333
|
+
* @param {object} text
|
|
334
|
+
* @returns {number}
|
|
335
|
+
*/
|
|
336
|
+
static #textStrokeWidth(text) {
|
|
337
|
+
return Math.max(
|
|
338
|
+
Number(text?.thickness) ||
|
|
339
|
+
Number(text?.strokeWidth) ||
|
|
340
|
+
4.7244094488,
|
|
341
|
+
0.01
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolves KiCad's small horizontal text adjustment.
|
|
347
|
+
* @param {object} text
|
|
348
|
+
* @returns {number}
|
|
349
|
+
*/
|
|
350
|
+
static #textStrokeHorizontalFudge(text) {
|
|
351
|
+
return PcbScene3dCopperTextFactory.#textStrokeWidth(text) / 1.52
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolves KiCad's small baseline text adjustment.
|
|
356
|
+
* @param {object} text
|
|
357
|
+
* @returns {number}
|
|
358
|
+
*/
|
|
359
|
+
static #textStrokeBaselineFudge(text) {
|
|
360
|
+
return (
|
|
361
|
+
PcbScene3dCopperTextFactory.#textStrokeWidth(text) *
|
|
362
|
+
PcbScene3dCopperTextFactory.#STROKE_BASELINE_FUDGE_RATIO
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Applies KiCad text rotation and mirrored text transforms.
|
|
368
|
+
* @param {object} text
|
|
369
|
+
* @param {{ x: number, y: number }} point
|
|
370
|
+
* @param {boolean} glyphYUp
|
|
371
|
+
* @returns {{ x: number, y: number }}
|
|
372
|
+
*/
|
|
373
|
+
static #transformTextPoint(text, point, glyphYUp) {
|
|
374
|
+
const origin = {
|
|
375
|
+
x: Number(text?.x || 0),
|
|
376
|
+
y: Number(text?.y || 0)
|
|
377
|
+
}
|
|
378
|
+
const sourcePoint = glyphYUp
|
|
379
|
+
? PcbScene3dCopperTextFactory.#mirrorPointY(point, origin)
|
|
380
|
+
: point
|
|
381
|
+
|
|
382
|
+
if (text?.mirrored) {
|
|
383
|
+
const rotated = PcbScene3dCopperTextFactory.#rotatePoint(
|
|
384
|
+
sourcePoint,
|
|
385
|
+
origin,
|
|
386
|
+
Number(text?.rotation || 0)
|
|
387
|
+
)
|
|
388
|
+
return {
|
|
389
|
+
x: origin.x - (rotated.x - origin.x),
|
|
390
|
+
y: rotated.y
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return PcbScene3dCopperTextFactory.#rotatePoint(
|
|
395
|
+
sourcePoint,
|
|
396
|
+
origin,
|
|
397
|
+
-Number(text?.rotation || 0)
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Mirrors one stroke-font point across the text anchor's local X axis.
|
|
403
|
+
* @param {{ x: number, y: number }} point
|
|
404
|
+
* @param {{ x: number, y: number }} origin
|
|
405
|
+
* @returns {{ x: number, y: number }}
|
|
406
|
+
*/
|
|
407
|
+
static #mirrorPointY(point, origin) {
|
|
408
|
+
return {
|
|
409
|
+
x: Number(point?.x || 0),
|
|
410
|
+
y: origin.y - (Number(point?.y || 0) - origin.y)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Rotates one point around an origin.
|
|
416
|
+
* @param {{ x: number, y: number }} point
|
|
417
|
+
* @param {{ x: number, y: number }} origin
|
|
418
|
+
* @param {number} angleDeg
|
|
419
|
+
* @returns {{ x: number, y: number }}
|
|
420
|
+
*/
|
|
421
|
+
static #rotatePoint(point, origin, angleDeg) {
|
|
422
|
+
const angle = (Number(angleDeg || 0) * Math.PI) / 180
|
|
423
|
+
const cos = Math.cos(angle)
|
|
424
|
+
const sin = Math.sin(angle)
|
|
425
|
+
const dx = Number(point?.x || 0) - origin.x
|
|
426
|
+
const dy = Number(point?.y || 0) - origin.y
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
x: origin.x + dx * cos - dy * sin,
|
|
430
|
+
y: origin.y + dx * sin + dy * cos
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Normalizes one board point and optionally mirrors it for underside use.
|
|
436
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
437
|
+
* @param {{ x: number, y: number }} point
|
|
438
|
+
* @param {boolean} mirrorY
|
|
439
|
+
* @returns {{ x: number, y: number }}
|
|
440
|
+
*/
|
|
441
|
+
static #normalizePoint(normalizeBoardPoint, point, mirrorY) {
|
|
442
|
+
const normalizedPoint = normalizeBoardPoint(point.x, point.y)
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
x: normalizedPoint.x,
|
|
446
|
+
y: mirrorY ? -normalizedPoint.y : normalizedPoint.y
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Appends one widened stroke segment as two triangles.
|
|
452
|
+
* @param {number[]} positions
|
|
453
|
+
* @param {{ x: number, y: number }} start
|
|
454
|
+
* @param {{ x: number, y: number }} end
|
|
455
|
+
* @param {number} width
|
|
456
|
+
* @param {number} z
|
|
457
|
+
* @returns {void}
|
|
458
|
+
*/
|
|
459
|
+
static #appendTrackTriangles(positions, start, end, width, z) {
|
|
460
|
+
PcbScene3dStrokeGeometryBuilder.appendTrack(
|
|
461
|
+
positions,
|
|
462
|
+
start,
|
|
463
|
+
end,
|
|
464
|
+
width,
|
|
465
|
+
z,
|
|
466
|
+
{ minWidth: 1 }
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Builds the shared copper text material.
|
|
472
|
+
* @param {any} THREE
|
|
473
|
+
* @param {number} color
|
|
474
|
+
* @returns {any}
|
|
475
|
+
*/
|
|
476
|
+
static #buildMaterial(THREE, color) {
|
|
477
|
+
return new THREE.MeshStandardMaterial({
|
|
478
|
+
color,
|
|
479
|
+
roughness: 0.38,
|
|
480
|
+
metalness: 0.55,
|
|
481
|
+
side: THREE.DoubleSide
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Resolves a safe RGB material color.
|
|
487
|
+
* @param {unknown} color
|
|
488
|
+
* @returns {number}
|
|
489
|
+
*/
|
|
490
|
+
static #resolveMaterialColor(color) {
|
|
491
|
+
const numericColor = Number(color)
|
|
492
|
+
|
|
493
|
+
return Number.isInteger(numericColor) &&
|
|
494
|
+
numericColor >= 0 &&
|
|
495
|
+
numericColor <= 0xffffff
|
|
496
|
+
? numericColor
|
|
497
|
+
: PcbScene3dCopperTextFactory.#DEFAULT_MATERIAL_COLOR
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Normalizes a side option.
|
|
502
|
+
* @param {string | undefined} side
|
|
503
|
+
* @returns {'top' | 'bottom'}
|
|
504
|
+
*/
|
|
505
|
+
static #normalizeSide(side) {
|
|
506
|
+
return String(side || '').toLowerCase() === 'bottom' ? 'bottom' : 'top'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Checks whether one text belongs to the requested copper side.
|
|
511
|
+
* @param {object} text
|
|
512
|
+
* @param {'top' | 'bottom'} side
|
|
513
|
+
* @returns {boolean}
|
|
514
|
+
*/
|
|
515
|
+
static #matchesSide(text, side) {
|
|
516
|
+
const layer = String(text?.layer || '').toUpperCase()
|
|
517
|
+
const layerId = Number(text?.layerId ?? NaN)
|
|
518
|
+
const textSide = String(text?.side || '').toLowerCase()
|
|
519
|
+
|
|
520
|
+
if (layer) {
|
|
521
|
+
return side === 'bottom' ? layer === 'B.CU' : layer === 'F.CU'
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (Number.isFinite(layerId)) {
|
|
525
|
+
return side === 'bottom' ? layerId === 32 : layerId === 1
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (side === 'bottom') {
|
|
529
|
+
return textSide === 'back' || textSide === 'bottom'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return textSide === 'front' || textSide === 'top'
|
|
533
|
+
}
|
|
534
|
+
}
|