@preference-sl/pref-viewer 2.16.2 → 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 +184 -22
- package/index.d.ts +11 -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-camera-framing.js +171 -0
- package/src/babylonjs-camera-zoom-controller.js +272 -0
- package/src/babylonjs-controller.js +640 -378
- package/src/babylonjs-selection-controller.js +215 -0
- package/src/babylonjs-view-cube-controller.js +810 -0
- package/src/file-storage.js +13 -0
- package/src/gltf-storage.js +97 -0
- package/src/localization/i18n.js +147 -74
- package/src/localization/translations.js +18 -0
- 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 +40 -5
- package/src/pref-viewer-dialog.js +7 -1
- package/src/pref-viewer-menu-3d.js +57 -37
- package/src/pref-viewer.js +155 -44
- package/src/render-math.js +130 -117
- package/src/styles.js +94 -3
- package/src/svg-resolver.js +5 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AssetContainer, Color3, HighlightLayer,
|
|
1
|
+
import { AssetContainer, Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
|
|
2
2
|
import { PrefViewerStyleVariables } from "./styles.js";
|
|
3
3
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
4
|
+
import BabylonJSAnimationOverlayController from "./babylonjs-animation-overlay-controller.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
|
|
@@ -48,7 +49,7 @@ export default class BabylonJSAnimationController {
|
|
|
48
49
|
#useHighlightLayer = true;
|
|
49
50
|
#lastHighlightedMeshId = null;
|
|
50
51
|
#lastHighlightedNodeIds = [];
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
// Static property to store global highlight enabled state
|
|
53
54
|
static #globalHighlightEnabled = true;
|
|
54
55
|
static #instanceId = 0;
|
|
@@ -70,6 +71,7 @@ export default class BabylonJSAnimationController {
|
|
|
70
71
|
/**
|
|
71
72
|
* Detects and stores animatable objects and animated nodes in the scene.
|
|
72
73
|
* @private
|
|
74
|
+
* @returns {void}
|
|
73
75
|
*/
|
|
74
76
|
#initializeAnimations() {
|
|
75
77
|
this.hideMenu(); // Clean up any existing menus
|
|
@@ -83,6 +85,7 @@ export default class BabylonJSAnimationController {
|
|
|
83
85
|
/**
|
|
84
86
|
* Collects the IDs of all nodes targeted by any animation group in the scene.
|
|
85
87
|
* @private
|
|
88
|
+
* @returns {void}
|
|
86
89
|
*/
|
|
87
90
|
#getAnimatedNodes() {
|
|
88
91
|
this.#assetContainer.animationGroups.forEach((animationGroup) => {
|
|
@@ -103,6 +106,7 @@ export default class BabylonJSAnimationController {
|
|
|
103
106
|
* @private
|
|
104
107
|
* @description
|
|
105
108
|
* Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
|
|
109
|
+
* @returns {void}
|
|
106
110
|
*/
|
|
107
111
|
#getOpeningAnimations() {
|
|
108
112
|
const openings = {};
|
|
@@ -131,7 +135,7 @@ export default class BabylonJSAnimationController {
|
|
|
131
135
|
* Finds the OpeningAnimation instance associated with a given node ID.
|
|
132
136
|
* @private
|
|
133
137
|
* @param {string} nodeId - The node identifier.
|
|
134
|
-
* @returns {OpeningAnimation|
|
|
138
|
+
* @returns {OpeningAnimation|undefined} The matching OpeningAnimation instance, or undefined when absent.
|
|
135
139
|
*/
|
|
136
140
|
#getOpeningAnimationByNode(nodeId) {
|
|
137
141
|
return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
|
|
@@ -172,163 +176,7 @@ export default class BabylonJSAnimationController {
|
|
|
172
176
|
* - Removes overlay when mouse moves away from highlighted meshes
|
|
173
177
|
*/
|
|
174
178
|
#addGroupOutlineOverlayForInstances(meshes) {
|
|
175
|
-
|
|
176
|
-
const OUTLINE_ENABLED = false; // Draw outline on individual meshes
|
|
177
|
-
const OUTLINE_COLOR = this.#highlightColor;
|
|
178
|
-
const OUTLINE_WIDTH = 1.5;
|
|
179
|
-
const OVERLAY_ENABLED = true; // Draw semi-transparent fill
|
|
180
|
-
const OVERLAY_COLOR = this.#highlightColor;
|
|
181
|
-
const OVERLAY_ALPHA = 0.25;
|
|
182
|
-
const RENDERING_GROUP_ID = 0; // MUST be > 0 to render on top of main scene
|
|
183
|
-
|
|
184
|
-
// Use UtilityLayerRenderer for rendering
|
|
185
|
-
const utilityLayer = new UtilityLayerRenderer(this.#scene, undefined, undefined);
|
|
186
|
-
const uScene = utilityLayer.utilityLayerScene;
|
|
187
|
-
|
|
188
|
-
// Cache map: baseMesh -> overlayBaseMesh to reuse for InstancedMeshes
|
|
189
|
-
const baseToOverlayBase = new Map();
|
|
190
|
-
|
|
191
|
-
// Pairs of { original mesh, overlay mesh } for tracking
|
|
192
|
-
const pairs = [];
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Gets the base mesh from either a regular mesh or an InstancedMesh
|
|
196
|
-
* @param {Mesh} mesh - The mesh to check
|
|
197
|
-
* @returns {Mesh} The base mesh or the mesh itself
|
|
198
|
-
*/
|
|
199
|
-
function getBaseMesh(mesh) {
|
|
200
|
-
return mesh instanceof InstancedMesh ? mesh.sourceMesh : mesh;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Creates or retrieves an overlay base mesh in the utility scene
|
|
205
|
-
* Reuses the same base for all instances of the same source mesh
|
|
206
|
-
* @param {Mesh} baseMesh - The base mesh to clone
|
|
207
|
-
* @returns {Mesh} The overlay base mesh
|
|
208
|
-
*/
|
|
209
|
-
function ensureOverlayBase(baseMesh) {
|
|
210
|
-
let overlayBase = baseToOverlayBase.get(baseMesh);
|
|
211
|
-
if (overlayBase) {
|
|
212
|
-
return overlayBase;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Clone the source mesh into the utility scene (deep clone materials/geometry)
|
|
216
|
-
overlayBase = baseMesh.clone(baseMesh.name + "_overlayBase", null, true, false);
|
|
217
|
-
overlayBase._scene = uScene;
|
|
218
|
-
overlayBase.isPickable = false;
|
|
219
|
-
overlayBase.setEnabled(true);
|
|
220
|
-
|
|
221
|
-
// Configure visual appearance for the overlay
|
|
222
|
-
overlayBase.renderOutline = OUTLINE_ENABLED; // Only draw outline if enabled
|
|
223
|
-
overlayBase.outlineColor = OUTLINE_COLOR;
|
|
224
|
-
overlayBase.outlineWidth = OUTLINE_WIDTH;
|
|
225
|
-
|
|
226
|
-
// Overlay color and transparency (semi-transparent colored fill)
|
|
227
|
-
overlayBase.renderOverlay = OVERLAY_ENABLED;
|
|
228
|
-
overlayBase.overlayColor = OVERLAY_COLOR;
|
|
229
|
-
overlayBase.overlayAlpha = OVERLAY_ALPHA;
|
|
230
|
-
|
|
231
|
-
overlayBase.visibility = 1;
|
|
232
|
-
|
|
233
|
-
// Ensure always-on-top rendering or not
|
|
234
|
-
overlayBase.renderingGroupId = RENDERING_GROUP_ID;
|
|
235
|
-
|
|
236
|
-
baseToOverlayBase.set(baseMesh, overlayBase);
|
|
237
|
-
return overlayBase;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Create overlay meshes for each input mesh
|
|
241
|
-
meshes.forEach((mesh) => {
|
|
242
|
-
// Skip meshes without geometry
|
|
243
|
-
if (!mesh.geometry && !mesh.getChildMeshes) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const baseMesh = getBaseMesh(mesh);
|
|
248
|
-
const overlayBase = ensureOverlayBase(baseMesh);
|
|
249
|
-
|
|
250
|
-
let overlay;
|
|
251
|
-
if (mesh instanceof InstancedMesh) {
|
|
252
|
-
// For instances, create a new instance of the overlay base
|
|
253
|
-
overlay = overlayBase.createInstance(mesh.name + "_overlayInst");
|
|
254
|
-
overlay._scene = uScene;
|
|
255
|
-
overlay.isPickable = false;
|
|
256
|
-
} else {
|
|
257
|
-
// For regular meshes, use the overlay base directly (avoids double cloning)
|
|
258
|
-
overlay = overlayBase;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Remove parent to allow independent world matrix control
|
|
262
|
-
overlay.parent = null;
|
|
263
|
-
overlay.renderingGroupId = RENDERING_GROUP_ID;
|
|
264
|
-
|
|
265
|
-
pairs.push({ original: mesh, overlay });
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Sync world matrix transformations
|
|
269
|
-
const pos = new Vector3();
|
|
270
|
-
const scl = new Vector3();
|
|
271
|
-
const rot = new Quaternion();
|
|
272
|
-
|
|
273
|
-
// Track all observers for cleanup
|
|
274
|
-
const observers = [];
|
|
275
|
-
|
|
276
|
-
// Subscribe to world matrix update events
|
|
277
|
-
// This is more efficient than updating every frame
|
|
278
|
-
pairs.forEach(({ original, overlay }) => {
|
|
279
|
-
// Skip if mesh has no geometry to sync
|
|
280
|
-
if (!original.getWorldMatrix) {
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Initial sync: copy the current world matrix
|
|
285
|
-
original.computeWorldMatrix(true);
|
|
286
|
-
original.getWorldMatrix().decompose(scl, rot, pos);
|
|
287
|
-
|
|
288
|
-
overlay.position.copyFrom(pos);
|
|
289
|
-
overlay.scaling.copyFrom(scl);
|
|
290
|
-
overlay.rotationQuaternion = overlay.rotationQuaternion || new Quaternion();
|
|
291
|
-
overlay.rotationQuaternion.copyFrom(rot);
|
|
292
|
-
overlay.computeWorldMatrix(true);
|
|
293
|
-
|
|
294
|
-
// Subscribe to world matrix changes
|
|
295
|
-
// Only updates when the original mesh's transformation actually changes
|
|
296
|
-
const obs = original.onAfterWorldMatrixUpdateObservable?.add(() => {
|
|
297
|
-
original.getWorldMatrix().decompose(scl, rot, pos);
|
|
298
|
-
|
|
299
|
-
overlay.position.copyFrom(pos);
|
|
300
|
-
overlay.scaling.copyFrom(scl);
|
|
301
|
-
overlay.rotationQuaternion.copyFrom(rot);
|
|
302
|
-
overlay.computeWorldMatrix(true);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
if (obs) {
|
|
306
|
-
observers.push({ mesh: original, observer: obs });
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// Cleanup API
|
|
311
|
-
return {
|
|
312
|
-
dispose() {
|
|
313
|
-
// Remove world matrix update observers
|
|
314
|
-
observers.forEach(({ mesh, observer }) => {
|
|
315
|
-
mesh.onAfterWorldMatrixUpdateObservable?.remove(observer);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Dispose overlay instances and meshes
|
|
319
|
-
pairs.forEach(({ original, overlay }) => {
|
|
320
|
-
overlay.dispose();
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// Dispose overlay base meshes
|
|
324
|
-
baseToOverlayBase.forEach((overlayBase) => {
|
|
325
|
-
overlayBase.dispose();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// Dispose utility layer scene/resources; without this each rebuild leaks GPU/JS resources.
|
|
329
|
-
utilityLayer.dispose();
|
|
330
|
-
},
|
|
331
|
-
};
|
|
179
|
+
return new BabylonJSAnimationOverlayController(this.#scene, meshes, this.#highlightColor);
|
|
332
180
|
}
|
|
333
181
|
|
|
334
182
|
/**
|
|
@@ -430,6 +278,7 @@ export default class BabylonJSAnimationController {
|
|
|
430
278
|
* Cleans up the highlight layer, animation menu, and internal animation/node lists.
|
|
431
279
|
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
432
280
|
* @public
|
|
281
|
+
* @returns {void}
|
|
433
282
|
*/
|
|
434
283
|
dispose() {
|
|
435
284
|
if (this.#highlightLayer) {
|
|
@@ -476,14 +325,14 @@ export default class BabylonJSAnimationController {
|
|
|
476
325
|
}
|
|
477
326
|
return { changed: false, highlighted: false };
|
|
478
327
|
}
|
|
479
|
-
|
|
328
|
+
|
|
480
329
|
let changed = false;
|
|
481
330
|
let highlighted = false;
|
|
482
331
|
|
|
483
332
|
// Check if we're hovering the same mesh to avoid recreating overlays/highlights
|
|
484
333
|
const pickedMeshId = !pickingInfo?.hit || !pickingInfo?.pickedMesh?.id ? null : pickingInfo.pickedMesh.id;
|
|
485
334
|
const pickedMesh = pickingInfo?.pickedMesh ? pickingInfo.pickedMesh : null;
|
|
486
|
-
|
|
335
|
+
|
|
487
336
|
if (this.#lastHighlightedMeshId === pickedMeshId) {
|
|
488
337
|
// No need to update if hovering the same mesh
|
|
489
338
|
if (this.#lastHighlightedMeshId !== null) {
|
|
@@ -651,7 +500,7 @@ export default class BabylonJSAnimationController {
|
|
|
651
500
|
this.hideMenu();
|
|
652
501
|
return;
|
|
653
502
|
}
|
|
654
|
-
|
|
503
|
+
|
|
655
504
|
const openingAnimations = [];
|
|
656
505
|
nodeIds.forEach((nodeId) => {
|
|
657
506
|
const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
|
|
@@ -117,11 +117,12 @@ export default class OpeningAnimationMenu {
|
|
|
117
117
|
* Sets button appearance and attaches the callback for user interaction.
|
|
118
118
|
* @private
|
|
119
119
|
* @param {string} name - Button identifier.
|
|
120
|
-
* @param {string}
|
|
120
|
+
* @param {string} imageSVG - SVG markup for the button icon.
|
|
121
121
|
* @param {boolean} enabled - Whether the button is enabled.
|
|
122
122
|
* @param {boolean} active - Whether the button is visually active.
|
|
123
123
|
* @param {boolean} visible - Whether the button is visible.
|
|
124
124
|
* @param {function} callback - Callback to invoke on button click.
|
|
125
|
+
* @returns {void}
|
|
125
126
|
*/
|
|
126
127
|
#addButton(name, imageSVG, enabled = true, active = false, visible = true, callback) {
|
|
127
128
|
const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
|
|
@@ -183,6 +184,7 @@ export default class OpeningAnimationMenu {
|
|
|
183
184
|
/**
|
|
184
185
|
* Creates all control buttons and sets their initial states.
|
|
185
186
|
* @private
|
|
187
|
+
* @returns {void}
|
|
186
188
|
*/
|
|
187
189
|
#createButtons() {
|
|
188
190
|
const buttonsState = this.#getButtonsState();
|
|
@@ -198,6 +200,7 @@ export default class OpeningAnimationMenu {
|
|
|
198
200
|
/**
|
|
199
201
|
* Creates and configures the animation progress slider.
|
|
200
202
|
* @private
|
|
203
|
+
* @returns {void}
|
|
201
204
|
*/
|
|
202
205
|
#createSlider() {
|
|
203
206
|
this.#slider = document.createElement("input");
|
|
@@ -216,6 +219,7 @@ export default class OpeningAnimationMenu {
|
|
|
216
219
|
/**
|
|
217
220
|
* Initializes and renders the menu UI, including buttons and slider.
|
|
218
221
|
* @private
|
|
222
|
+
* @returns {void}
|
|
219
223
|
*/
|
|
220
224
|
#createMenu() {
|
|
221
225
|
this.#containerMain = document.createElement("div");
|
|
@@ -334,6 +338,7 @@ export default class OpeningAnimationMenu {
|
|
|
334
338
|
* @param {boolean} enabled
|
|
335
339
|
* @param {boolean} active
|
|
336
340
|
* @param {boolean} visible
|
|
341
|
+
* @returns {void}
|
|
337
342
|
*/
|
|
338
343
|
#setButtonState(name, enabled, active, visible = true) {
|
|
339
344
|
const button = this.#getButtonByName(name);
|
|
@@ -350,6 +355,7 @@ export default class OpeningAnimationMenu {
|
|
|
350
355
|
/**
|
|
351
356
|
* Updates all loop control buttons according to current loop mode.
|
|
352
357
|
* @private
|
|
358
|
+
* @returns {void}
|
|
353
359
|
*/
|
|
354
360
|
#setLoopButtonsState() {
|
|
355
361
|
const buttonsState = this.#getLoopButtonsState();
|
|
@@ -360,6 +366,7 @@ export default class OpeningAnimationMenu {
|
|
|
360
366
|
/**
|
|
361
367
|
* Updates all playback control buttons according to current animation state.
|
|
362
368
|
* @private
|
|
369
|
+
* @returns {void}
|
|
363
370
|
*/
|
|
364
371
|
#setPlayerButtonsState() {
|
|
365
372
|
const buttonsState = this.#getPlayerButtonsState();
|
|
@@ -108,6 +108,11 @@ export default class OpeningAnimation {
|
|
|
108
108
|
this.#goToClosed();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Binds opening and closing completion handlers once for stable subscription and removal.
|
|
113
|
+
* @private
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
111
116
|
#bindHandlers() {
|
|
112
117
|
this.#handlers.onOpened = this.#onOpened.bind(this);
|
|
113
118
|
this.#handlers.onClosed = this.#onClosed.bind(this);
|
|
@@ -118,6 +123,7 @@ export default class OpeningAnimation {
|
|
|
118
123
|
* Collects node IDs affected by the opening and closing animation groups.
|
|
119
124
|
* Populates the #nodes array with unique node identifiers.
|
|
120
125
|
* @private
|
|
126
|
+
* @returns {void}
|
|
121
127
|
*/
|
|
122
128
|
#getNodesFromAnimationGroups() {
|
|
123
129
|
[this.#openAnimation, this.#closeAnimation].forEach((animationGroup) => {
|
|
@@ -132,6 +138,7 @@ export default class OpeningAnimation {
|
|
|
132
138
|
/**
|
|
133
139
|
* Handles the end of the opening animation group.
|
|
134
140
|
* @private
|
|
141
|
+
* @returns {void}
|
|
135
142
|
*/
|
|
136
143
|
#onOpened() {
|
|
137
144
|
this.#goToOpened(true);
|
|
@@ -140,6 +147,7 @@ export default class OpeningAnimation {
|
|
|
140
147
|
/**
|
|
141
148
|
* Handles the end of the closing animation group.
|
|
142
149
|
* @private
|
|
150
|
+
* @returns {void}
|
|
143
151
|
*/
|
|
144
152
|
#onClosed() {
|
|
145
153
|
this.#goToClosed(true);
|
|
@@ -149,6 +157,7 @@ export default class OpeningAnimation {
|
|
|
149
157
|
* Moves the animation to the fully opened state, optionally looping to close.
|
|
150
158
|
* @private
|
|
151
159
|
* @param {boolean} useLoop - If true, starts closing animation after opening.
|
|
160
|
+
* @returns {void}
|
|
152
161
|
*/
|
|
153
162
|
#goToOpened(useLoop = false) {
|
|
154
163
|
this.#lastPausedFrame = this.#endFrame;
|
|
@@ -178,6 +187,7 @@ export default class OpeningAnimation {
|
|
|
178
187
|
* Moves the animation to the fully closed state, optionally looping to open.
|
|
179
188
|
* @private
|
|
180
189
|
* @param {boolean} useLoop - If true, starts opening animation after closing.
|
|
190
|
+
* @returns {void}
|
|
181
191
|
*/
|
|
182
192
|
#goToClosed(useLoop = false) {
|
|
183
193
|
this.#lastPausedFrame = this.#startFrame;
|
|
@@ -207,6 +217,7 @@ export default class OpeningAnimation {
|
|
|
207
217
|
* Moves the animation to a specific frame and pauses.
|
|
208
218
|
* @private
|
|
209
219
|
* @param {number} frame - The frame to go to and pause.
|
|
220
|
+
* @returns {void}
|
|
210
221
|
*/
|
|
211
222
|
#goToFrameAndPause(frame) {
|
|
212
223
|
if (!frame) {
|
|
@@ -350,6 +361,7 @@ export default class OpeningAnimation {
|
|
|
350
361
|
/**
|
|
351
362
|
* Updates the slider value in the animation control menu to match the current progress.
|
|
352
363
|
* @private
|
|
364
|
+
* @returns {void}
|
|
353
365
|
*/
|
|
354
366
|
#updateControlsSlider() {
|
|
355
367
|
if (!this.isControlsVisible()) {
|
|
@@ -362,6 +374,7 @@ export default class OpeningAnimation {
|
|
|
362
374
|
* Broadcasts the latest animation state and, when the menu is visible, syncs its controls
|
|
363
375
|
* (buttons and progress slider) with the current state/progress values.
|
|
364
376
|
* @private
|
|
377
|
+
* @returns {void}
|
|
365
378
|
*/
|
|
366
379
|
#updateControls() {
|
|
367
380
|
this.#notifyStateChange();
|
|
@@ -412,6 +425,7 @@ export default class OpeningAnimation {
|
|
|
412
425
|
/**
|
|
413
426
|
* Starts the opening animation.
|
|
414
427
|
* @public
|
|
428
|
+
* @returns {void}
|
|
415
429
|
*/
|
|
416
430
|
playOpen() {
|
|
417
431
|
if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
|
|
@@ -437,6 +451,7 @@ export default class OpeningAnimation {
|
|
|
437
451
|
/**
|
|
438
452
|
* Starts the closing animation.
|
|
439
453
|
* @public
|
|
454
|
+
* @returns {void}
|
|
440
455
|
*/
|
|
441
456
|
playClose() {
|
|
442
457
|
if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
|
|
@@ -462,6 +477,7 @@ export default class OpeningAnimation {
|
|
|
462
477
|
/**
|
|
463
478
|
* Pauses the current animation.
|
|
464
479
|
* @public
|
|
480
|
+
* @returns {void}
|
|
465
481
|
*/
|
|
466
482
|
pause() {
|
|
467
483
|
if (this.#state === OpeningAnimation.states.opening) {
|
|
@@ -479,6 +495,7 @@ export default class OpeningAnimation {
|
|
|
479
495
|
/**
|
|
480
496
|
* Moves animation to the fully opened state.
|
|
481
497
|
* @public
|
|
498
|
+
* @returns {void}
|
|
482
499
|
*/
|
|
483
500
|
goToOpened() {
|
|
484
501
|
this.#goToOpened(false);
|
|
@@ -487,6 +504,7 @@ export default class OpeningAnimation {
|
|
|
487
504
|
/**
|
|
488
505
|
* Moves animation to the fully closed state.
|
|
489
506
|
* @public
|
|
507
|
+
* @returns {void}
|
|
490
508
|
*/
|
|
491
509
|
goToClosed() {
|
|
492
510
|
this.#goToClosed(false);
|
|
@@ -498,6 +516,7 @@ export default class OpeningAnimation {
|
|
|
498
516
|
* @public
|
|
499
517
|
* @param {HTMLCanvasElement} canvas - The canvas element for rendering.
|
|
500
518
|
* @param {Array<OpeningAnimation>} openingAnimations - Array of OpeningAnimation instances to manage.
|
|
519
|
+
* @returns {void}
|
|
501
520
|
*/
|
|
502
521
|
showControls(canvas, openingAnimations = []) {
|
|
503
522
|
const controlCallbacks = {
|
|
@@ -562,6 +581,7 @@ export default class OpeningAnimation {
|
|
|
562
581
|
/**
|
|
563
582
|
* Hides the animation control menu and removes observers.
|
|
564
583
|
* @public
|
|
584
|
+
* @returns {void}
|
|
565
585
|
*/
|
|
566
586
|
hideControls() {
|
|
567
587
|
if (!this.isControlsVisible()) {
|
|
@@ -590,7 +610,7 @@ export default class OpeningAnimation {
|
|
|
590
610
|
/**
|
|
591
611
|
* Returns the current animation state.
|
|
592
612
|
* @public
|
|
593
|
-
* @returns {number}
|
|
613
|
+
* @returns {number} 0 paused, 1 closed, 2 opened, 3 opening or 4 closing.
|
|
594
614
|
*/
|
|
595
615
|
get state() {
|
|
596
616
|
return this.#state;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { InstancedMesh, Mesh, Quaternion, UtilityLayerRenderer, Vector3 } from "@babylonjs/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Owns one animated-mesh overlay group, its utility scene and transform observers.
|
|
5
|
+
* Groups retain independent caches and release only their own rendering resources.
|
|
6
|
+
* @class BabylonJSAnimationOverlayController
|
|
7
|
+
*/
|
|
8
|
+
export default class BabylonJSAnimationOverlayController {
|
|
9
|
+
#utilityLayer;
|
|
10
|
+
#color;
|
|
11
|
+
#baseToOverlayBase = new Map();
|
|
12
|
+
#pairs = [];
|
|
13
|
+
#observers = [];
|
|
14
|
+
#position = new Vector3();
|
|
15
|
+
#scale = new Vector3();
|
|
16
|
+
#rotation = new Quaternion();
|
|
17
|
+
#disposed = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates presentation meshes and follows their original world transforms.
|
|
21
|
+
* @public
|
|
22
|
+
* @param {import("@babylonjs/core").Scene} scene - Host scene used by the utility renderer.
|
|
23
|
+
* @param {Array<import("@babylonjs/core").AbstractMesh>} meshes - Original meshes to highlight.
|
|
24
|
+
* @param {import("@babylonjs/core").Color3} color - Highlight color for the group.
|
|
25
|
+
*/
|
|
26
|
+
constructor(scene, meshes, color) {
|
|
27
|
+
this.#color = color;
|
|
28
|
+
this.#utilityLayer = new UtilityLayerRenderer(scene, undefined, undefined);
|
|
29
|
+
for (const mesh of meshes) {
|
|
30
|
+
this.#createOverlay(mesh);
|
|
31
|
+
}
|
|
32
|
+
for (const pair of this.#pairs) {
|
|
33
|
+
if (!pair.original.getWorldMatrix) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
pair.original.computeWorldMatrix(true);
|
|
37
|
+
this.#syncPair(pair);
|
|
38
|
+
const observer = pair.original.onAfterWorldMatrixUpdateObservable?.add(() => this.#syncPair(pair));
|
|
39
|
+
if (observer) {
|
|
40
|
+
this.#observers.push({ mesh: pair.original, observer });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the source geometry mesh for an instance or returns a regular mesh unchanged.
|
|
47
|
+
* @private
|
|
48
|
+
* @static
|
|
49
|
+
* @param {import("@babylonjs/core").AbstractMesh} mesh - Original mesh.
|
|
50
|
+
* @returns {import("@babylonjs/core").Mesh} Mesh supplying the overlay geometry.
|
|
51
|
+
*/
|
|
52
|
+
static #getBaseMesh(mesh) {
|
|
53
|
+
return mesh instanceof InstancedMesh ? mesh.sourceMesh : mesh;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates or reuses the group's presentation base for a source mesh.
|
|
58
|
+
* @private
|
|
59
|
+
* @param {import("@babylonjs/core").Mesh} baseMesh - Source mesh to clone.
|
|
60
|
+
* @returns {import("@babylonjs/core").Mesh} Cached overlay base.
|
|
61
|
+
*/
|
|
62
|
+
#ensureOverlayBase(baseMesh) {
|
|
63
|
+
let overlayBase = this.#baseToOverlayBase.get(baseMesh);
|
|
64
|
+
if (overlayBase) {
|
|
65
|
+
return overlayBase;
|
|
66
|
+
}
|
|
67
|
+
// Register the clone in its owning scene from creation; changing _scene later
|
|
68
|
+
// leaves Babylon's node and root-node registries in the original scene.
|
|
69
|
+
overlayBase = new Mesh(baseMesh.name + "_overlayBase", this.#utilityLayer.utilityLayerScene, null, baseMesh, true, false);
|
|
70
|
+
overlayBase.isPickable = false;
|
|
71
|
+
overlayBase.isVisible = false;
|
|
72
|
+
overlayBase.setEnabled(true);
|
|
73
|
+
overlayBase.renderOutline = false;
|
|
74
|
+
overlayBase.outlineColor = this.#color;
|
|
75
|
+
overlayBase.outlineWidth = 1.5;
|
|
76
|
+
overlayBase.renderOverlay = true;
|
|
77
|
+
overlayBase.overlayColor = this.#color;
|
|
78
|
+
overlayBase.overlayAlpha = 0.25;
|
|
79
|
+
overlayBase.visibility = 1;
|
|
80
|
+
overlayBase.renderingGroupId = 0;
|
|
81
|
+
this.#baseToOverlayBase.set(baseMesh, overlayBase);
|
|
82
|
+
return overlayBase;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Adds a presentation mesh or an instance of a cached presentation base.
|
|
87
|
+
* @private
|
|
88
|
+
* @param {import("@babylonjs/core").AbstractMesh} mesh - Original mesh to present.
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
91
|
+
#createOverlay(mesh) {
|
|
92
|
+
if (!mesh.geometry && !mesh.getChildMeshes) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const baseMesh = BabylonJSAnimationOverlayController.#getBaseMesh(mesh);
|
|
96
|
+
const overlayBase = this.#ensureOverlayBase(baseMesh);
|
|
97
|
+
const overlay = mesh instanceof InstancedMesh ? overlayBase.createInstance(mesh.name + "_overlayInst") : overlayBase;
|
|
98
|
+
if (mesh instanceof InstancedMesh) {
|
|
99
|
+
overlay.isPickable = false;
|
|
100
|
+
} else {
|
|
101
|
+
overlay.isVisible = true;
|
|
102
|
+
}
|
|
103
|
+
overlay.parent = null;
|
|
104
|
+
overlay.renderingGroupId = 0;
|
|
105
|
+
this.#pairs.push({ original: mesh, overlay });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Copies an original world transform to its overlay using reusable group-local scratch vectors.
|
|
110
|
+
* @private
|
|
111
|
+
* @param {{original:object,overlay:object}} pair - Original and presentation mesh pair.
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
#syncPair(pair) {
|
|
115
|
+
pair.original.getWorldMatrix().decompose(this.#scale, this.#rotation, this.#position);
|
|
116
|
+
pair.overlay.position.copyFrom(this.#position);
|
|
117
|
+
pair.overlay.scaling.copyFrom(this.#scale);
|
|
118
|
+
pair.overlay.rotationQuaternion = pair.overlay.rotationQuaternion || new Quaternion();
|
|
119
|
+
pair.overlay.rotationQuaternion.copyFrom(this.#rotation);
|
|
120
|
+
pair.overlay.computeWorldMatrix(true);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Removes transform observers and disposes each owned mesh and the utility renderer once.
|
|
125
|
+
* @public
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
dispose() {
|
|
129
|
+
if (this.#disposed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.#disposed = true;
|
|
133
|
+
for (const { mesh, observer } of this.#observers) {
|
|
134
|
+
mesh.onAfterWorldMatrixUpdateObservable?.remove(observer);
|
|
135
|
+
}
|
|
136
|
+
const overlays = new Set([...this.#pairs.map(pair => pair.overlay), ...this.#baseToOverlayBase.values()]);
|
|
137
|
+
for (const overlay of overlays) {
|
|
138
|
+
overlay.dispose();
|
|
139
|
+
}
|
|
140
|
+
this.#utilityLayer.dispose();
|
|
141
|
+
this.#observers.length = 0;
|
|
142
|
+
this.#pairs.length = 0;
|
|
143
|
+
this.#baseToOverlayBase.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|