pcb-scene3d-viewer 1.1.1 → 1.1.2

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.
@@ -0,0 +1,581 @@
1
+ /**
2
+ * Renders selected-component inspector markup and normalizes live transform
3
+ * adjustment values.
4
+ */
5
+ export class PcbScene3dSelectionInspectorRenderer {
6
+ /**
7
+ * Renders the empty inspector state.
8
+ * @param {(key: string) => string} translate Translation lookup.
9
+ * @returns {string}
10
+ */
11
+ static renderEmpty(translate) {
12
+ return (
13
+ '<h4 class="scene-3d__selection-title">' +
14
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
15
+ translate('scene3d.componentInspector')
16
+ ) +
17
+ '</h4><p class="scene-3d__selection-empty">' +
18
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
19
+ translate('scene3d.inspectPrompt')
20
+ ) +
21
+ '</p>'
22
+ )
23
+ }
24
+
25
+ /**
26
+ * Renders a no-metadata state for a selected designator.
27
+ * @param {string} designator Selected designator.
28
+ * @param {(key: string) => string} translate Translation lookup.
29
+ * @returns {string}
30
+ */
31
+ static renderMissing(designator, translate) {
32
+ return (
33
+ '<h4 class="scene-3d__selection-title">' +
34
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
35
+ translate('scene3d.componentInspector')
36
+ ) +
37
+ '</h4><p class="scene-3d__selection-empty">' +
38
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
39
+ translate('scene3d.noMetadataFor')
40
+ ) +
41
+ ' ' +
42
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(designator) +
43
+ '.</p>'
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Renders selected component details and optional editable transform controls.
49
+ * @param {{ designator: string, selection?: { sourceType?: string } | null, selectionEntry: { component: any | null, externalPlacement: any | null }, adjustment: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }, includeControls?: boolean, translate: (key: string) => string }} options Render options.
50
+ * @returns {string}
51
+ */
52
+ static renderSelected(options) {
53
+ const component = options.selectionEntry.component
54
+ const externalPlacement = options.selectionEntry.externalPlacement
55
+ const fields = [
56
+ [options.translate('scene3d.designator'), options.designator],
57
+ [
58
+ options.translate('scene3d.picked'),
59
+ options.selection?.sourceType === 'external-model'
60
+ ? options.translate('scene3d.externalModel')
61
+ : options.translate('scene3d.fallbackBody')
62
+ ],
63
+ [
64
+ options.translate('scene3d.mountSide'),
65
+ externalPlacement?.mountSide || component?.mountSide || ''
66
+ ],
67
+ [
68
+ options.translate('scene3d.rotation'),
69
+ PcbScene3dSelectionInspectorRenderer.#formatValue(
70
+ component?.rotationDeg ?? externalPlacement?.rotationDeg,
71
+ 'deg'
72
+ )
73
+ ],
74
+ [
75
+ options.translate('scene3d.boardPosition'),
76
+ component?.boardPositionMil
77
+ ? PcbScene3dSelectionInspectorRenderer.#formatPoint(
78
+ component.boardPositionMil,
79
+ true
80
+ )
81
+ : ''
82
+ ],
83
+ [
84
+ options.translate('scene3d.pattern'),
85
+ String(component?.pattern || '')
86
+ ],
87
+ [
88
+ options.translate('scene3d.source'),
89
+ String(component?.source || '')
90
+ ],
91
+ [
92
+ options.translate('scene3d.model'),
93
+ PcbScene3dSelectionInspectorRenderer.#formatModelName(
94
+ externalPlacement,
95
+ component
96
+ )
97
+ ],
98
+ [
99
+ options.translate('scene3d.bodyPosition'),
100
+ externalPlacement?.bodyPositionMil
101
+ ? PcbScene3dSelectionInspectorRenderer.#formatPoint(
102
+ externalPlacement.bodyPositionMil,
103
+ false
104
+ )
105
+ : ''
106
+ ],
107
+ [
108
+ options.translate('scene3d.bodyRotation'),
109
+ PcbScene3dSelectionInspectorRenderer.#formatValue(
110
+ externalPlacement?.bodyRotationDeg,
111
+ 'deg'
112
+ )
113
+ ],
114
+ [
115
+ options.translate('scene3d.modelRotation'),
116
+ PcbScene3dSelectionInspectorRenderer.#formatRotation(
117
+ externalPlacement?.modelTransform?.rotationDeg
118
+ )
119
+ ],
120
+ [
121
+ 'dz',
122
+ PcbScene3dSelectionInspectorRenderer.#formatValue(
123
+ externalPlacement?.modelTransform?.dzMil,
124
+ 'mil'
125
+ )
126
+ ]
127
+ ].filter(([, value]) => String(value || '').trim())
128
+
129
+ return (
130
+ '<h4 class="scene-3d__selection-title">' +
131
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
132
+ options.translate('scene3d.componentInspector')
133
+ ) +
134
+ '</h4><dl class="scene-3d__selection-list">' +
135
+ fields
136
+ .map(
137
+ ([label, value]) =>
138
+ '<div class="scene-3d__selection-field"><dt>' +
139
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
140
+ label
141
+ ) +
142
+ '</dt><dd>' +
143
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
144
+ String(value)
145
+ ) +
146
+ '</dd></div>'
147
+ )
148
+ .join('') +
149
+ '</dl>' +
150
+ (options.includeControls === false
151
+ ? ''
152
+ : PcbScene3dSelectionInspectorRenderer.renderControls(
153
+ options.adjustment,
154
+ options.translate
155
+ ))
156
+ )
157
+ }
158
+
159
+ /**
160
+ * Renders an empty transform-control host state.
161
+ * @param {(key: string) => string} translate Translation lookup.
162
+ * @returns {string}
163
+ */
164
+ static renderControlsEmpty(translate) {
165
+ return (
166
+ '<p class="scene-3d__selection-empty">' +
167
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
168
+ translate('scene3d.inspectPrompt')
169
+ ) +
170
+ '</p>'
171
+ )
172
+ }
173
+
174
+ /**
175
+ * Renders a missing-metadata transform-control host state.
176
+ * @param {string} designator Selected designator.
177
+ * @param {(key: string) => string} translate Translation lookup.
178
+ * @returns {string}
179
+ */
180
+ static renderControlsMissing(designator, translate) {
181
+ return (
182
+ '<p class="scene-3d__selection-empty">' +
183
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
184
+ translate('scene3d.noMetadataFor')
185
+ ) +
186
+ ' ' +
187
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(designator) +
188
+ '.</p>'
189
+ )
190
+ }
191
+
192
+ /**
193
+ * Renders selected component transform controls for an external host.
194
+ * @param {{ designator: string, adjustment: { scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }, translate: (key: string) => string }} options Render options.
195
+ * @returns {string}
196
+ */
197
+ static renderControlsPanel(options) {
198
+ return (
199
+ '<div class="scene-3d__adjustment-target"><span>' +
200
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
201
+ options.translate('scene3d.designator')
202
+ ) +
203
+ '</span><strong>' +
204
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
205
+ options.designator
206
+ ) +
207
+ '</strong></div>' +
208
+ PcbScene3dSelectionInspectorRenderer.renderControls(
209
+ options.adjustment,
210
+ options.translate
211
+ )
212
+ )
213
+ }
214
+
215
+ /**
216
+ * Renders editable transform controls.
217
+ * @param {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }} adjustment Current adjustment.
218
+ * @param {(key: string) => string} translate Translation lookup.
219
+ * @returns {string}
220
+ */
221
+ static renderControls(adjustment, translate) {
222
+ return (
223
+ '<div class="scene-3d__adjustments">' +
224
+ PcbScene3dSelectionInspectorRenderer.#renderGroup(
225
+ translate('scene3d.scale'),
226
+ [
227
+ ['scale.x', 'X', adjustment.scale.x, '0.0001', 4],
228
+ ['scale.y', 'Y', adjustment.scale.y, '0.0001', 4],
229
+ ['scale.z', 'Z', adjustment.scale.z, '0.0001', 4]
230
+ ]
231
+ ) +
232
+ PcbScene3dSelectionInspectorRenderer.#renderGroup(
233
+ translate('scene3d.rotation'),
234
+ [
235
+ ['rotation.x', 'X', adjustment.rotationDeg.x, '0.01', 2],
236
+ ['rotation.y', 'Y', adjustment.rotationDeg.y, '0.01', 2],
237
+ ['rotation.z', 'Z', adjustment.rotationDeg.z, '0.01', 2]
238
+ ]
239
+ ) +
240
+ PcbScene3dSelectionInspectorRenderer.#renderGroup(
241
+ translate('scene3d.offset'),
242
+ [
243
+ [
244
+ 'offset.x',
245
+ 'X',
246
+ PcbScene3dSelectionInspectorRenderer.milToMm(
247
+ adjustment.offsetMil.x
248
+ ),
249
+ '0.000001',
250
+ 6
251
+ ],
252
+ [
253
+ 'offset.y',
254
+ 'Y',
255
+ PcbScene3dSelectionInspectorRenderer.milToMm(
256
+ adjustment.offsetMil.y
257
+ ),
258
+ '0.000001',
259
+ 6
260
+ ],
261
+ [
262
+ 'offset.z',
263
+ 'Z',
264
+ PcbScene3dSelectionInspectorRenderer.milToMm(
265
+ adjustment.offsetMil.z
266
+ ),
267
+ '0.000001',
268
+ 6
269
+ ]
270
+ ]
271
+ ) +
272
+ '<button class="scene-3d__adjustment-reset" type="button" data-scene-3d-adjustment-reset>' +
273
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
274
+ translate('scene3d.resetTransform')
275
+ ) +
276
+ '</button></div>'
277
+ )
278
+ }
279
+
280
+ /**
281
+ * Resolves the original adjustment for one selected component.
282
+ * @param {Map<string, { externalPlacement: any | null }>} selectionIndex Selection lookup.
283
+ * @param {string} designator Selected designator.
284
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
285
+ */
286
+ static resolveBaseline(selectionIndex, designator) {
287
+ const modelTransform =
288
+ selectionIndex.get(designator)?.externalPlacement?.modelTransform ||
289
+ null
290
+ const rotationDeg = modelTransform?.rotationDeg || {}
291
+ const scale = modelTransform?.scale || {}
292
+
293
+ return {
294
+ scale: {
295
+ x: PcbScene3dSelectionInspectorRenderer.numberOr(scale.x, 1),
296
+ y: PcbScene3dSelectionInspectorRenderer.numberOr(scale.y, 1),
297
+ z: PcbScene3dSelectionInspectorRenderer.numberOr(scale.z, 1)
298
+ },
299
+ rotationDeg: {
300
+ x: PcbScene3dSelectionInspectorRenderer.numberOr(
301
+ rotationDeg.x,
302
+ 0
303
+ ),
304
+ y: PcbScene3dSelectionInspectorRenderer.numberOr(
305
+ rotationDeg.y,
306
+ 0
307
+ ),
308
+ z: PcbScene3dSelectionInspectorRenderer.numberOr(
309
+ rotationDeg.z,
310
+ 0
311
+ )
312
+ },
313
+ offsetMil:
314
+ PcbScene3dSelectionInspectorRenderer.resolveModelOffsetMil(
315
+ modelTransform
316
+ )
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Finds an adjustment input from an event target.
322
+ * @param {any} target Event target.
323
+ * @returns {any | null}
324
+ */
325
+ static closestInput(target) {
326
+ const closest = target?.closest?.('[data-scene-3d-adjustment]')
327
+ return closest || target?.getAttribute?.('data-scene-3d-adjustment')
328
+ ? closest || target
329
+ : null
330
+ }
331
+
332
+ /**
333
+ * Writes one input value into a cloned adjustment object.
334
+ * @param {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }} adjustment Mutable adjustment.
335
+ * @param {string} path Adjustment path.
336
+ * @param {number} value Input value.
337
+ * @returns {boolean}
338
+ */
339
+ static writePath(adjustment, path, value) {
340
+ const [group, axis] = String(path || '').split('.')
341
+ if (!['x', 'y', 'z'].includes(axis)) return false
342
+ if (group === 'scale') adjustment.scale[axis] = value
343
+ else if (group === 'rotation') adjustment.rotationDeg[axis] = value
344
+ else if (group === 'offset') {
345
+ adjustment.offsetMil[axis] =
346
+ PcbScene3dSelectionInspectorRenderer.mmToMil(value)
347
+ } else return false
348
+ return true
349
+ }
350
+
351
+ /**
352
+ * Clones one adjustment object.
353
+ * @param {{ scale: object, rotationDeg: object, offsetMil: object }} adjustment Source adjustment.
354
+ * @returns {{ scale: { x: number, y: number, z: number }, rotationDeg: { x: number, y: number, z: number }, offsetMil: { x: number, y: number, z: number } }}
355
+ */
356
+ static cloneAdjustment(adjustment) {
357
+ return {
358
+ scale: { ...adjustment.scale },
359
+ rotationDeg: { ...adjustment.rotationDeg },
360
+ offsetMil: { ...adjustment.offsetMil }
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Converts mil to millimeters.
366
+ * @param {number} milValue Mil value.
367
+ * @returns {number}
368
+ */
369
+ static milToMm(milValue) {
370
+ return (
371
+ Number(milValue || 0) /
372
+ PcbScene3dSelectionInspectorRenderer.#milsPerMm()
373
+ )
374
+ }
375
+
376
+ /**
377
+ * Converts millimeters to mil.
378
+ * @param {number} mmValue Millimeter value.
379
+ * @returns {number}
380
+ */
381
+ static mmToMil(mmValue) {
382
+ return (
383
+ Number(mmValue || 0) *
384
+ PcbScene3dSelectionInspectorRenderer.#milsPerMm()
385
+ )
386
+ }
387
+
388
+ /**
389
+ * Resolves model offset from current and legacy transform properties.
390
+ * @param {object | null} modelTransform Model transform.
391
+ * @returns {{ x: number, y: number, z: number }}
392
+ */
393
+ static resolveModelOffsetMil(modelTransform) {
394
+ const offsetMil = modelTransform?.offsetMil || {}
395
+ return {
396
+ x: PcbScene3dSelectionInspectorRenderer.numberOr(
397
+ offsetMil.x ?? modelTransform?.dxMil,
398
+ 0
399
+ ),
400
+ y: PcbScene3dSelectionInspectorRenderer.numberOr(
401
+ offsetMil.y ?? modelTransform?.dyMil,
402
+ 0
403
+ ),
404
+ z: PcbScene3dSelectionInspectorRenderer.numberOr(
405
+ offsetMil.z ?? modelTransform?.dzMil,
406
+ 0
407
+ )
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Returns a finite number or fallback.
413
+ * @param {unknown} value Source value.
414
+ * @param {number} fallback Fallback value.
415
+ * @returns {number}
416
+ */
417
+ static numberOr(value, fallback) {
418
+ const numericValue = Number(value)
419
+ return Number.isFinite(numericValue) ? numericValue : fallback
420
+ }
421
+
422
+ /**
423
+ * Renders one transform control group.
424
+ * @param {string} title Group title.
425
+ * @param {Array<[string, string, number, string, number]>} rows Rows.
426
+ * @returns {string}
427
+ */
428
+ static #renderGroup(title, rows) {
429
+ return (
430
+ '<fieldset class="scene-3d__adjustment-group"><legend>' +
431
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(title) +
432
+ '</legend>' +
433
+ rows
434
+ .map(
435
+ ([path, axis, value, step, decimals]) =>
436
+ '<label class="scene-3d__adjustment-row"><span>' +
437
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(axis) +
438
+ '</span><span class="scene-3d__adjustment-input-wrap"><input type="text" inputmode="decimal" autocomplete="off" spellcheck="false" step="' +
439
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(step) +
440
+ '" value="' +
441
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
442
+ PcbScene3dSelectionInspectorRenderer.#formatFixed(
443
+ value,
444
+ decimals
445
+ )
446
+ ) +
447
+ '" data-scene-3d-adjustment="' +
448
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(path) +
449
+ '" aria-label="' +
450
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(
451
+ title + ' ' + axis
452
+ ) +
453
+ '" /><span class="scene-3d__adjustment-stepper" aria-hidden="true"><button type="button" tabindex="-1" data-scene-3d-adjustment-step="up" data-scene-3d-adjustment-step-for="' +
454
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(path) +
455
+ '"></button><button type="button" tabindex="-1" data-scene-3d-adjustment-step="down" data-scene-3d-adjustment-step-for="' +
456
+ PcbScene3dSelectionInspectorRenderer.#escapeHtml(path) +
457
+ '"></button></span></span></label>'
458
+ )
459
+ .join('') +
460
+ '</fieldset>'
461
+ )
462
+ }
463
+
464
+ /**
465
+ * Formats a model file label.
466
+ * @param {any} externalPlacement External placement.
467
+ * @param {any} component Component.
468
+ * @returns {string}
469
+ */
470
+ static #formatModelName(externalPlacement, component) {
471
+ const model =
472
+ externalPlacement?.externalModel || component?.externalModel
473
+ return model
474
+ ? String(model.name || '') + ' (' + String(model.format || '') + ')'
475
+ : ''
476
+ }
477
+
478
+ /**
479
+ * Formats one rotation triplet.
480
+ * @param {{ x?: number, y?: number, z?: number } | null | undefined} rotationDeg Rotation.
481
+ * @returns {string}
482
+ */
483
+ static #formatRotation(rotationDeg) {
484
+ return rotationDeg
485
+ ? 'X ' +
486
+ PcbScene3dSelectionInspectorRenderer.#formatNumber(
487
+ rotationDeg.x
488
+ ) +
489
+ ', Y ' +
490
+ PcbScene3dSelectionInspectorRenderer.#formatNumber(
491
+ rotationDeg.y
492
+ ) +
493
+ ', Z ' +
494
+ PcbScene3dSelectionInspectorRenderer.#formatNumber(
495
+ rotationDeg.z
496
+ )
497
+ : ''
498
+ }
499
+
500
+ /**
501
+ * Formats one point.
502
+ * @param {{ x?: number, y?: number, z?: number }} point Point.
503
+ * @param {boolean} includeZ Whether to include Z.
504
+ * @returns {string}
505
+ */
506
+ static #formatPoint(point, includeZ) {
507
+ const values = [
508
+ 'X ' + PcbScene3dSelectionInspectorRenderer.#formatNumber(point?.x),
509
+ 'Y ' + PcbScene3dSelectionInspectorRenderer.#formatNumber(point?.y)
510
+ ]
511
+ if (includeZ) {
512
+ values.push(
513
+ 'Z ' +
514
+ PcbScene3dSelectionInspectorRenderer.#formatNumber(point?.z)
515
+ )
516
+ }
517
+ return values.join(', ') + ' mil'
518
+ }
519
+
520
+ /**
521
+ * Formats one value with a unit.
522
+ * @param {number | undefined} value Value.
523
+ * @param {string} unit Unit.
524
+ * @returns {string}
525
+ */
526
+ static #formatValue(value, unit) {
527
+ if (!Number.isFinite(Number(value))) return ''
528
+ return (
529
+ PcbScene3dSelectionInspectorRenderer.#formatNumber(value) +
530
+ ' ' +
531
+ unit
532
+ )
533
+ }
534
+
535
+ /**
536
+ * Formats one compact display number.
537
+ * @param {number | undefined} value Value.
538
+ * @returns {string}
539
+ */
540
+ static #formatNumber(value) {
541
+ const numericValue = Number(value)
542
+ return Number.isFinite(numericValue)
543
+ ? numericValue.toFixed(2).replace(/\.00$/, '')
544
+ : ''
545
+ }
546
+
547
+ /**
548
+ * Formats one input number with fixed decimals.
549
+ * @param {number} value Value.
550
+ * @param {number} decimals Decimal places.
551
+ * @returns {string}
552
+ */
553
+ static #formatFixed(value, decimals) {
554
+ const numericValue = Number(value)
555
+ return Number.isFinite(numericValue)
556
+ ? numericValue.toFixed(decimals)
557
+ : ''
558
+ }
559
+
560
+ /**
561
+ * Escapes user-facing HTML values.
562
+ * @param {string} value Raw value.
563
+ * @returns {string}
564
+ */
565
+ static #escapeHtml(value) {
566
+ return String(value)
567
+ .replace(/&/g, '&amp;')
568
+ .replace(/</g, '&lt;')
569
+ .replace(/>/g, '&gt;')
570
+ .replace(/"/g, '&quot;')
571
+ .replace(/'/g, '&#39;')
572
+ }
573
+
574
+ /**
575
+ * Returns mils per millimeter.
576
+ * @returns {number}
577
+ */
578
+ static #milsPerMm() {
579
+ return 1000 / 25.4
580
+ }
581
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Resolves selection payloads from picked Three.js object trees.
3
+ */
4
+ export class PcbScene3dSelectionResolver {
5
+ /**
6
+ * Resolves the picked selection record from raycast intersections.
7
+ * @param {{ object?: any }[]} intersections Intersections from a raycast.
8
+ * @returns {{ designator?: string, sourceType?: string } | null}
9
+ */
10
+ static fromIntersections(intersections) {
11
+ for (const intersection of intersections) {
12
+ const selection = PcbScene3dSelectionResolver.fromObject(
13
+ intersection?.object
14
+ )
15
+ if (selection) {
16
+ return selection
17
+ }
18
+ }
19
+
20
+ return null
21
+ }
22
+
23
+ /**
24
+ * Resolves one selection payload by walking up a picked object tree.
25
+ * @param {any} object Picked object.
26
+ * @returns {{ designator?: string, sourceType?: string } | null}
27
+ */
28
+ static fromObject(object) {
29
+ let current = object
30
+
31
+ while (current) {
32
+ const selection = current?.userData?.scene3dSelection
33
+ if (selection) {
34
+ return selection
35
+ }
36
+
37
+ current = current.parent || null
38
+ }
39
+
40
+ return null
41
+ }
42
+ }
@@ -860,7 +860,10 @@ export class PcbScene3dSilkscreenFactory {
860
860
  opacity: 1,
861
861
  toneMapped: false,
862
862
  fog: false,
863
- side: THREE.DoubleSide
863
+ side: THREE.DoubleSide,
864
+ polygonOffset: true,
865
+ polygonOffsetFactor: -3,
866
+ polygonOffsetUnits: -3
864
867
  })
865
868
  }
866
869
 
@@ -40,7 +40,10 @@ const FALLBACK_MESSAGES = {
40
40
  'scene3d.placements': 'Placements',
41
41
  'scene3d.pointerHint':
42
42
  'Drag to orbit, right-drag to pan, and use the wheel to zoom.',
43
+ 'scene3d.offset': 'Offset',
44
+ 'scene3d.resetTransform': 'Reset',
43
45
  'scene3d.rotation': 'Rotation',
46
+ 'scene3d.scale': 'Scale',
44
47
  'scene3d.skipped': 'Skipped',
45
48
  'scene3d.source': 'Source',
46
49
  'scene3d.startFailed': '3D preview could not start:',