@preference-sl/pref-viewer 2.17.0-beta.0 → 2.17.0-beta.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/README.md +48 -16
- package/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/babylonjs-animation-controller.js +12 -163
- package/src/babylonjs-animation-opening-menu.js +8 -1
- package/src/babylonjs-animation-opening.js +21 -1
- package/src/babylonjs-animation-overlay-controller.js +145 -0
- package/src/babylonjs-controller.js +332 -177
- package/src/babylonjs-selection-controller.js +215 -0
- package/src/babylonjs-view-cube-controller.js +169 -77
- package/src/file-storage.js +13 -0
- package/src/gltf-storage.js +97 -0
- package/src/localization/i18n.js +147 -74
- package/src/material-utils.js +67 -60
- package/src/panzoom-controller.js +14 -1
- package/src/pref-viewer-2d.js +16 -4
- package/src/pref-viewer-3d-data.js +166 -0
- package/src/pref-viewer-3d.js +11 -0
- package/src/pref-viewer-dialog.js +7 -1
- package/src/pref-viewer-menu-3d.js +55 -35
- package/src/pref-viewer.js +56 -20
- package/src/render-math.js +130 -117
- package/src/svg-resolver.js +5 -4
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
158
|
+
this.#setHover(BabylonJSViewCubeController.#getRegionAt(event));
|
|
155
159
|
});
|
|
156
|
-
|
|
160
|
+
this.#addListener(this.#svg, "pointerleave", () => {
|
|
157
161
|
this.#pointer = null;
|
|
158
162
|
this.#setHover(null);
|
|
159
163
|
});
|
|
160
|
-
|
|
161
|
-
if (
|
|
164
|
+
this.#addListener(this.#svg, "pointerdown", (event) => {
|
|
165
|
+
if (BabylonJSViewCubeController.#getRegionAt(event)) {
|
|
162
166
|
event.stopPropagation();
|
|
163
167
|
}
|
|
164
168
|
});
|
|
165
|
-
|
|
166
|
-
const id =
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
BabylonJSViewCubeController.#addRegion(regions,
|
|
261
242
|
signs,
|
|
262
|
-
signs.map((s, i) =>
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
+
|
|
543
647
|
const viewport = this.#host.getViewport();
|
|
544
|
-
t.camera.orthoLeft =
|
|
545
|
-
t.camera.orthoRight =
|
|
546
|
-
t.camera.orthoBottom =
|
|
547
|
-
t.camera.orthoTop =
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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";
|
package/src/file-storage.js
CHANGED
|
@@ -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
|
},
|