@preference-sl/pref-viewer 2.17.0-beta.0 → 2.17.0-beta.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,215 @@
1
+ /**
2
+ * Applies semantic selection state to Babylon model meshes using non-destructive overlays.
3
+ * Retains the semantic snapshot across model replacements and restores original overlays on cleanup.
4
+ * @class BabylonJSSelectionController
5
+ */
6
+ export default class BabylonJSSelectionController {
7
+ #getModelMeshes;
8
+ #requestRender;
9
+ #selectableColor;
10
+ #selectedColor;
11
+ #overlayAlpha;
12
+ #originalOverlays = new Map();
13
+ #state = {};
14
+
15
+ /**
16
+ * Removes the glTF loader suffix from a mesh id while preserving every other id shape.
17
+ * @public
18
+ * @static
19
+ * @param {unknown} value - Mesh id candidate.
20
+ * @returns {string} Normalized semantic id, or an empty string when unusable.
21
+ */
22
+ static normalizeMeshId(value) {
23
+ return typeof value === "string" ? value.replace(/_primitive\d+$/, "") : "";
24
+ }
25
+
26
+ /**
27
+ * Determines whether a completed pointer gesture represents a primary click.
28
+ * @public
29
+ * @static
30
+ * @param {{x:number,y:number,button:number,pointerId?:number,dragged?:boolean}|null} start - Pointer-down snapshot.
31
+ * @param {{x:number,y:number,button:number,pointerId?:number}|null} end - Pointer-up snapshot.
32
+ * @param {number} maxDistancePx - Maximum Euclidean movement in CSS pixels.
33
+ * @returns {boolean} Whether the gesture is a click.
34
+ */
35
+ static isPrimaryClickGesture(start, end, maxDistancePx) {
36
+ if (!start || !end || start.button !== 0 || end.button !== 0 || start.dragged || start.pointerId !== end.pointerId) {
37
+ return false;
38
+ }
39
+ return Math.hypot(end.x - start.x, end.y - start.y) <= maxDistancePx;
40
+ }
41
+
42
+ /**
43
+ * Checks that a model mesh has geometry and can participate in semantic picking.
44
+ * Explicit ray predicates bypass Babylon's default visibility and pickability checks.
45
+ * @public
46
+ * @static
47
+ * @param {object} mesh - Babylon mesh considered by the picking predicate.
48
+ * @param {Set<object>} modelMeshes - Mesh references belonging to the model container.
49
+ * @returns {boolean} Whether the mesh is visible, enabled, pickable and has vertices.
50
+ */
51
+ static isPickableModelMesh(mesh, modelMeshes) {
52
+ return modelMeshes.has(mesh) && mesh.isVisible !== false && mesh.visibility !== 0
53
+ && mesh.isPickable !== false && mesh.isEnabled?.() !== false
54
+ && (mesh.getTotalVertices?.() ?? 0) > 0;
55
+ }
56
+
57
+ /**
58
+ * Builds the set of model meshes accepted by picking, including Babylon instances owned by them.
59
+ * @public
60
+ * @static
61
+ * @param {Array<object>} meshes - Meshes from the model AssetContainer.
62
+ * @returns {Set<object>} Accepted model mesh references.
63
+ */
64
+ static getModelMeshSet(meshes = []) {
65
+ const result = new Set();
66
+ for (const mesh of meshes ?? []) {
67
+ if (!mesh) {
68
+ continue;
69
+ }
70
+ result.add(mesh);
71
+ for (const instance of mesh.instances ?? []) {
72
+ result.add(instance);
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Converts ordered Babylon ray hits into the public semantic mesh-id list.
80
+ * @public
81
+ * @static
82
+ * @param {Array<object>} picks - Babylon PickingInfo results.
83
+ * @param {Array<object>} modelMeshes - Meshes belonging to the model container.
84
+ * @returns {string[]} Stable, distance-ordered, deduplicated semantic ids.
85
+ */
86
+ static collectPickedMeshIds(picks = [], modelMeshes = []) {
87
+ const acceptedMeshes = BabylonJSSelectionController.getModelMeshSet(modelMeshes);
88
+ const ids = [];
89
+ const seen = new Set();
90
+ [...(picks ?? [])]
91
+ .filter((pick) => pick?.hit === true && acceptedMeshes.has(pick.pickedMesh))
92
+ .sort((left, right) => (left.distance ?? Number.POSITIVE_INFINITY) - (right.distance ?? Number.POSITIVE_INFINITY))
93
+ .forEach((pick) => {
94
+ const id = BabylonJSSelectionController.normalizeMeshId(pick.pickedMesh?.id);
95
+ if (id && !seen.has(id)) {
96
+ seen.add(id);
97
+ ids.push(id);
98
+ }
99
+ });
100
+ return ids;
101
+ }
102
+
103
+ /**
104
+ * Creates a controller with a live model-mesh provider and selection presentation settings.
105
+ * @public
106
+ * @param {function(): Array<object>} getModelMeshes - Returns current model meshes.
107
+ * @param {{selectableColor:object,selectedColor:object,overlayAlpha:number,requestRender?:function():void}} options - Overlay configuration.
108
+ */
109
+ constructor(getModelMeshes, options) {
110
+ this.#getModelMeshes = getModelMeshes;
111
+ this.#selectableColor = options.selectableColor;
112
+ this.#selectedColor = options.selectedColor;
113
+ this.#overlayAlpha = options.overlayAlpha;
114
+ this.#requestRender = options.requestRender ?? (() => {});
115
+ }
116
+
117
+ /**
118
+ * Applies a complete selection snapshot to every matching model mesh.
119
+ * @public
120
+ * @param {{selectableIds?:string[],selectedIds?:string[],selectedItems?:Array<{id:string,highlights?:string[]}>}} state - Canonical selection state.
121
+ * @returns {void}
122
+ */
123
+ applySelectionState(state = {}) {
124
+ this.#state = state;
125
+ this.refresh();
126
+ }
127
+
128
+ /**
129
+ * Reapplies the retained snapshot after model meshes are replaced.
130
+ * Restores removed meshes, gives selected ids priority and requests rendering only on changes.
131
+ * @public
132
+ * @returns {void}
133
+ */
134
+ refresh() {
135
+ const selectableIds = new Set(this.#state.selectableIds ?? []);
136
+ const selectedIds = new Set(this.#state.selectedIds ?? []);
137
+ for (const item of this.#state.selectedItems ?? []) {
138
+ if (item?.id) {
139
+ selectedIds.add(item.id);
140
+ }
141
+ for (const id of item?.highlights ?? []) {
142
+ selectedIds.add(id);
143
+ }
144
+ }
145
+
146
+ const currentMeshes = new Set(BabylonJSSelectionController.getModelMeshSet(this.#getModelMeshes?.() ?? []));
147
+ let changed = false;
148
+ for (const mesh of this.#originalOverlays.keys()) {
149
+ if (!currentMeshes.has(mesh)) {
150
+ changed = this.#restoreMesh(mesh) || changed;
151
+ }
152
+ }
153
+
154
+ for (const mesh of currentMeshes) {
155
+ const id = BabylonJSSelectionController.normalizeMeshId(mesh?.id);
156
+ const color = selectedIds.has(id)
157
+ ? this.#selectedColor
158
+ : (selectableIds.has(id) ? this.#selectableColor : null);
159
+ if (color) {
160
+ if (!this.#originalOverlays.has(mesh)) {
161
+ this.#originalOverlays.set(mesh, {
162
+ renderOverlay: mesh.renderOverlay,
163
+ overlayColor: mesh.overlayColor,
164
+ overlayAlpha: mesh.overlayAlpha,
165
+ });
166
+ }
167
+ if (mesh.renderOverlay !== true || mesh.overlayColor !== color || mesh.overlayAlpha !== this.#overlayAlpha) {
168
+ mesh.renderOverlay = true;
169
+ mesh.overlayColor = color;
170
+ mesh.overlayAlpha = this.#overlayAlpha;
171
+ changed = true;
172
+ }
173
+ } else {
174
+ changed = this.#restoreMesh(mesh) || changed;
175
+ }
176
+ }
177
+
178
+ if (changed) {
179
+ this.#requestRender();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Restores all meshes changed by this controller and releases their saved overlay properties.
185
+ * @public
186
+ * @returns {void}
187
+ */
188
+ dispose() {
189
+ let changed = false;
190
+ for (const mesh of [...this.#originalOverlays.keys()]) {
191
+ changed = this.#restoreMesh(mesh) || changed;
192
+ }
193
+ if (changed) {
194
+ this.#requestRender();
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Restores a mesh's saved overlay properties and removes its restoration snapshot.
200
+ * @private
201
+ * @param {object} mesh - Mesh previously updated by this controller.
202
+ * @returns {boolean} Whether a saved overlay was restored.
203
+ */
204
+ #restoreMesh(mesh) {
205
+ const original = this.#originalOverlays.get(mesh);
206
+ if (!original) {
207
+ return false;
208
+ }
209
+ mesh.renderOverlay = original.renderOverlay;
210
+ mesh.overlayColor = original.overlayColor;
211
+ mesh.overlayAlpha = original.overlayAlpha;
212
+ this.#originalOverlays.delete(mesh);
213
+ return true;
214
+ }
215
+ }
@@ -1,6 +1,6 @@
1
1
  import { Matrix, Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector.js";
2
2
  import { PrefViewerViewCubeStyles } from "./styles.js";
3
- import { translate } from "./localization/i18n.js";
3
+ import I18n from "./localization/i18n.js";
4
4
  import BabylonJSController from "./babylonjs-controller.js";
5
5
  import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
6
6
 
@@ -46,6 +46,15 @@ import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
46
46
  * @property {number|null} top - Current orthographic top extent.
47
47
  */
48
48
 
49
+ /**
50
+ * Virtual camera inputs used to project cube surfaces into SVG.
51
+ * @typedef {Object} ViewCubeProjection
52
+ * @property {Matrix} view - Cube-local to virtual camera rotation matrix.
53
+ * @property {boolean} perspective - Whether to apply perspective scaling.
54
+ * @property {number} depthSign - Scene handedness depth direction.
55
+ * @property {number} eyeDistance - Virtual camera distance independent of model zoom.
56
+ */
57
+
49
58
  /**
50
59
  * BabylonJSViewCubeController - Owns an SVG orientation indicator and model-framing camera navigation.
51
60
  *
@@ -144,26 +153,21 @@ export default class BabylonJSViewCubeController {
144
153
  this.#svg.append(node.outline);
145
154
  }
146
155
  host.canvas.parentElement.append(this.#root);
147
- const on = (element, name, handler) => {
148
- element.addEventListener(name, handler);
149
- this.#listeners.push(() => element.removeEventListener(name, handler));
150
- };
151
- const regionAt = (event) => event.target?.closest?.("polygon[data-region]")?.dataset.region ?? null;
152
- on(this.#svg, "pointermove", (event) => {
156
+ this.#addListener(this.#svg, "pointermove", (event) => {
153
157
  this.#pointer = { x: event.clientX, y: event.clientY };
154
- this.#setHover(regionAt(event));
158
+ this.#setHover(BabylonJSViewCubeController.#getRegionAt(event));
155
159
  });
156
- on(this.#svg, "pointerleave", () => {
160
+ this.#addListener(this.#svg, "pointerleave", () => {
157
161
  this.#pointer = null;
158
162
  this.#setHover(null);
159
163
  });
160
- on(this.#svg, "pointerdown", (event) => {
161
- if (regionAt(event)) {
164
+ this.#addListener(this.#svg, "pointerdown", (event) => {
165
+ if (BabylonJSViewCubeController.#getRegionAt(event)) {
162
166
  event.stopPropagation();
163
167
  }
164
168
  });
165
- on(this.#svg, "click", (event) => {
166
- const id = regionAt(event);
169
+ this.#addListener(this.#svg, "click", (event) => {
170
+ const id = BabylonJSViewCubeController.#getRegionAt(event);
167
171
  if (!id) {
168
172
  return;
169
173
  }
@@ -173,7 +177,7 @@ export default class BabylonJSViewCubeController {
173
177
  }
174
178
  });
175
179
  for (const event of ["pointerdown", "wheel", "keydown"]) {
176
- on(host.canvas, event, () => this.cancel());
180
+ this.#addListener(host.canvas, event, () => this.cancel());
177
181
  }
178
182
  this.#resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.resize());
179
183
  this.#resizeObserver?.observe(host.canvas);
@@ -192,41 +196,18 @@ export default class BabylonJSViewCubeController {
192
196
  static createViewCubeRegions() {
193
197
  const regions = [];
194
198
  const inset = 0.78;
195
- const surface = (axis, sign, ranges) => {
196
- const other = [0, 1, 2].filter((i) => i !== axis);
197
- const normalComponents = [0, 0, 0];
198
- normalComponents[axis] = sign;
199
- const normal = new Vector3(...normalComponents);
200
- const vertices = [
201
- [0, 0],
202
- [1, 0],
203
- [1, 1],
204
- [0, 1],
205
- ].map(([u, v]) => {
206
- const p = [0, 0, 0];
207
- p[axis] = sign;
208
- p[other[0]] = ranges[other[0]][u];
209
- p[other[1]] = ranges[other[1]][v];
210
- return new Vector3(...p);
211
- });
212
- return { normal, vertices };
213
- };
214
- const add = (axis, surfaces) => {
215
- const direction = new Vector3(...axis).normalize();
216
- regions.push({ id: axis.join(","), direction, surfaces, kind: ["", "face", "edge", "corner"][axis.filter(Boolean).length] });
217
- };
218
199
  for (let i = 0; i < 3; i++) {
219
200
  for (const s of [-1, 1]) {
220
201
  const axis = [0, 0, 0];
221
202
  axis[i] = s;
222
- add(axis, [
223
- surface(i, s, [
203
+ BabylonJSViewCubeController.#addRegion(regions, axis, [
204
+ BabylonJSViewCubeController.#createSurface(i, s, [
224
205
  [-inset, inset],
225
206
  [-inset, inset],
226
207
  [-inset, inset],
227
208
  ]),
228
209
  ]);
229
- regions.at(-1).outline = surface(i, s, [
210
+ regions.at(-1).outline = BabylonJSViewCubeController.#createSurface(i, s, [
230
211
  [-1, 1],
231
212
  [-1, 1],
232
213
  [-1, 1],
@@ -247,7 +228,7 @@ export default class BabylonJSViewCubeController {
247
228
  ];
248
229
  ranges[i] = [s * inset, s];
249
230
  ranges[j] = [t * inset, t];
250
- add(axis, [surface(i, s, ranges), surface(j, t, ranges)]);
231
+ BabylonJSViewCubeController.#addRegion(regions, axis, [BabylonJSViewCubeController.#createSurface(i, s, ranges), BabylonJSViewCubeController.#createSurface(j, t, ranges)]);
251
232
  }
252
233
  }
253
234
  }
@@ -257,9 +238,9 @@ export default class BabylonJSViewCubeController {
257
238
  for (const z of [-1, 1]) {
258
239
  const signs = [x, y, z];
259
240
  const ranges = signs.map((s) => [s * inset, s]);
260
- add(
241
+ BabylonJSViewCubeController.#addRegion(regions,
261
242
  signs,
262
- signs.map((s, i) => surface(i, s, ranges)),
243
+ signs.map((s, i) => BabylonJSViewCubeController.#createSurface(i, s, ranges)),
263
244
  );
264
245
  }
265
246
  }
@@ -267,6 +248,107 @@ export default class BabylonJSViewCubeController {
267
248
  return regions;
268
249
  }
269
250
 
251
+ /**
252
+ * Creates a signed-axis quad with its normal and four cube-space vertices.
253
+ * @private
254
+ * @static
255
+ * @param {number} axis - Fixed coordinate axis index.
256
+ * @param {number} sign - Fixed axis direction, -1 or 1.
257
+ * @param {Array<number[]>} ranges - Lower and upper coordinates for each axis.
258
+ * @returns {{normal:Vector3,vertices:Vector3[]}}
259
+ */
260
+ static #createSurface(axis, sign, ranges) {
261
+ const other = [0, 1, 2].filter((i) => i !== axis);
262
+ const normalComponents = [0, 0, 0];
263
+ normalComponents[axis] = sign;
264
+ const normal = new Vector3(...normalComponents);
265
+ const vertices = [
266
+ [0, 0],
267
+ [1, 0],
268
+ [1, 1],
269
+ [0, 1],
270
+ ].map(([u, v]) => {
271
+ const p = [0, 0, 0];
272
+ p[axis] = sign;
273
+ p[other[0]] = ranges[other[0]][u];
274
+ p[other[1]] = ranges[other[1]][v];
275
+ return new Vector3(...p);
276
+ });
277
+ return { normal, vertices };
278
+ }
279
+
280
+ /**
281
+ * Adds a face, edge or corner preset from its signed-axis vector and clickable surfaces.
282
+ * @private
283
+ * @static
284
+ * @param {ViewCubeRegion[]} regions - Destination region collection.
285
+ * @param {number[]} axis - Signed direction components.
286
+ * @param {Array<{normal:Vector3,vertices:Vector3[]}>} surfaces - Surfaces sharing that direction.
287
+ * @returns {void}
288
+ */
289
+ static #addRegion(regions, axis, surfaces) {
290
+ const direction = new Vector3(...axis).normalize();
291
+ regions.push({ id: axis.join(","), direction, surfaces, kind: ["", "face", "edge", "corner"][axis.filter(Boolean).length] });
292
+ }
293
+
294
+ /**
295
+ * Finds the semantic cube-region id under the event target.
296
+ * @private
297
+ * @static
298
+ * @param {PointerEvent|MouseEvent} event - Overlay input event.
299
+ * @returns {string|null}
300
+ */
301
+ static #getRegionAt(event) {
302
+ return event.target?.closest?.("polygon[data-region]")?.dataset.region ?? null;
303
+ }
304
+
305
+ /**
306
+ * Interpolates an orthographic extent from its start or fallback using the current eased transition.
307
+ * @private
308
+ * @static
309
+ * @param {number|null} start - Initial extent.
310
+ * @param {number} end - Destination extent.
311
+ * @param {number} fallback - Initial extent when absent.
312
+ * @param {number} eased - Eased transition fraction from 0 to 1.
313
+ * @returns {number}
314
+ */
315
+ static #interpolateExtent(start, end, fallback, eased) {
316
+ return (start ?? fallback) + (end - (start ?? fallback)) * eased;
317
+ }
318
+
319
+ /**
320
+ * Projects a cube-space vertex into the overlay SVG using the effective camera projection.
321
+ * @private
322
+ * @static
323
+ * @param {Vector3} vertex - Cube-space vertex.
324
+ * @param {ViewCubeProjection} projection - Current virtual cube projection.
325
+ * @returns {{x:number,y:number}}
326
+ */
327
+ static #projectVertex(vertex, projection) {
328
+ const { view, perspective, depthSign, eyeDistance } = projection;
329
+ const point = Vector3.TransformNormal(vertex, view);
330
+ const scale = perspective ? eyeDistance / (eyeDistance + point.z * depthSign) : 1;
331
+ return { x: 60 + point.x * 31 * scale, y: 60 - point.y * 31 * scale };
332
+ }
333
+
334
+ /**
335
+ * Updates surface visibility and projected polygon points for the current camera orientation.
336
+ * @private
337
+ * @static
338
+ * @param {object} surface - Surface containing its normal, vertices and SVG polygon.
339
+ * @param {ViewCubeProjection} projection - Current virtual cube projection.
340
+ * @returns {{shown:boolean,points:Array<{x:number,y:number}>}}
341
+ */
342
+ static #drawSurface(surface, projection) {
343
+ const { view, perspective, depthSign, eyeDistance } = projection;
344
+ const normal = Vector3.TransformNormal(surface.normal, view);
345
+ const shown = perspective ? -normal.z * depthSign * eyeDistance > 1.001 : normal.z * depthSign < -0.001;
346
+ surface.polygon.style.display = shown ? "" : "none";
347
+ const points = surface.vertices.map((vertex) => BabylonJSViewCubeController.#projectVertex(vertex, projection));
348
+ surface.polygon.setAttribute("points", points.map((p) => `${p.x},${p.y}`).join(" "));
349
+ return { shown, points };
350
+ }
351
+
270
352
  /**
271
353
  * Converts a quaternion into a new rotation matrix without changing the quaternion.
272
354
  * @private
@@ -390,6 +472,37 @@ export default class BabylonJSViewCubeController {
390
472
  this.#host.onLayout?.(cubeHeight, sceneHeight);
391
473
  }
392
474
 
475
+ /**
476
+ * Registers an overlay or canvas listener and retains its removal callback for disposal.
477
+ * @private
478
+ * @param {EventTarget} element - Event source.
479
+ * @param {string} name - DOM event name.
480
+ * @param {EventListener} handler - Handler to register.
481
+ * @returns {void}
482
+ */
483
+ #addListener(element, name, handler) {
484
+ element.addEventListener(name, handler);
485
+ this.#listeners.push(() => element.removeEventListener(name, handler));
486
+ }
487
+
488
+ /**
489
+ * Updates hover feedback for every surface and the outline of one region.
490
+ * @private
491
+ * @param {string|null} key - Cube region id whose hover feedback is updated.
492
+ * @param {boolean} active - Whether hover feedback is active.
493
+ * @returns {void}
494
+ */
495
+ #setRegionHover(key, active) {
496
+ const node = this.#nodes.get(key);
497
+ if (!node) {
498
+ return;
499
+ }
500
+ for (const surface of node.surfaces) {
501
+ surface.polygon.toggleAttribute("data-hover", active);
502
+ }
503
+ node.outline.toggleAttribute("data-hover", active);
504
+ }
505
+
393
506
  /**
394
507
  * Updates only the old/new region feedback attributes without rebuilding geometry.
395
508
  * Clears hover when disabled or locked; all surfaces of an edge or corner share the same state.
@@ -404,19 +517,10 @@ export default class BabylonJSViewCubeController {
404
517
  if (id === this.#hover) {
405
518
  return;
406
519
  }
407
- const update = (key, active) => {
408
- const node = this.#nodes.get(key);
409
- if (!node) {
410
- return;
411
- }
412
- for (const surface of node.surfaces) {
413
- surface.polygon.toggleAttribute("data-hover", active);
414
- }
415
- node.outline.toggleAttribute("data-hover", active);
416
- };
417
- update(this.#hover, false);
520
+
521
+ this.#setRegionHover(this.#hover, false);
418
522
  this.#hover = id;
419
- update(id, true);
523
+ this.#setRegionHover(id, true);
420
524
  }
421
525
 
422
526
  /**
@@ -539,12 +643,12 @@ export default class BabylonJSViewCubeController {
539
643
  const position = target.subtract(forward.scale(distance));
540
644
  this.#writePose(t.camera, position, target, distance, t.fit);
541
645
  if (t.mode === 1) {
542
- const lerp = (start, end, fallback) => (start ?? fallback) + (end - (start ?? fallback)) * eased;
646
+
543
647
  const viewport = this.#host.getViewport();
544
- t.camera.orthoLeft = lerp(t.start.left, -t.fit.halfWidth, -viewport.width / 2);
545
- t.camera.orthoRight = lerp(t.start.right, t.fit.halfWidth, viewport.width / 2);
546
- t.camera.orthoBottom = lerp(t.start.bottom, -t.fit.halfHeight, -viewport.height / 2);
547
- t.camera.orthoTop = lerp(t.start.top, t.fit.halfHeight, viewport.height / 2);
648
+ t.camera.orthoLeft = BabylonJSViewCubeController.#interpolateExtent(t.start.left, -t.fit.halfWidth, -viewport.width / 2, eased);
649
+ t.camera.orthoRight = BabylonJSViewCubeController.#interpolateExtent(t.start.right, t.fit.halfWidth, viewport.width / 2, eased);
650
+ t.camera.orthoBottom = BabylonJSViewCubeController.#interpolateExtent(t.start.bottom, -t.fit.halfHeight, -viewport.height / 2, eased);
651
+ t.camera.orthoTop = BabylonJSViewCubeController.#interpolateExtent(t.start.top, t.fit.halfHeight, viewport.height / 2, eased);
548
652
  }
549
653
  if (p === 1) {
550
654
  this.cancel();
@@ -605,22 +709,10 @@ export default class BabylonJSViewCubeController {
605
709
  const fov = Number.isFinite(camera.fov) && camera.fov > 0 && camera.fov < Math.PI ? camera.fov : 0.8;
606
710
  // An isolated virtual eye follows the scene camera's orientation/FOV, not its model zoom.
607
711
  const eyeDistance = 1.8 + 1.8 / Math.tan(fov / 2);
608
- const project = (vertex) => {
609
- const point = Vector3.TransformNormal(vertex, view);
610
- const scale = perspective ? eyeDistance / (eyeDistance + point.z * depthSign) : 1;
611
- return { x: 60 + point.x * 31 * scale, y: 60 - point.y * 31 * scale };
612
- };
613
- const draw = (surface) => {
614
- const normal = Vector3.TransformNormal(surface.normal, view);
615
- const shown = perspective ? -normal.z * depthSign * eyeDistance > 1.001 : normal.z * depthSign < -0.001;
616
- surface.polygon.style.display = shown ? "" : "none";
617
- const points = surface.vertices.map(project);
618
- surface.polygon.setAttribute("points", points.map((p) => `${p.x},${p.y}`).join(" "));
619
- return { shown, points };
620
- };
712
+ const projection = { view, perspective, depthSign, eyeDistance };
621
713
  if (geometryChanged) {
622
714
  for (const surface of this.#shell) {
623
- draw(surface);
715
+ BabylonJSViewCubeController.#drawSurface(surface, projection);
624
716
  }
625
717
  }
626
718
  const towardObserver = Vector3.TransformNormal(new Vector3(0, 0, -depthSign), this.#rotationMatrix(pose.rotation)).normalize();
@@ -632,7 +724,7 @@ export default class BabylonJSViewCubeController {
632
724
  // Draw full faces first, then the coplanar edge prisms and corner cubes.
633
725
  // Visibility belongs to each physical face, not the semantic preset direction.
634
726
  if (localeChanged) {
635
- this.#svg.setAttribute("aria-label", translate("prefViewer.viewCube.title", { locale }));
727
+ this.#svg.setAttribute("aria-label", I18n.translate("prefViewer.viewCube.title", { locale }));
636
728
  }
637
729
  for (const r of this.#regions) {
638
730
  const node = this.#nodes.get(r.id);
@@ -640,7 +732,7 @@ export default class BabylonJSViewCubeController {
640
732
  const names = parts
641
733
  .map((s, i) =>
642
734
  s
643
- ? translate(
735
+ ? I18n.translate(
644
736
  `prefViewer.viewCube.${
645
737
  [
646
738
  ["right", "left"],
@@ -656,7 +748,7 @@ export default class BabylonJSViewCubeController {
656
748
  let visible = false;
657
749
  const outlineSegments = [];
658
750
  for (const surface of node.surfaces) {
659
- const { shown, points } = geometryChanged ? draw(surface) : { shown: surface.polygon.style.display !== "none", points: [] };
751
+ const { shown, points } = geometryChanged ? BabylonJSViewCubeController.#drawSurface(surface, projection) : { shown: surface.polygon.style.display !== "none", points: [] };
660
752
  visible ||= shown;
661
753
  if (lockChanged || geometryChanged) {
662
754
  surface.polygon.style.pointerEvents = locked ? "none" : "visiblePainted";
@@ -130,6 +130,12 @@ export default class FileStorage {
130
130
  #maxQuotaEvictionRetries = 5; // max retries after quota error
131
131
  #lastCleanupAt = 0; // timestamp of last automatic cleanup run
132
132
 
133
+ /**
134
+ * Configures the file-cache database and object store; opening is deferred until storage is used.
135
+ * @public
136
+ * @param {string} [dbName="FilesDB"] - IndexedDB cache database name.
137
+ * @param {string} [osName="FilesObjectStore"] - Object store for cached files.
138
+ */
133
139
  constructor(dbName = "FilesDB", osName = "FilesObjectStore") {
134
140
  this.dbName = dbName; // database name
135
141
  this.osName = osName; // object store name used for file cache
@@ -447,6 +453,7 @@ export default class FileStorage {
447
453
  * @private
448
454
  * @param {IDBDatabase} db IndexedDB database instance
449
455
  * @param {String} osName Object store name
456
+ * @returns {void}
450
457
  */
451
458
  #createObjectStore(db, osName) {
452
459
  if (!db.objectStoreNames.contains(osName)) {
@@ -466,6 +473,12 @@ export default class FileStorage {
466
473
  const self = this;
467
474
  try {
468
475
  return await openDB(this.dbName, this.#dbVersion, {
476
+ /**
477
+ * Creates the configured cache object store during an IndexedDB schema upgrade.
478
+ * @private
479
+ * @param {IDBDatabase} db - Database being upgraded.
480
+ * @returns {void}
481
+ */
469
482
  upgrade(db) {
470
483
  self.#createObjectStore(db, self.osName);
471
484
  },