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,322 @@
|
|
|
1
|
+
import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Appends triangle geometry for PCB stroke-style tracks and arcs.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dStrokeGeometryBuilder {
|
|
7
|
+
static #DEFAULT_MIN_WIDTH = 1
|
|
8
|
+
static #ROUND_CAP_SEGMENTS = 16
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Appends one widened track with round end caps.
|
|
12
|
+
* @param {number[]} positions Position buffer.
|
|
13
|
+
* @param {{ x: number, y: number }} start Start point.
|
|
14
|
+
* @param {{ x: number, y: number }} end End point.
|
|
15
|
+
* @param {number} width Stroke width.
|
|
16
|
+
* @param {number} z Z position.
|
|
17
|
+
* @param {{ minWidth?: number }} [options] Stroke options.
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
static appendTrack(positions, start, end, width, z, options = {}) {
|
|
21
|
+
const dx = end.x - start.x
|
|
22
|
+
const dy = end.y - start.y
|
|
23
|
+
const length = Math.hypot(dx, dy)
|
|
24
|
+
const halfWidth =
|
|
25
|
+
PcbScene3dStrokeGeometryBuilder.#safeStrokeWidth(
|
|
26
|
+
width,
|
|
27
|
+
options.minWidth
|
|
28
|
+
) / 2
|
|
29
|
+
|
|
30
|
+
if (length <= 0.001) {
|
|
31
|
+
PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
|
|
32
|
+
positions,
|
|
33
|
+
start,
|
|
34
|
+
halfWidth,
|
|
35
|
+
z
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalX = (-dy / length) * halfWidth
|
|
41
|
+
const normalY = (dx / length) * halfWidth
|
|
42
|
+
|
|
43
|
+
PcbScene3dStrokeGeometryBuilder.#appendQuadTriangles(
|
|
44
|
+
positions,
|
|
45
|
+
{ x: start.x + normalX, y: start.y + normalY },
|
|
46
|
+
{ x: end.x + normalX, y: end.y + normalY },
|
|
47
|
+
{ x: end.x - normalX, y: end.y - normalY },
|
|
48
|
+
{ x: start.x - normalX, y: start.y - normalY },
|
|
49
|
+
z
|
|
50
|
+
)
|
|
51
|
+
PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
|
|
52
|
+
positions,
|
|
53
|
+
start,
|
|
54
|
+
halfWidth,
|
|
55
|
+
z
|
|
56
|
+
)
|
|
57
|
+
PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
|
|
58
|
+
positions,
|
|
59
|
+
end,
|
|
60
|
+
halfWidth,
|
|
61
|
+
z
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Appends one widened arc band with round caps for open arcs.
|
|
67
|
+
* @param {number[]} positions Position buffer.
|
|
68
|
+
* @param {{ x: number, y: number }} center Arc center.
|
|
69
|
+
* @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number }} arc
|
|
70
|
+
* Arc primitive.
|
|
71
|
+
* @param {number} z Z position.
|
|
72
|
+
* @param {boolean} mirrorY Whether the arc is mirrored around the X axis.
|
|
73
|
+
* @param {{ minWidth?: number, fullCircleEpsilon?: number }} [options]
|
|
74
|
+
* Stroke options.
|
|
75
|
+
* @returns {void}
|
|
76
|
+
*/
|
|
77
|
+
static appendArc(positions, center, arc, z, mirrorY, options = {}) {
|
|
78
|
+
const strokeWidth = PcbScene3dStrokeGeometryBuilder.#safeStrokeWidth(
|
|
79
|
+
arc?.width,
|
|
80
|
+
options.minWidth
|
|
81
|
+
)
|
|
82
|
+
const radius = Math.max(Number(arc?.radius || 0), strokeWidth / 2, 0.8)
|
|
83
|
+
const outerRadius = radius + strokeWidth / 2
|
|
84
|
+
const innerRadius = Math.max(radius - strokeWidth / 2, 0)
|
|
85
|
+
const startAngleRad = (Number(arc?.startAngle || 0) * Math.PI) / 180
|
|
86
|
+
const deltaAngleDeg = PcbScene3dArcUtils.resolveSweepDelta(
|
|
87
|
+
Number(arc?.startAngle || 0),
|
|
88
|
+
Number(arc?.endAngle || 0)
|
|
89
|
+
)
|
|
90
|
+
const epsilon = Number(options.fullCircleEpsilon ?? 0.001)
|
|
91
|
+
const isFullCircle =
|
|
92
|
+
Math.abs(deltaAngleDeg) <= epsilon ||
|
|
93
|
+
Math.abs(deltaAngleDeg) >= 360 - epsilon
|
|
94
|
+
const deltaAngleRad = isFullCircle
|
|
95
|
+
? Math.PI * 2
|
|
96
|
+
: (deltaAngleDeg * Math.PI) / 180
|
|
97
|
+
const segments = Math.max(
|
|
98
|
+
isFullCircle ? 20 : 8,
|
|
99
|
+
Math.ceil((Math.abs(deltaAngleRad) / Math.PI) * 18)
|
|
100
|
+
)
|
|
101
|
+
const yDirection = mirrorY ? -1 : 1
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < segments; index += 1) {
|
|
104
|
+
PcbScene3dStrokeGeometryBuilder.#appendArcSegmentTriangles(
|
|
105
|
+
positions,
|
|
106
|
+
center,
|
|
107
|
+
{
|
|
108
|
+
innerRadius,
|
|
109
|
+
outerRadius,
|
|
110
|
+
startAngle:
|
|
111
|
+
startAngleRad + (deltaAngleRad * index) / segments,
|
|
112
|
+
endAngle:
|
|
113
|
+
startAngleRad +
|
|
114
|
+
(deltaAngleRad * (index + 1)) / segments,
|
|
115
|
+
yDirection
|
|
116
|
+
},
|
|
117
|
+
z
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!isFullCircle) {
|
|
122
|
+
PcbScene3dStrokeGeometryBuilder.#appendArcCapTriangles(
|
|
123
|
+
positions,
|
|
124
|
+
center,
|
|
125
|
+
radius,
|
|
126
|
+
strokeWidth / 2,
|
|
127
|
+
startAngleRad,
|
|
128
|
+
yDirection,
|
|
129
|
+
z
|
|
130
|
+
)
|
|
131
|
+
PcbScene3dStrokeGeometryBuilder.#appendArcCapTriangles(
|
|
132
|
+
positions,
|
|
133
|
+
center,
|
|
134
|
+
radius,
|
|
135
|
+
strokeWidth / 2,
|
|
136
|
+
startAngleRad + deltaAngleRad,
|
|
137
|
+
yDirection,
|
|
138
|
+
z
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Appends one arc segment band.
|
|
145
|
+
* @param {number[]} positions Position buffer.
|
|
146
|
+
* @param {{ x: number, y: number }} center Arc center.
|
|
147
|
+
* @param {{ innerRadius: number, outerRadius: number, startAngle: number, endAngle: number, yDirection: number }} segment
|
|
148
|
+
* Arc segment geometry.
|
|
149
|
+
* @param {number} z Z position.
|
|
150
|
+
* @returns {void}
|
|
151
|
+
*/
|
|
152
|
+
static #appendArcSegmentTriangles(positions, center, segment, z) {
|
|
153
|
+
const outerStart = {
|
|
154
|
+
x: center.x + Math.cos(segment.startAngle) * segment.outerRadius,
|
|
155
|
+
y:
|
|
156
|
+
center.y +
|
|
157
|
+
Math.sin(segment.startAngle) *
|
|
158
|
+
segment.outerRadius *
|
|
159
|
+
segment.yDirection
|
|
160
|
+
}
|
|
161
|
+
const outerEnd = {
|
|
162
|
+
x: center.x + Math.cos(segment.endAngle) * segment.outerRadius,
|
|
163
|
+
y:
|
|
164
|
+
center.y +
|
|
165
|
+
Math.sin(segment.endAngle) *
|
|
166
|
+
segment.outerRadius *
|
|
167
|
+
segment.yDirection
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (segment.innerRadius <= 0.001) {
|
|
171
|
+
PcbScene3dStrokeGeometryBuilder.#appendTriangle(
|
|
172
|
+
positions,
|
|
173
|
+
{ x: center.x, y: center.y },
|
|
174
|
+
outerStart,
|
|
175
|
+
outerEnd,
|
|
176
|
+
z
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const innerStart = {
|
|
182
|
+
x: center.x + Math.cos(segment.startAngle) * segment.innerRadius,
|
|
183
|
+
y:
|
|
184
|
+
center.y +
|
|
185
|
+
Math.sin(segment.startAngle) *
|
|
186
|
+
segment.innerRadius *
|
|
187
|
+
segment.yDirection
|
|
188
|
+
}
|
|
189
|
+
const innerEnd = {
|
|
190
|
+
x: center.x + Math.cos(segment.endAngle) * segment.innerRadius,
|
|
191
|
+
y:
|
|
192
|
+
center.y +
|
|
193
|
+
Math.sin(segment.endAngle) *
|
|
194
|
+
segment.innerRadius *
|
|
195
|
+
segment.yDirection
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
PcbScene3dStrokeGeometryBuilder.#appendQuadTriangles(
|
|
199
|
+
positions,
|
|
200
|
+
outerStart,
|
|
201
|
+
outerEnd,
|
|
202
|
+
innerEnd,
|
|
203
|
+
innerStart,
|
|
204
|
+
z
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Appends one rounded arc endpoint.
|
|
210
|
+
* @param {number[]} positions Position buffer.
|
|
211
|
+
* @param {{ x: number, y: number }} center Arc center.
|
|
212
|
+
* @param {number} radius Centerline radius.
|
|
213
|
+
* @param {number} capRadius Cap radius.
|
|
214
|
+
* @param {number} angle Arc endpoint angle.
|
|
215
|
+
* @param {number} yDirection Mirrored Y direction.
|
|
216
|
+
* @param {number} z Z position.
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
219
|
+
static #appendArcCapTriangles(
|
|
220
|
+
positions,
|
|
221
|
+
center,
|
|
222
|
+
radius,
|
|
223
|
+
capRadius,
|
|
224
|
+
angle,
|
|
225
|
+
yDirection,
|
|
226
|
+
z
|
|
227
|
+
) {
|
|
228
|
+
PcbScene3dStrokeGeometryBuilder.#appendDiscTriangles(
|
|
229
|
+
positions,
|
|
230
|
+
{
|
|
231
|
+
x: center.x + Math.cos(angle) * radius,
|
|
232
|
+
y: center.y + Math.sin(angle) * radius * yDirection
|
|
233
|
+
},
|
|
234
|
+
capRadius,
|
|
235
|
+
z
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Appends one rectangle as two triangles.
|
|
241
|
+
* @param {number[]} positions Position buffer.
|
|
242
|
+
* @param {{ x: number, y: number }} a First corner.
|
|
243
|
+
* @param {{ x: number, y: number }} b Second corner.
|
|
244
|
+
* @param {{ x: number, y: number }} c Third corner.
|
|
245
|
+
* @param {{ x: number, y: number }} d Fourth corner.
|
|
246
|
+
* @param {number} z Z position.
|
|
247
|
+
* @returns {void}
|
|
248
|
+
*/
|
|
249
|
+
static #appendQuadTriangles(positions, a, b, c, d, z) {
|
|
250
|
+
PcbScene3dStrokeGeometryBuilder.#appendTriangle(positions, a, b, c, z)
|
|
251
|
+
PcbScene3dStrokeGeometryBuilder.#appendTriangle(positions, a, c, d, z)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Appends one filled circle fan.
|
|
256
|
+
* @param {number[]} positions Position buffer.
|
|
257
|
+
* @param {{ x: number, y: number }} center Disc center.
|
|
258
|
+
* @param {number} radius Disc radius.
|
|
259
|
+
* @param {number} z Z position.
|
|
260
|
+
* @returns {void}
|
|
261
|
+
*/
|
|
262
|
+
static #appendDiscTriangles(positions, center, radius, z) {
|
|
263
|
+
const safeRadius = Math.max(Number(radius || 0), 0)
|
|
264
|
+
if (safeRadius <= 0) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (
|
|
269
|
+
let index = 0;
|
|
270
|
+
index < PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS;
|
|
271
|
+
index += 1
|
|
272
|
+
) {
|
|
273
|
+
const startAngle =
|
|
274
|
+
(Math.PI * 2 * index) /
|
|
275
|
+
PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS
|
|
276
|
+
const endAngle =
|
|
277
|
+
(Math.PI * 2 * (index + 1)) /
|
|
278
|
+
PcbScene3dStrokeGeometryBuilder.#ROUND_CAP_SEGMENTS
|
|
279
|
+
|
|
280
|
+
PcbScene3dStrokeGeometryBuilder.#appendTriangle(
|
|
281
|
+
positions,
|
|
282
|
+
center,
|
|
283
|
+
{
|
|
284
|
+
x: center.x + Math.cos(startAngle) * safeRadius,
|
|
285
|
+
y: center.y + Math.sin(startAngle) * safeRadius
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
x: center.x + Math.cos(endAngle) * safeRadius,
|
|
289
|
+
y: center.y + Math.sin(endAngle) * safeRadius
|
|
290
|
+
},
|
|
291
|
+
z
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolves a finite stroke width with a caller-specific minimum.
|
|
298
|
+
* @param {unknown} width Authored stroke width.
|
|
299
|
+
* @param {unknown} minWidth Minimum width.
|
|
300
|
+
* @returns {number}
|
|
301
|
+
*/
|
|
302
|
+
static #safeStrokeWidth(width, minWidth) {
|
|
303
|
+
return Math.max(
|
|
304
|
+
Number(width) || PcbScene3dStrokeGeometryBuilder.#DEFAULT_MIN_WIDTH,
|
|
305
|
+
Number(minWidth) ||
|
|
306
|
+
PcbScene3dStrokeGeometryBuilder.#DEFAULT_MIN_WIDTH
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Appends one triangle into the position buffer.
|
|
312
|
+
* @param {number[]} positions Position buffer.
|
|
313
|
+
* @param {{ x: number, y: number }} a First point.
|
|
314
|
+
* @param {{ x: number, y: number }} b Second point.
|
|
315
|
+
* @param {{ x: number, y: number }} c Third point.
|
|
316
|
+
* @param {number} z Z position.
|
|
317
|
+
* @returns {void}
|
|
318
|
+
*/
|
|
319
|
+
static #appendTriangle(positions, a, b, c, z) {
|
|
320
|
+
positions.push(a.x, a.y, z, b.x, b.y, z, c.x, c.y, z)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const FALLBACK_MESSAGES = {
|
|
2
|
+
'scene3d.boardEnvelopeSuffix': 'mil board envelope',
|
|
3
|
+
'scene3d.boardPosition': 'Board position',
|
|
4
|
+
'scene3d.bodyPosition': 'Body position',
|
|
5
|
+
'scene3d.bodyRotation': 'Body rotation',
|
|
6
|
+
'scene3d.bomGroups': 'BOM groups',
|
|
7
|
+
'scene3d.bottom': 'Bottom',
|
|
8
|
+
'scene3d.companionModelsHint':
|
|
9
|
+
'Companion models will be used when matching WRL or STEP files are loaded in the session.',
|
|
10
|
+
'scene3d.componentInspector': 'Component inspector',
|
|
11
|
+
'scene3d.componentsSuffix': 'components',
|
|
12
|
+
'scene3d.controlsAria': '3D detail toggles',
|
|
13
|
+
'scene3d.copperDetail': 'Copper detail',
|
|
14
|
+
'scene3d.designator': 'Designator',
|
|
15
|
+
'scene3d.downloadModelsZip': 'Download Models ZIP',
|
|
16
|
+
'scene3d.downloaded': 'Downloaded',
|
|
17
|
+
'scene3d.entries': 'entries',
|
|
18
|
+
'scene3d.entry': 'entry',
|
|
19
|
+
'scene3d.exportFailed': 'Model ZIP export failed:',
|
|
20
|
+
'scene3d.externalModel': 'External model',
|
|
21
|
+
'scene3d.externalModels': 'External models',
|
|
22
|
+
'scene3d.fallbackBody': 'Fallback body',
|
|
23
|
+
'scene3d.fallbackBodies': 'Fallback bodies',
|
|
24
|
+
'scene3d.footprint': 'Footprint',
|
|
25
|
+
'scene3d.inspectPrompt': 'Click a component to inspect it.',
|
|
26
|
+
'scene3d.interactiveViewAria': 'Interactive 3D PCB view',
|
|
27
|
+
'scene3d.isometric': 'Isometric',
|
|
28
|
+
'scene3d.loading': 'Preparing 3D scene...',
|
|
29
|
+
'scene3d.model': 'Model',
|
|
30
|
+
'scene3d.modelFile': 'model file',
|
|
31
|
+
'scene3d.modelFiles': 'model files',
|
|
32
|
+
'scene3d.modelRotation': 'Model rotation',
|
|
33
|
+
'scene3d.mountSide': 'Mount side',
|
|
34
|
+
'scene3d.noMetadataFor': 'No metadata is available for',
|
|
35
|
+
'scene3d.noModelsForExport':
|
|
36
|
+
'No STEP or WRL models were resolved for export.',
|
|
37
|
+
'scene3d.noPcb': '3D preview is available after loading a PCB document.',
|
|
38
|
+
'scene3d.pattern': 'Pattern',
|
|
39
|
+
'scene3d.picked': 'Picked',
|
|
40
|
+
'scene3d.placements': 'Placements',
|
|
41
|
+
'scene3d.pointerHint':
|
|
42
|
+
'Drag to orbit, right-drag to pan, and use the wheel to zoom.',
|
|
43
|
+
'scene3d.rotation': 'Rotation',
|
|
44
|
+
'scene3d.skipped': 'Skipped',
|
|
45
|
+
'scene3d.source': 'Source',
|
|
46
|
+
'scene3d.startFailed': '3D preview could not start:',
|
|
47
|
+
'scene3d.stillPreparing': '3D scene is still preparing.',
|
|
48
|
+
'scene3d.title': '3D preview',
|
|
49
|
+
'scene3d.to': 'to',
|
|
50
|
+
'scene3d.toolbarAria': '3D camera presets',
|
|
51
|
+
'scene3d.top': 'Top',
|
|
52
|
+
'scene3d.touchHint':
|
|
53
|
+
'Drag with one finger to orbit, pinch to zoom, and drag with two fingers to pan.',
|
|
54
|
+
'scene3d.unresolved': 'unresolved'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fallback-aware text helpers for viewer-owned UI copy.
|
|
59
|
+
*/
|
|
60
|
+
export class PcbScene3dText {
|
|
61
|
+
/**
|
|
62
|
+
* Translates one key with English fallback copy.
|
|
63
|
+
* @param {((key: string) => string) | null | undefined} translate
|
|
64
|
+
* Host translation lookup.
|
|
65
|
+
* @param {string} key Message key.
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
static translate(translate, key) {
|
|
69
|
+
if (typeof translate !== 'function') {
|
|
70
|
+
return PcbScene3dText.fallback(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const value = translate(key)
|
|
74
|
+
if (!value || value === key) {
|
|
75
|
+
return PcbScene3dText.fallback(key)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a stable translator function with English fallback copy.
|
|
83
|
+
* @param {((key: string) => string) | null | undefined} translate
|
|
84
|
+
* Host translation lookup.
|
|
85
|
+
* @returns {(key: string) => string}
|
|
86
|
+
*/
|
|
87
|
+
static createTranslator(translate) {
|
|
88
|
+
return (key) => PcbScene3dText.translate(translate, key)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns English fallback copy for one key.
|
|
93
|
+
* @param {string} key Message key.
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
static fallback(key) {
|
|
97
|
+
return FALLBACK_MESSAGES[key] || key
|
|
98
|
+
}
|
|
99
|
+
}
|