@preference-sl/pref-viewer 2.13.0-beta.7 → 2.13.0-beta.8
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/package.json
CHANGED
|
@@ -6,22 +6,32 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
|
6
6
|
* BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
|
|
7
7
|
*
|
|
8
8
|
* Summary:
|
|
9
|
-
* This class detects
|
|
9
|
+
* This class detects and groups opening/closing animations, manages hover highlighting for animated nodes,
|
|
10
|
+
* exposes whether interactive animations are available, and coordinates the contextual animation menu.
|
|
10
11
|
*
|
|
11
12
|
* Key features:
|
|
12
13
|
* - Detects and groups opening/closing animations in the scene.
|
|
13
14
|
* - Tracks animated transformation nodes and their relationships to meshes.
|
|
15
|
+
* - Exposes animation availability through `hasAnimations()`.
|
|
14
16
|
* - Highlights animated nodes and their child meshes on pointer hover.
|
|
17
|
+
* - Avoids redundant highlight rebuilds using cached mesh/node IDs and order-insensitive node-id comparison.
|
|
18
|
+
* - Reports highlight-state deltas from `highlightMeshes()` so callers can react efficiently.
|
|
15
19
|
* - Displays and disposes the animation control menu for animated nodes.
|
|
16
|
-
* - Provides public API for highlighting, showing the animation menu, and disposing resources.
|
|
20
|
+
* - Provides public API for availability checks, highlighting, showing the animation menu, and disposing resources.
|
|
17
21
|
* - Cleans up all resources and observers to prevent memory leaks.
|
|
18
22
|
*
|
|
19
23
|
* Public Methods:
|
|
20
24
|
* - dispose(): Disposes all resources managed by the animation controller.
|
|
21
|
-
* -
|
|
25
|
+
* - hasAnimations(): Returns whether opening/closing animations are available.
|
|
26
|
+
* - highlightMeshes(pickingInfo): Updates hover highlighting and returns `{ changed, highlighted }`.
|
|
22
27
|
* - hideMenu(): Hides and disposes the animation control menu if it exists.
|
|
23
28
|
* - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
|
|
24
29
|
*
|
|
30
|
+
* Private Methods (high level):
|
|
31
|
+
* - #haveSameNodeIds(arr1, arr2): Compares hovered animated node sets ignoring order.
|
|
32
|
+
* - #addHighlight(nodeIds): Applies highlighting with HighlightLayer or overlay meshes.
|
|
33
|
+
* - #removeHighlight(): Clears current highlights and cached hover state.
|
|
34
|
+
*
|
|
25
35
|
* @class
|
|
26
36
|
*/
|
|
27
37
|
export default class BabylonJSAnimationController {
|
|
@@ -37,6 +47,7 @@ export default class BabylonJSAnimationController {
|
|
|
37
47
|
#overlayLayer = null;
|
|
38
48
|
#useHighlightLayer = false; // Set to true to use HighlightLayer (better performance) and false to use overlay meshes (UtilityLayerRenderer - always on top)
|
|
39
49
|
#lastHighlightedMeshId = null; // Cache to avoid redundant highlight updates
|
|
50
|
+
#lastHighlightedNodeIds = []; // Cache to avoid redundant highlight updates
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
53
|
* Creates a new BabylonJSAnimationController for a Babylon.js scene.
|
|
@@ -126,15 +137,18 @@ export default class BabylonJSAnimationController {
|
|
|
126
137
|
* @returns {Array<string>} Array of animated node IDs associated with the mesh.
|
|
127
138
|
*/
|
|
128
139
|
#getNodesAnimatedByMesh(mesh) {
|
|
129
|
-
let
|
|
140
|
+
let nodeIds = [];
|
|
141
|
+
if (!mesh || !mesh.id) {
|
|
142
|
+
return nodeIds;
|
|
143
|
+
}
|
|
130
144
|
let node = mesh;
|
|
131
145
|
while (node.parent !== null) {
|
|
132
146
|
node = node.parent;
|
|
133
|
-
if (this.#animatedNodes.includes(node.id) && !
|
|
134
|
-
|
|
147
|
+
if (this.#animatedNodes.includes(node.id) && !nodeIds.includes(node.id)) {
|
|
148
|
+
nodeIds.push(node.id);
|
|
135
149
|
}
|
|
136
150
|
}
|
|
137
|
-
return
|
|
151
|
+
return nodeIds;
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
/**
|
|
@@ -308,62 +322,60 @@ export default class BabylonJSAnimationController {
|
|
|
308
322
|
}
|
|
309
323
|
|
|
310
324
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
325
|
+
* Compares two node ID arrays and checks whether they contain the same values,
|
|
326
|
+
* regardless of element order.
|
|
327
|
+
* @private
|
|
328
|
+
* @param {string[]} arr1 - First node ID array.
|
|
329
|
+
* @param {string[]} arr2 - Second node ID array.
|
|
330
|
+
* @returns {boolean} True when both arrays contain the same node IDs; otherwise false.
|
|
314
331
|
*/
|
|
332
|
+
#haveSameNodeIds(arr1, arr2) {
|
|
333
|
+
if (arr1.length !== arr2.length) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const sortedArr1 = [...arr1].sort();
|
|
337
|
+
const sortedArr2 = [...arr2].sort();
|
|
338
|
+
return sortedArr1.every((value, index) => value === sortedArr2[index]);
|
|
339
|
+
}
|
|
315
340
|
|
|
316
341
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* @
|
|
342
|
+
* Clears the current highlight state from either HighlightLayer or overlay meshes.
|
|
343
|
+
* Also resets cached mesh/node tracking used to avoid redundant highlight updates.
|
|
344
|
+
* @private
|
|
345
|
+
* @returns {void}
|
|
321
346
|
*/
|
|
322
|
-
|
|
323
|
-
if (this.#
|
|
324
|
-
this.#highlightLayer
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
347
|
+
#removeHighlight() {
|
|
348
|
+
if (this.#useHighlightLayer) {
|
|
349
|
+
if (this.#highlightLayer) {
|
|
350
|
+
this.#highlightLayer.removeAllMeshes();
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
if (this.#overlayLayer) {
|
|
354
|
+
this.#overlayLayer.dispose();
|
|
355
|
+
this.#overlayLayer = null;
|
|
356
|
+
}
|
|
331
357
|
}
|
|
332
|
-
this
|
|
333
|
-
this.#
|
|
334
|
-
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
335
|
-
this.#openingAnimations = [];
|
|
358
|
+
this.#lastHighlightedMeshId = null;
|
|
359
|
+
this.#lastHighlightedNodeIds = [];
|
|
336
360
|
}
|
|
337
361
|
|
|
338
362
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
363
|
+
* Applies highlighting for all meshes belonging to the provided animated node IDs.
|
|
364
|
+
* Depending on configuration, it uses Babylon's HighlightLayer or overlay meshes rendered
|
|
365
|
+
* in a utility layer for always-on-top outlines/fills.
|
|
366
|
+
* @private
|
|
367
|
+
* @param {string[]} nodeIds - Animated node IDs whose child meshes should be highlighted.
|
|
368
|
+
* @returns {void}
|
|
342
369
|
*/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return; // No need to update if hovering the same mesh
|
|
370
|
+
#addHighlight(nodeIds) {
|
|
371
|
+
this.#removeHighlight();
|
|
372
|
+
if (!nodeIds.length) {
|
|
373
|
+
return;
|
|
348
374
|
}
|
|
349
|
-
this.#lastHighlightedMeshId = pickedMeshId;
|
|
350
|
-
|
|
351
375
|
if (this.#useHighlightLayer) {
|
|
352
376
|
if (!this.#highlightLayer) {
|
|
353
377
|
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
354
378
|
}
|
|
355
|
-
|
|
356
|
-
this.#highlightLayer.removeAllMeshes();
|
|
357
|
-
|
|
358
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
363
|
-
if (!nodeIds.length) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
379
|
const transformNodes = [];
|
|
368
380
|
nodeIds.forEach((nodeId) => {
|
|
369
381
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -383,20 +395,6 @@ export default class BabylonJSAnimationController {
|
|
|
383
395
|
}
|
|
384
396
|
});
|
|
385
397
|
} else {
|
|
386
|
-
if (this.#overlayLayer) {
|
|
387
|
-
this.#overlayLayer.dispose();
|
|
388
|
-
this.#overlayLayer = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
396
|
-
if (!nodeIds.length) {
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
398
|
const meshes = [];
|
|
401
399
|
nodeIds.forEach((nodeId) => {
|
|
402
400
|
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
@@ -408,6 +406,99 @@ export default class BabylonJSAnimationController {
|
|
|
408
406
|
|
|
409
407
|
this.#overlayLayer = this.#addGroupOutlineOverlayForInstances(meshes);
|
|
410
408
|
}
|
|
409
|
+
this.#lastHighlightedNodeIds = nodeIds;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* ---------------------------
|
|
414
|
+
* Public methods
|
|
415
|
+
* ---------------------------
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Disposes all resources managed by the animation controller.
|
|
420
|
+
* Cleans up the highlight layer, animation menu, and internal animation/node lists.
|
|
421
|
+
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
422
|
+
* @public
|
|
423
|
+
*/
|
|
424
|
+
dispose() {
|
|
425
|
+
if (this.#highlightLayer) {
|
|
426
|
+
this.#highlightLayer.removeAllMeshes();
|
|
427
|
+
this.#highlightLayer.dispose();
|
|
428
|
+
this.#highlightLayer = null;
|
|
429
|
+
}
|
|
430
|
+
if (this.#overlayLayer) {
|
|
431
|
+
this.#overlayLayer.dispose();
|
|
432
|
+
this.#overlayLayer = null;
|
|
433
|
+
}
|
|
434
|
+
this.hideMenu();
|
|
435
|
+
this.#animatedNodes = [];
|
|
436
|
+
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
437
|
+
this.#openingAnimations = [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Indicates whether the current asset container exposes at least one opening/closing animation pair.
|
|
442
|
+
* @public
|
|
443
|
+
* @returns {boolean} True when interactive opening animations are available; otherwise false.
|
|
444
|
+
*/
|
|
445
|
+
hasAnimations() {
|
|
446
|
+
return this.#openingAnimations.length > 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Updates hover highlighting for meshes under animated nodes.
|
|
451
|
+
* Uses cached mesh/node state to avoid rebuilding highlight layers when the effective
|
|
452
|
+
* highlighted node set has not changed.
|
|
453
|
+
* @public
|
|
454
|
+
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
455
|
+
* @returns {{changed:boolean, highlighted:boolean}}
|
|
456
|
+
* Returns whether highlight visuals changed in this call and whether any mesh remains highlighted.
|
|
457
|
+
*/
|
|
458
|
+
highlightMeshes(pickingInfo) {
|
|
459
|
+
let changed = false;
|
|
460
|
+
let highlighted = false;
|
|
461
|
+
|
|
462
|
+
// Check if we're hovering the same mesh to avoid recreating overlays/highlights
|
|
463
|
+
const pickedMeshId = !pickingInfo?.hit || !pickingInfo?.pickedMesh?.id ? null : pickingInfo.pickedMesh.id;
|
|
464
|
+
const pickedMesh = pickingInfo?.pickedMesh ? pickingInfo.pickedMesh : null;
|
|
465
|
+
|
|
466
|
+
if (this.#lastHighlightedMeshId === pickedMeshId) {
|
|
467
|
+
// No need to update if hovering the same mesh
|
|
468
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
469
|
+
changed = false;
|
|
470
|
+
highlighted = true;
|
|
471
|
+
} else {
|
|
472
|
+
changed = false;
|
|
473
|
+
highlighted = false;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickedMesh);
|
|
477
|
+
if (!nodeIds.length) {
|
|
478
|
+
if (this.#lastHighlightedMeshId !== null) {
|
|
479
|
+
this.#removeHighlight();
|
|
480
|
+
changed = true;
|
|
481
|
+
highlighted = false;
|
|
482
|
+
} else {
|
|
483
|
+
changed = false;
|
|
484
|
+
highlighted = false;
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
if (this.#haveSameNodeIds(nodeIds, this.#lastHighlightedNodeIds)) {
|
|
488
|
+
// No need to update if hovering a mesh under the same animated nodes
|
|
489
|
+
changed = false;
|
|
490
|
+
highlighted = true;
|
|
491
|
+
} else {
|
|
492
|
+
// Hovering a different mesh or same mesh under different animated nodes - update highlight
|
|
493
|
+
this.#addHighlight(nodeIds);
|
|
494
|
+
changed = true;
|
|
495
|
+
highlighted = true;
|
|
496
|
+
}
|
|
497
|
+
this.#lastHighlightedMeshId = pickedMeshId;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { changed: changed, highlighted: highlighted };
|
|
411
502
|
}
|
|
412
503
|
|
|
413
504
|
/**
|
|
@@ -8,6 +8,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
8
8
|
* - Tracks animation state (paused, closed, opened, opening, closing).
|
|
9
9
|
* - Synchronizes animation progress and UI controls.
|
|
10
10
|
* - Handles loop mode and progress threshold logic.
|
|
11
|
+
* - Dispatches `prefviewer-animation-update` events with animation state snapshots.
|
|
11
12
|
* - Provides methods for play, pause, go to opened/closed, and progress control.
|
|
12
13
|
* - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
|
|
13
14
|
*
|
|
@@ -36,9 +37,11 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
36
37
|
* - #getCurrentFrame(): Gets the current frame based on state.
|
|
37
38
|
* - #getFrameFromProgress(progress): Calculates frame from progress value.
|
|
38
39
|
* - #getProgress(): Calculates progress (0-1) from current frame.
|
|
40
|
+
* - #getPrefViewer3DComponent(): Resolves and caches the nearest `pref-viewer-3d` host element.
|
|
39
41
|
* - #checkProgress(progress): Applies threshold logic to progress.
|
|
42
|
+
* - #notifyStateChange(): Emits `prefviewer-animation-update` with state/progress/loop metadata.
|
|
40
43
|
* - #updateControlsSlider(): Updates the slider in the control menu.
|
|
41
|
-
* - #updateControls():
|
|
44
|
+
* - #updateControls(): Emits state updates and syncs controls when the menu is visible.
|
|
42
45
|
*/
|
|
43
46
|
export default class OpeningAnimation {
|
|
44
47
|
static states = {
|
|
@@ -54,6 +57,7 @@ export default class OpeningAnimation {
|
|
|
54
57
|
#closeAnimation = null;
|
|
55
58
|
#nodes = [];
|
|
56
59
|
#menu = null;
|
|
60
|
+
#prefViewer3D = undefined; // Reference to parent custom elements for event dispatching
|
|
57
61
|
|
|
58
62
|
#state = OpeningAnimation.states.closed;
|
|
59
63
|
#lastPausedFrame = 0;
|
|
@@ -271,6 +275,29 @@ export default class OpeningAnimation {
|
|
|
271
275
|
return progress;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the animation canvas.
|
|
280
|
+
* @private
|
|
281
|
+
* @returns {void}
|
|
282
|
+
*/
|
|
283
|
+
#getPrefViewer3DComponent() {
|
|
284
|
+
if (this.#prefViewer3D !== undefined) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const scene = this.#openAnimation?.getScene?.();
|
|
289
|
+
const engine = scene?.getEngine?.();
|
|
290
|
+
const canvas = engine?.getRenderingCanvas?.();
|
|
291
|
+
|
|
292
|
+
let prefViewer3D = canvas?.closest?.("pref-viewer-3d") || null;
|
|
293
|
+
if (!prefViewer3D) {
|
|
294
|
+
const host = canvas?.getRootNode?.()?.host;
|
|
295
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.#prefViewer3D = prefViewer3D;
|
|
299
|
+
}
|
|
300
|
+
|
|
274
301
|
/**
|
|
275
302
|
* Applies threshold logic to the progress value to snap to 0 or 1 if near the ends.
|
|
276
303
|
* Prevents floating point errors from leaving the animation in an in-between state.
|
|
@@ -287,6 +314,33 @@ export default class OpeningAnimation {
|
|
|
287
314
|
return progress;
|
|
288
315
|
}
|
|
289
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Dispatches a `prefviewer-animation-update` CustomEvent from the nearest `pref-viewer-3d` host.
|
|
319
|
+
* The event bubbles and crosses shadow DOM boundaries, exposing the current animation snapshot
|
|
320
|
+
* (`name`, `state`, `progress`, and `loop`) in `event.detail`.
|
|
321
|
+
* @private
|
|
322
|
+
* @returns {boolean} True when the event was dispatched, false when no `pref-viewer-3d` host is available.
|
|
323
|
+
*/
|
|
324
|
+
#notifyStateChange() {
|
|
325
|
+
this.#getPrefViewer3DComponent();
|
|
326
|
+
if (!this.#prefViewer3D) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const customEventOptions = {
|
|
331
|
+
bubbles: true,
|
|
332
|
+
composed: true,
|
|
333
|
+
detail: {
|
|
334
|
+
name: this.name,
|
|
335
|
+
state: this.#state,
|
|
336
|
+
progress: this.#getProgress(),
|
|
337
|
+
loop: this.#loop,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
this.#prefViewer3D.dispatchEvent(new CustomEvent("prefviewer-animation-changed", customEventOptions));
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
290
344
|
/**
|
|
291
345
|
* Updates the slider value in the animation control menu to match the current progress.
|
|
292
346
|
* @private
|
|
@@ -299,10 +353,12 @@ export default class OpeningAnimation {
|
|
|
299
353
|
}
|
|
300
354
|
|
|
301
355
|
/**
|
|
302
|
-
*
|
|
356
|
+
* Broadcasts the latest animation state and, when the menu is visible, syncs its controls
|
|
357
|
+
* (buttons and progress slider) with the current state/progress values.
|
|
303
358
|
* @private
|
|
304
359
|
*/
|
|
305
360
|
#updateControls() {
|
|
361
|
+
this.#notifyStateChange();
|
|
306
362
|
if (!this.isControlsVisible()) {
|
|
307
363
|
return;
|
|
308
364
|
}
|
|
@@ -8,6 +8,7 @@ import JSZip from "jszip";
|
|
|
8
8
|
import GLTFResolver from "./gltf-resolver.js";
|
|
9
9
|
import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
10
10
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
11
|
+
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
11
12
|
import { translate } from "./localization/i18n.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -63,7 +64,6 @@ import { translate } from "./localization/i18n.js";
|
|
|
63
64
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
64
65
|
*/
|
|
65
66
|
export default class BabylonJSController {
|
|
66
|
-
|
|
67
67
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
68
68
|
|
|
69
69
|
// Default render settings
|
|
@@ -75,7 +75,7 @@ export default class BabylonJSController {
|
|
|
75
75
|
iblEnabled: true,
|
|
76
76
|
shadowsEnabled: false,
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
// Canvas HTML element
|
|
80
80
|
#canvas = null;
|
|
81
81
|
|
|
@@ -93,11 +93,13 @@ export default class BabylonJSController {
|
|
|
93
93
|
#shadowGen = [];
|
|
94
94
|
#XRExperience = null;
|
|
95
95
|
#canvasResizeObserver = null;
|
|
96
|
+
|
|
96
97
|
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
97
|
-
|
|
98
|
+
#lastPickedMeshId = null;
|
|
99
|
+
|
|
98
100
|
#containers = {};
|
|
99
101
|
#options = {};
|
|
100
|
-
|
|
102
|
+
|
|
101
103
|
#gltfResolver = null; // GLTFResolver instance
|
|
102
104
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
103
105
|
|
|
@@ -110,11 +112,28 @@ export default class BabylonJSController {
|
|
|
110
112
|
#handlers = {
|
|
111
113
|
onKeyUp: null,
|
|
112
114
|
onPointerObservable: null,
|
|
115
|
+
onAnimationGroupChanged: null,
|
|
116
|
+
onResize: null,
|
|
113
117
|
renderLoop: null,
|
|
114
118
|
};
|
|
115
|
-
|
|
119
|
+
|
|
116
120
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
117
121
|
|
|
122
|
+
#renderState = {
|
|
123
|
+
isLoopRunning: false,
|
|
124
|
+
dirtyFrames: 0,
|
|
125
|
+
continuousUntil: 0,
|
|
126
|
+
lastRenderAt: 0,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
#renderConfig = {
|
|
130
|
+
burstFramesBase: 2,
|
|
131
|
+
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
132
|
+
interactionMs: 250,
|
|
133
|
+
animationMs: 200,
|
|
134
|
+
idleThrottleMs: 1000 / 15,
|
|
135
|
+
};
|
|
136
|
+
|
|
118
137
|
/**
|
|
119
138
|
* Constructs a new BabylonJSController instance.
|
|
120
139
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
@@ -147,8 +166,10 @@ export default class BabylonJSController {
|
|
|
147
166
|
* @returns {void}
|
|
148
167
|
*/
|
|
149
168
|
#bindHandlers() {
|
|
169
|
+
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
150
170
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
151
171
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
172
|
+
this.#handlers.onResize = this.#onResize.bind(this);
|
|
152
173
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
153
174
|
}
|
|
154
175
|
|
|
@@ -271,16 +292,177 @@ export default class BabylonJSController {
|
|
|
271
292
|
}
|
|
272
293
|
}
|
|
273
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Starts Babylon's engine render loop if it is not already running.
|
|
297
|
+
* @private
|
|
298
|
+
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
299
|
+
*/
|
|
300
|
+
#startEngineRenderLoop() {
|
|
301
|
+
if (!this.#engine || this.#renderState.isLoopRunning) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
305
|
+
this.#renderState.isLoopRunning = true;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Stops Babylon's engine render loop when it is currently active.
|
|
311
|
+
* @private
|
|
312
|
+
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
313
|
+
*/
|
|
314
|
+
#stopEngineRenderLoop() {
|
|
315
|
+
if (!this.#engine || !this.#renderState.isLoopRunning) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
319
|
+
this.#renderState.isLoopRunning = false;
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
325
|
+
* Ensures the engine loop is running so the requested frames can be produced.
|
|
326
|
+
* @private
|
|
327
|
+
* @param {{frames?:number, continuousMs?:number}} [options={}] - Render request options.
|
|
328
|
+
* @param {number} [options.frames=1] - Minimum number of frames to render.
|
|
329
|
+
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
330
|
+
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
331
|
+
*/
|
|
332
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
333
|
+
if (!this.#scene || !this.#engine) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
338
|
+
this.#renderState.dirtyFrames = Math.max(this.#renderState.dirtyFrames, Math.max(1, frames));
|
|
339
|
+
if (continuousMs > 0) {
|
|
340
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + continuousMs);
|
|
341
|
+
}
|
|
342
|
+
this.#startEngineRenderLoop();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks whether an ArcRotateCamera still has non-zero inertial movement.
|
|
348
|
+
* @private
|
|
349
|
+
* @param {ArcRotateCamera} camera - Camera to evaluate.
|
|
350
|
+
* @returns {boolean} True when any inertial offset is still active.
|
|
351
|
+
*/
|
|
352
|
+
#isArcRotateCameraInMotion(camera) {
|
|
353
|
+
const EPSILON = 0.00001;
|
|
354
|
+
return Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Checks whether a FreeCamera/UniversalCamera is currently moving or rotating.
|
|
359
|
+
* @private
|
|
360
|
+
* @param {FreeCamera|UniversalCamera} camera - Camera to evaluate.
|
|
361
|
+
* @returns {boolean} True when translation or rotation deltas are active.
|
|
362
|
+
*/
|
|
363
|
+
#isUniversalOrFreeCameraInMotion(camera) {
|
|
364
|
+
const EPSILON = 0.00001;
|
|
365
|
+
const direction = camera?.cameraDirection;
|
|
366
|
+
const rotation = camera?.cameraRotation;
|
|
367
|
+
const directionMoving = !!direction && (Math.abs(direction.x) > EPSILON || Math.abs(direction.y) > EPSILON || Math.abs(direction.z) > EPSILON);
|
|
368
|
+
const rotationMoving = !!rotation && (Math.abs(rotation.x) > EPSILON || Math.abs(rotation.y) > EPSILON || Math.abs(rotation.z) > EPSILON);
|
|
369
|
+
return directionMoving || rotationMoving;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Detects motion for the current active camera based on its concrete camera type.
|
|
374
|
+
* @private
|
|
375
|
+
* @returns {boolean} True when the active camera is moving, otherwise false.
|
|
376
|
+
*/
|
|
377
|
+
#isCameraInMotion() {
|
|
378
|
+
const camera = this.#scene?.activeCamera;
|
|
379
|
+
if (!camera) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
if (camera instanceof ArcRotateCamera) {
|
|
383
|
+
return this.#isArcRotateCameraInMotion(camera);
|
|
384
|
+
}
|
|
385
|
+
if (camera instanceof UniversalCamera || camera instanceof FreeCamera) {
|
|
386
|
+
return this.#isUniversalOrFreeCameraInMotion(camera);
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Determines whether scene animations are currently running.
|
|
393
|
+
* @private
|
|
394
|
+
* @returns {boolean} True when at least one animation group is playing.
|
|
395
|
+
*/
|
|
396
|
+
#isAnimationRunning() {
|
|
397
|
+
if (!this.#scene) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const hasAnimatables = (this.#scene.animatables?.length || 0) > 0;
|
|
401
|
+
if (!hasAnimatables) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return this.#scene.animationGroups?.some((group) => group?.isPlaying) || false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Evaluates whether the renderer should stay in continuous mode.
|
|
409
|
+
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
410
|
+
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
411
|
+
* @private
|
|
412
|
+
* @param {number} now - Current high-resolution timestamp.
|
|
413
|
+
* @returns {boolean} True when continuous rendering should remain active.
|
|
414
|
+
*/
|
|
415
|
+
#shouldRenderContinuously(now) {
|
|
416
|
+
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
417
|
+
if (inXR) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const animationRunning = this.#isAnimationRunning();
|
|
422
|
+
const cameraInMotion = this.#isCameraInMotion();
|
|
423
|
+
|
|
424
|
+
if (animationRunning) {
|
|
425
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.animationMs);
|
|
426
|
+
}
|
|
427
|
+
if (cameraInMotion) {
|
|
428
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.interactionMs);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return animationRunning || cameraInMotion || this.#renderState.continuousUntil > now;
|
|
432
|
+
}
|
|
433
|
+
|
|
274
434
|
/**
|
|
275
435
|
* Render loop callback for Babylon.js.
|
|
436
|
+
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
437
|
+
* It self-stops when the scene becomes idle.
|
|
276
438
|
* @private
|
|
277
439
|
* @returns {void}
|
|
278
|
-
* @description
|
|
279
|
-
* Continuously renders the current scene if it exists.
|
|
280
|
-
* Used by the engine's runRenderLoop method to update the view.
|
|
281
440
|
*/
|
|
282
441
|
#renderLoop() {
|
|
283
|
-
|
|
442
|
+
if (!this.#scene) {
|
|
443
|
+
this.#stopEngineRenderLoop();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
448
|
+
const continuous = this.#shouldRenderContinuously(now);
|
|
449
|
+
const needsRender = continuous || this.#renderState.dirtyFrames > 0;
|
|
450
|
+
|
|
451
|
+
if (!needsRender) {
|
|
452
|
+
this.#stopEngineRenderLoop();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!continuous && this.#renderState.lastRenderAt > 0 && now - this.#renderState.lastRenderAt < this.#renderConfig.idleThrottleMs) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.#scene.render();
|
|
461
|
+
this.#renderState.lastRenderAt = now;
|
|
462
|
+
|
|
463
|
+
if (this.#renderState.dirtyFrames > 0) {
|
|
464
|
+
this.#renderState.dirtyFrames -= 1;
|
|
465
|
+
}
|
|
284
466
|
}
|
|
285
467
|
|
|
286
468
|
/**
|
|
@@ -417,24 +599,20 @@ export default class BabylonJSController {
|
|
|
417
599
|
this.#scene.environmentTexture = null;
|
|
418
600
|
lightsChanged = true;
|
|
419
601
|
}
|
|
420
|
-
if (this.#hdrTexture) {
|
|
421
|
-
this.#hdrTexture.dispose();
|
|
422
|
-
this.#hdrTexture = null;
|
|
423
|
-
}
|
|
424
602
|
|
|
425
603
|
// Add a hemispheric light for basic ambient illumination
|
|
426
604
|
if (!this.#hemiLight) {
|
|
427
605
|
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
428
606
|
this.#hemiLight.intensity = 0.6;
|
|
429
607
|
}
|
|
430
|
-
|
|
608
|
+
|
|
431
609
|
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
432
610
|
if (!this.#dirLight) {
|
|
433
611
|
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
434
612
|
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
435
613
|
this.#dirLight.intensity = 0.6;
|
|
436
614
|
}
|
|
437
|
-
|
|
615
|
+
|
|
438
616
|
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
439
617
|
if (!this.#cameraLight) {
|
|
440
618
|
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
@@ -459,7 +637,7 @@ export default class BabylonJSController {
|
|
|
459
637
|
}
|
|
460
638
|
|
|
461
639
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
462
|
-
|
|
640
|
+
|
|
463
641
|
if (supportedPipelines === undefined) {
|
|
464
642
|
return false;
|
|
465
643
|
}
|
|
@@ -499,23 +677,23 @@ export default class BabylonJSController {
|
|
|
499
677
|
return false;
|
|
500
678
|
}
|
|
501
679
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
502
|
-
|
|
680
|
+
|
|
503
681
|
if (supportedPipelines === undefined) {
|
|
504
682
|
return false;
|
|
505
683
|
}
|
|
506
|
-
|
|
684
|
+
|
|
507
685
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
508
686
|
|
|
509
687
|
const ssaoRatio = {
|
|
510
688
|
ssaoRatio: 0.5,
|
|
511
|
-
combineRatio: 1.0
|
|
689
|
+
combineRatio: 1.0,
|
|
512
690
|
};
|
|
513
691
|
|
|
514
692
|
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
515
693
|
|
|
516
|
-
if (!ssaoPipeline){
|
|
694
|
+
if (!ssaoPipeline) {
|
|
517
695
|
return false;
|
|
518
|
-
}
|
|
696
|
+
}
|
|
519
697
|
|
|
520
698
|
if (ssaoPipeline.isSupported) {
|
|
521
699
|
ssaoPipeline.fallOff = 0.000001;
|
|
@@ -523,7 +701,7 @@ export default class BabylonJSController {
|
|
|
523
701
|
ssaoPipeline.radius = 0.0001;
|
|
524
702
|
ssaoPipeline.totalStrength = 1;
|
|
525
703
|
ssaoPipeline.base = 0.6;
|
|
526
|
-
|
|
704
|
+
|
|
527
705
|
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
528
706
|
if (ssaoPipeline._ssaoPostProcess) {
|
|
529
707
|
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
@@ -533,7 +711,7 @@ export default class BabylonJSController {
|
|
|
533
711
|
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
534
712
|
ssaoPipeline._combinePostProcess.samples = 1;
|
|
535
713
|
}
|
|
536
|
-
|
|
714
|
+
|
|
537
715
|
this.#renderPipelines.ssao = ssaoPipeline;
|
|
538
716
|
pipelineManager.update();
|
|
539
717
|
return true;
|
|
@@ -559,7 +737,7 @@ export default class BabylonJSController {
|
|
|
559
737
|
}
|
|
560
738
|
|
|
561
739
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
562
|
-
|
|
740
|
+
|
|
563
741
|
if (supportedPipelines === undefined) {
|
|
564
742
|
return false;
|
|
565
743
|
}
|
|
@@ -600,18 +778,18 @@ export default class BabylonJSController {
|
|
|
600
778
|
return false;
|
|
601
779
|
}
|
|
602
780
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
603
|
-
|
|
781
|
+
|
|
604
782
|
if (supportedPipelines === undefined) {
|
|
605
783
|
return false;
|
|
606
784
|
}
|
|
607
|
-
|
|
785
|
+
|
|
608
786
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
609
787
|
|
|
610
788
|
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
611
789
|
|
|
612
|
-
if (!defaultPipeline){
|
|
790
|
+
if (!defaultPipeline) {
|
|
613
791
|
return false;
|
|
614
|
-
}
|
|
792
|
+
}
|
|
615
793
|
|
|
616
794
|
if (defaultPipeline.isSupported) {
|
|
617
795
|
// MSAA - Multisample Anti-Aliasing
|
|
@@ -716,7 +894,7 @@ export default class BabylonJSController {
|
|
|
716
894
|
}
|
|
717
895
|
|
|
718
896
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
719
|
-
|
|
897
|
+
|
|
720
898
|
if (supportedPipelines === undefined) {
|
|
721
899
|
return false;
|
|
722
900
|
}
|
|
@@ -751,13 +929,13 @@ export default class BabylonJSController {
|
|
|
751
929
|
* @private
|
|
752
930
|
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
753
931
|
*/
|
|
754
|
-
async #initializeIBLShadows() {
|
|
932
|
+
async #initializeIBLShadows() {
|
|
755
933
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
756
|
-
|
|
934
|
+
|
|
757
935
|
if (!this.#scene || !this.#scene?.activeCamera || !this.#scene?.environmentTexture || !pipelineManager) {
|
|
758
936
|
return false;
|
|
759
937
|
}
|
|
760
|
-
|
|
938
|
+
|
|
761
939
|
if (!this.#scene.environmentTexture.isReady()) {
|
|
762
940
|
const self = this;
|
|
763
941
|
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
@@ -771,11 +949,11 @@ export default class BabylonJSController {
|
|
|
771
949
|
if (isRootMesh) {
|
|
772
950
|
return false;
|
|
773
951
|
}
|
|
774
|
-
|
|
952
|
+
|
|
775
953
|
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
776
954
|
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
777
955
|
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
778
|
-
|
|
956
|
+
|
|
779
957
|
if (meshGenerateShadows) {
|
|
780
958
|
return true;
|
|
781
959
|
}
|
|
@@ -793,7 +971,7 @@ export default class BabylonJSController {
|
|
|
793
971
|
}
|
|
794
972
|
|
|
795
973
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
796
|
-
|
|
974
|
+
|
|
797
975
|
if (!supportedPipelines) {
|
|
798
976
|
return false;
|
|
799
977
|
}
|
|
@@ -816,7 +994,7 @@ export default class BabylonJSController {
|
|
|
816
994
|
if (!iblShadowsPipeline) {
|
|
817
995
|
return false;
|
|
818
996
|
}
|
|
819
|
-
|
|
997
|
+
|
|
820
998
|
if (iblShadowsPipeline.isSupported) {
|
|
821
999
|
// Disable all debug passes for performance
|
|
822
1000
|
const pipelineProps = {
|
|
@@ -835,7 +1013,7 @@ export default class BabylonJSController {
|
|
|
835
1013
|
|
|
836
1014
|
meshesForCastingShadows.forEach((mesh) => iblShadowsPipeline.addShadowCastingMesh(mesh));
|
|
837
1015
|
materialsForReceivingShadows.forEach((material) => iblShadowsPipeline.addShadowReceivingMaterial(material));
|
|
838
|
-
|
|
1016
|
+
|
|
839
1017
|
iblShadowsPipeline.updateSceneBounds();
|
|
840
1018
|
iblShadowsPipeline.toggleShadow(true);
|
|
841
1019
|
iblShadowsPipeline.updateVoxelization();
|
|
@@ -1002,23 +1180,6 @@ export default class BabylonJSController {
|
|
|
1002
1180
|
}
|
|
1003
1181
|
}
|
|
1004
1182
|
|
|
1005
|
-
/**
|
|
1006
|
-
* Handles pointer events observed on the Babylon.js scene.
|
|
1007
|
-
* @private
|
|
1008
|
-
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1009
|
-
* @returns {void}
|
|
1010
|
-
*/
|
|
1011
|
-
#onPointerObservable(info) {
|
|
1012
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1013
|
-
if (info.type === PointerEventTypes.POINTERUP) {
|
|
1014
|
-
this.#onPointerUp(info.event, pickInfo);
|
|
1015
|
-
} else if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1016
|
-
this.#onPointerMove(info.event, pickInfo);
|
|
1017
|
-
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1018
|
-
this.#onMouseWheel(info.event, pickInfo);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1183
|
/**
|
|
1023
1184
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
1024
1185
|
* @private
|
|
@@ -1031,6 +1192,13 @@ export default class BabylonJSController {
|
|
|
1031
1192
|
if (this.#scene) {
|
|
1032
1193
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
1033
1194
|
}
|
|
1195
|
+
if (this.#engine) {
|
|
1196
|
+
this.#canvasResizeObserver = new ResizeObserver(() => {
|
|
1197
|
+
this.#engine.resize();
|
|
1198
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1199
|
+
});
|
|
1200
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1201
|
+
}
|
|
1034
1202
|
}
|
|
1035
1203
|
|
|
1036
1204
|
/**
|
|
@@ -1045,6 +1213,9 @@ export default class BabylonJSController {
|
|
|
1045
1213
|
if (this.#scene !== null) {
|
|
1046
1214
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1047
1215
|
}
|
|
1216
|
+
this.#canvasResizeObserver?.disconnect();
|
|
1217
|
+
this.#canvasResizeObserver = null;
|
|
1218
|
+
this.#detachAnimationChangedListener();
|
|
1048
1219
|
}
|
|
1049
1220
|
|
|
1050
1221
|
/**
|
|
@@ -1105,6 +1276,75 @@ export default class BabylonJSController {
|
|
|
1105
1276
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1106
1277
|
}
|
|
1107
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* Handles animation state events emitted by `OpeningAnimation` instances.
|
|
1281
|
+
* Routes opening/closing states to continuous rendering and all other states
|
|
1282
|
+
* (paused/opened/closed) to a short final render burst.
|
|
1283
|
+
* @private
|
|
1284
|
+
* @param {CustomEvent} event - Event carrying animation state in `event.detail.state`.
|
|
1285
|
+
* @returns {void}
|
|
1286
|
+
*/
|
|
1287
|
+
#onAnimationGroupChanged(event) {
|
|
1288
|
+
const state = event?.detail?.state;
|
|
1289
|
+
if (state === undefined) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (state === OpeningAnimation.states.opening || state === OpeningAnimation.states.closing) {
|
|
1294
|
+
this.#onAnimationGroupPlay();
|
|
1295
|
+
} else {
|
|
1296
|
+
this.#onAnimationGroupStop();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Marks animation playback as active and requests short continuous rendering so animated transforms remain smooth while state is changing.
|
|
1302
|
+
* @private
|
|
1303
|
+
* @returns {void}
|
|
1304
|
+
*/
|
|
1305
|
+
#onAnimationGroupPlay() {
|
|
1306
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.animationMs });
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Handles animation stop/pause/end transitions by requesting a final render burst.
|
|
1311
|
+
* @private
|
|
1312
|
+
* @returns {void}
|
|
1313
|
+
*/
|
|
1314
|
+
#onAnimationGroupStop() {
|
|
1315
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1316
|
+
this.#requestRender({ frames: frames });
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Attaches the `prefviewer-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1321
|
+
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1322
|
+
* @private
|
|
1323
|
+
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
1324
|
+
*/
|
|
1325
|
+
#attachAnimationChangedListener() {
|
|
1326
|
+
this.#getPrefViewer3DComponent();
|
|
1327
|
+
if (!this.#prefViewer3D) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
this.#detachAnimationChangedListener();
|
|
1331
|
+
this.#prefViewer3D.addEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Detaches the `prefviewer-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1337
|
+
* @private
|
|
1338
|
+
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1339
|
+
*/
|
|
1340
|
+
#detachAnimationChangedListener() {
|
|
1341
|
+
if (!this.#prefViewer3D) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
this.#prefViewer3D.removeEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1108
1348
|
/**
|
|
1109
1349
|
* Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
|
|
1110
1350
|
* @private
|
|
@@ -1132,11 +1372,12 @@ export default class BabylonJSController {
|
|
|
1132
1372
|
* @returns {void|false} Returns false if there is no active camera; otherwise, void.
|
|
1133
1373
|
*/
|
|
1134
1374
|
#onMouseWheel(event, pickInfo) {
|
|
1375
|
+
event.preventDefault();
|
|
1135
1376
|
const camera = this.#scene?.activeCamera;
|
|
1136
1377
|
if (!camera) {
|
|
1137
1378
|
return false;
|
|
1138
1379
|
}
|
|
1139
|
-
if (!camera.metadata?.locked) {
|
|
1380
|
+
if (!camera.metadata?.locked) {
|
|
1140
1381
|
if (camera instanceof ArcRotateCamera) {
|
|
1141
1382
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
1142
1383
|
camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
|
|
@@ -1149,8 +1390,8 @@ export default class BabylonJSController {
|
|
|
1149
1390
|
const movementVector = direction.scale(zoomSpeed);
|
|
1150
1391
|
camera.position = camera.position.add(movementVector);
|
|
1151
1392
|
}
|
|
1393
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1152
1394
|
}
|
|
1153
|
-
event.preventDefault();
|
|
1154
1395
|
}
|
|
1155
1396
|
|
|
1156
1397
|
/**
|
|
@@ -1178,9 +1419,54 @@ export default class BabylonJSController {
|
|
|
1178
1419
|
* @returns {void}
|
|
1179
1420
|
*/
|
|
1180
1421
|
#onPointerMove(event, pickInfo) {
|
|
1422
|
+
const camera = this.#scene?.activeCamera;
|
|
1423
|
+
if (camera && !camera.metadata?.locked) {
|
|
1424
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1425
|
+
}
|
|
1181
1426
|
if (this.#babylonJSAnimationController) {
|
|
1182
|
-
|
|
1427
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1428
|
+
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1429
|
+
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1430
|
+
if (highlightResult.changed) {
|
|
1431
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Handles pointer events observed on the Babylon.js scene.
|
|
1439
|
+
* @private
|
|
1440
|
+
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1441
|
+
* @returns {void}
|
|
1442
|
+
*/
|
|
1443
|
+
#onPointerObservable(info) {
|
|
1444
|
+
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1445
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1446
|
+
|
|
1447
|
+
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1448
|
+
this.#onPointerMove(info.event, pickInfo);
|
|
1449
|
+
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1450
|
+
this.#onPointerUp(info.event, pickInfo);
|
|
1451
|
+
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1452
|
+
this.#onMouseWheel(info.event, pickInfo);
|
|
1453
|
+
}
|
|
1454
|
+
this.#lastPickedMeshId = pickedMeshId;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Handles canvas resize notifications.
|
|
1459
|
+
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
1460
|
+
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
1461
|
+
* @private
|
|
1462
|
+
* @returns {void}
|
|
1463
|
+
*/
|
|
1464
|
+
#onResize() {
|
|
1465
|
+
if (!this.#engine) {
|
|
1466
|
+
return;
|
|
1183
1467
|
}
|
|
1468
|
+
this.#engine.resize();
|
|
1469
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1184
1470
|
}
|
|
1185
1471
|
|
|
1186
1472
|
/**
|
|
@@ -1222,7 +1508,7 @@ export default class BabylonJSController {
|
|
|
1222
1508
|
.forEach((mesh) => {
|
|
1223
1509
|
mesh.material = material;
|
|
1224
1510
|
someSetted = true;
|
|
1225
|
-
})
|
|
1511
|
+
}),
|
|
1226
1512
|
);
|
|
1227
1513
|
|
|
1228
1514
|
if (someSetted) {
|
|
@@ -1336,34 +1622,47 @@ export default class BabylonJSController {
|
|
|
1336
1622
|
}
|
|
1337
1623
|
|
|
1338
1624
|
/**
|
|
1339
|
-
*
|
|
1625
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the rendering canvas.
|
|
1340
1626
|
* @private
|
|
1341
1627
|
* @returns {void}
|
|
1342
1628
|
*/
|
|
1343
1629
|
#getPrefViewer3DComponent() {
|
|
1344
|
-
if (this.#prefViewer3D
|
|
1345
|
-
|
|
1346
|
-
|
|
1630
|
+
if (this.#prefViewer3D !== undefined) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
let prefViewer3D = this.#canvas?.closest?.("pref-viewer-3d") || undefined;
|
|
1635
|
+
if (!prefViewer3D) {
|
|
1636
|
+
const host = this.#canvas?.getRootNode?.()?.host;
|
|
1637
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
1347
1638
|
}
|
|
1639
|
+
|
|
1640
|
+
this.#prefViewer3D = prefViewer3D;
|
|
1348
1641
|
}
|
|
1349
1642
|
|
|
1350
1643
|
/**
|
|
1351
|
-
*
|
|
1644
|
+
* Resolves and caches the closest `pref-viewer` host associated with `#prefViewer3D`.
|
|
1352
1645
|
* @private
|
|
1353
1646
|
* @returns {void}
|
|
1354
1647
|
*/
|
|
1355
1648
|
#getPrefViewerComponent() {
|
|
1356
|
-
if (this.#prefViewer
|
|
1357
|
-
|
|
1358
|
-
this.#getPrefViewer3DComponent();
|
|
1359
|
-
}
|
|
1360
|
-
if (!this.#prefViewer3D) {
|
|
1361
|
-
this.#prefViewer = null;
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
|
|
1365
|
-
this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
|
|
1649
|
+
if (this.#prefViewer !== undefined) {
|
|
1650
|
+
return;
|
|
1366
1651
|
}
|
|
1652
|
+
|
|
1653
|
+
this.#getPrefViewer3DComponent();
|
|
1654
|
+
if (this.#prefViewer3D === undefined) {
|
|
1655
|
+
this.#prefViewer = undefined;
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
let prefViewer = this.#prefViewer3D.closest?.("pref-viewer") || undefined;
|
|
1660
|
+
if (!prefViewer) {
|
|
1661
|
+
const host = this.#prefViewer3D.getRootNode?.()?.host;
|
|
1662
|
+
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
this.#prefViewer = prefViewer;
|
|
1367
1666
|
}
|
|
1368
1667
|
|
|
1369
1668
|
/**
|
|
@@ -1512,7 +1811,10 @@ export default class BabylonJSController {
|
|
|
1512
1811
|
* @returns {void}
|
|
1513
1812
|
*/
|
|
1514
1813
|
async #stopRender() {
|
|
1515
|
-
this.#
|
|
1814
|
+
this.#stopEngineRenderLoop();
|
|
1815
|
+
this.#renderState.dirtyFrames = 0;
|
|
1816
|
+
this.#renderState.continuousUntil = 0;
|
|
1817
|
+
this.#renderState.lastRenderAt = 0;
|
|
1516
1818
|
await this.#unloadCameraDependentEffects();
|
|
1517
1819
|
}
|
|
1518
1820
|
/**
|
|
@@ -1523,9 +1825,9 @@ export default class BabylonJSController {
|
|
|
1523
1825
|
*/
|
|
1524
1826
|
async #startRender() {
|
|
1525
1827
|
await this.#loadCameraDependentEffects();
|
|
1526
|
-
this.#scene.
|
|
1527
|
-
|
|
1528
|
-
});
|
|
1828
|
+
await this.#scene.whenReadyAsync();
|
|
1829
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1830
|
+
this.#requestRender({ frames: frames, continuousMs: this.#renderConfig.interactionMs });
|
|
1529
1831
|
}
|
|
1530
1832
|
|
|
1531
1833
|
/**
|
|
@@ -1545,7 +1847,6 @@ export default class BabylonJSController {
|
|
|
1545
1847
|
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
1546
1848
|
*/
|
|
1547
1849
|
async #loadAssetContainer(container, force = false) {
|
|
1548
|
-
|
|
1549
1850
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1550
1851
|
return [container, false];
|
|
1551
1852
|
}
|
|
@@ -1606,6 +1907,7 @@ export default class BabylonJSController {
|
|
|
1606
1907
|
* Returns an object with success status and error details.
|
|
1607
1908
|
*/
|
|
1608
1909
|
async #loadContainers() {
|
|
1910
|
+
this.#detachAnimationChangedListener();
|
|
1609
1911
|
await this.#stopRender();
|
|
1610
1912
|
|
|
1611
1913
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
@@ -1662,6 +1964,9 @@ export default class BabylonJSController {
|
|
|
1662
1964
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1663
1965
|
this.#setMaxSimultaneousLights();
|
|
1664
1966
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1967
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
1968
|
+
this.#attachAnimationChangedListener();
|
|
1969
|
+
}
|
|
1665
1970
|
await this.#startRender();
|
|
1666
1971
|
});
|
|
1667
1972
|
return detail;
|
|
@@ -1921,10 +2226,10 @@ export default class BabylonJSController {
|
|
|
1921
2226
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1922
2227
|
this.#engine.disableUniformBuffers = true;
|
|
1923
2228
|
this.#scene = new Scene(this.#engine);
|
|
1924
|
-
|
|
2229
|
+
|
|
1925
2230
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1926
|
-
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1927
|
-
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
2231
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
2232
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1928
2233
|
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1929
2234
|
if (geometryBufferRenderer) {
|
|
1930
2235
|
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
@@ -1941,12 +2246,14 @@ export default class BabylonJSController {
|
|
|
1941
2246
|
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
1942
2247
|
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
1943
2248
|
|
|
2249
|
+
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2250
|
+
this.#scene.skipPointerMovePicking = true;
|
|
2251
|
+
this.#scene.skipPointerDownPicking = true;
|
|
2252
|
+
this.#scene.skipPointerUpPicking = true;
|
|
2253
|
+
|
|
1944
2254
|
this.#createCamera();
|
|
1945
2255
|
this.#enableInteraction();
|
|
1946
2256
|
await this.#createXRExperience();
|
|
1947
|
-
this.#startRender();
|
|
1948
|
-
this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
1949
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1950
2257
|
}
|
|
1951
2258
|
|
|
1952
2259
|
/**
|
|
@@ -1956,11 +2263,11 @@ export default class BabylonJSController {
|
|
|
1956
2263
|
* @returns {void}
|
|
1957
2264
|
*/
|
|
1958
2265
|
disable() {
|
|
1959
|
-
this.#canvasResizeObserver.disconnect();
|
|
1960
2266
|
this.#disableInteraction();
|
|
1961
2267
|
this.#disposeAnimationController();
|
|
1962
2268
|
this.#disposeXRExperience();
|
|
1963
2269
|
this.#unloadCameraDependentEffects();
|
|
2270
|
+
this.#stopEngineRenderLoop();
|
|
1964
2271
|
this.#disposeEngine();
|
|
1965
2272
|
}
|
|
1966
2273
|
|