@preference-sl/pref-viewer 2.11.0-beta.9 → 2.11.0

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.
@@ -1,5 +1,4 @@
1
- import { AdvancedDynamicTexture } from "@babylonjs/gui";
2
- import { OpeningAnimationMenu } from "./babylonjs-animation-opening-menu.js";
1
+ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
3
2
 
4
3
  /**
5
4
  * OpeningAnimation - Manages open/close animations for a model part (e.g., a door) in a Babylon.js scene.
@@ -13,13 +12,14 @@ import { OpeningAnimationMenu } from "./babylonjs-animation-opening-menu.js";
13
12
  * - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
14
13
  *
15
14
  * Public Methods:
15
+ * - dispose(): Disposes the OpeningAnimation instance and releases all associated resources.
16
16
  * - isAnimationForNode(node): Checks if the animation affects the given node.
17
17
  * - playOpen(): Starts the opening animation.
18
18
  * - playClose(): Starts the closing animation.
19
19
  * - pause(): Pauses the current animation.
20
20
  * - goToOpened(): Moves animation to the fully opened state.
21
21
  * - goToClosed(): Moves animation to the fully closed state.
22
- * - showControls(advancedDynamicTexture): Displays the animation control menu.
22
+ * - showControls(canvas): Displays the animation control menu.
23
23
  * - hideControls(): Hides the animation control menu.
24
24
  * - isControlsVisible(): Returns true if the control menu is visible for this animation.
25
25
  *
@@ -39,16 +39,8 @@ import { OpeningAnimationMenu } from "./babylonjs-animation-opening-menu.js";
39
39
  * - #checkProgress(progress): Applies threshold logic to progress.
40
40
  * - #updateControlsSlider(): Updates the slider in the control menu.
41
41
  * - #updateControls(): Updates all controls in the menu.
42
- *
43
- * Usage Example:
44
- * const anim = new OpeningAnimation("door", openGroup, closeGroup);
45
- * anim.playOpen();
46
- * anim.pause();
47
- * anim.goToClosed();
48
- * anim.showControls(adt);
49
- * anim.hideControls();
50
42
  */
51
- export class OpeningAnimation {
43
+ export default class OpeningAnimation {
52
44
  static states = {
53
45
  paused: 0,
54
46
  closed: 1,
@@ -57,21 +49,26 @@ export class OpeningAnimation {
57
49
  closing: 4,
58
50
  };
59
51
 
52
+ name = "";
60
53
  #openAnimation = null;
61
54
  #closeAnimation = null;
62
-
63
55
  #nodes = [];
56
+ #menu = null;
57
+
64
58
  #state = OpeningAnimation.states.closed;
65
59
  #lastPausedFrame = 0;
66
60
  #startFrame = 0;
67
61
  #endFrame = 0;
68
62
  #speedRatio = 1.0;
69
63
  #loop = false;
70
-
71
- #advancedDynamicTexture = null;
72
- #menu = null;
73
64
  #progressThreshold = 0.025;
74
65
 
66
+ #handlers = {
67
+ onOpened: null,
68
+ onClosed: null,
69
+ updateControlsSlider: null,
70
+ };
71
+
75
72
  /**
76
73
  * Creates a new OpeningAnimation instance for managing open/close animations of a model part.
77
74
  * @param {string} name - The identifier for this animation (e.g., door name).
@@ -95,8 +92,15 @@ export class OpeningAnimation {
95
92
  this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
96
93
 
97
94
  this.#getNodesFromAnimationGroups();
98
- this.#openAnimation.onAnimationGroupEndObservable.add(this.#onOpened.bind(this));
99
- this.#closeAnimation.onAnimationGroupEndObservable.add(this.#onClosed.bind(this));
95
+ this.#bindHandlers();
96
+ this.#openAnimation.onAnimationGroupEndObservable.add(this.#handlers.onOpened);
97
+ this.#closeAnimation.onAnimationGroupEndObservable.add(this.#handlers.onClosed);
98
+ }
99
+
100
+ #bindHandlers() {
101
+ this.#handlers.onOpened = this.#onOpened.bind(this);
102
+ this.#handlers.onClosed = this.#onClosed.bind(this);
103
+ this.#handlers.updateControlsSlider = this.#updateControlsSlider.bind(this);
100
104
  }
101
105
 
102
106
  /**
@@ -138,13 +142,18 @@ export class OpeningAnimation {
138
142
  #goToOpened(useLoop = false) {
139
143
  this.#lastPausedFrame = this.#endFrame;
140
144
 
141
- if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused) {
145
+ if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused){
142
146
  this.#openAnimation.pause();
143
147
  }
144
- this.#openAnimation.goToFrame(this.#endFrame);
148
+ this.#openAnimation.goToFrame(this.#endFrame);
145
149
 
146
- this.#closeAnimation.pause();
147
- this.#closeAnimation.goToFrame(this.#startFrame);
150
+ if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
151
+ this.#closeAnimation.goToFrame(this.#startFrame);
152
+ } else {
153
+ this.#closeAnimation.start();
154
+ this.#closeAnimation.pause();
155
+ this.#closeAnimation.goToFrame(this.#startFrame);
156
+ }
148
157
 
149
158
  this.#state = OpeningAnimation.states.opened;
150
159
  this.#updateControls();
@@ -167,8 +176,13 @@ export class OpeningAnimation {
167
176
  }
168
177
  this.#closeAnimation.goToFrame(this.#endFrame - this.#startFrame);
169
178
 
170
- this.#openAnimation.pause();
171
- this.#openAnimation.goToFrame(this.#startFrame);
179
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
180
+ this.#openAnimation.goToFrame(this.#startFrame);
181
+ } else {
182
+ this.#openAnimation.start();
183
+ this.#openAnimation.pause();
184
+ this.#openAnimation.goToFrame(this.#startFrame);
185
+ }
172
186
 
173
187
  this.#state = OpeningAnimation.states.closed;
174
188
  this.#updateControls();
@@ -303,6 +317,25 @@ export class OpeningAnimation {
303
317
  * ---------------------------
304
318
  */
305
319
 
320
+ /**
321
+ * Disposes the OpeningAnimation instance and releases all associated resources.
322
+ * @public
323
+ * @returns {void}
324
+ */
325
+ dispose() {
326
+ this.hideControls();
327
+ if (this.#openAnimation) {
328
+ this.#openAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onOpened);
329
+ this.#openAnimation = null;
330
+ }
331
+ if (this.#closeAnimation) {
332
+ this.#closeAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onClosed);
333
+ this.#closeAnimation = null;
334
+ }
335
+ this.#nodes = [];
336
+ this.name = "";
337
+ }
338
+
306
339
  /**
307
340
  * Checks if the animation affects the given node.
308
341
  * @param {string} node - Node identifier.
@@ -320,6 +353,7 @@ export class OpeningAnimation {
320
353
  if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
321
354
  return;
322
355
  }
356
+
323
357
  if (this.#state === OpeningAnimation.states.closing) {
324
358
  this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
325
359
  this.#closeAnimation.pause();
@@ -344,6 +378,7 @@ export class OpeningAnimation {
344
378
  if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
345
379
  return;
346
380
  }
381
+
347
382
  if (this.#state === OpeningAnimation.states.opening) {
348
383
  this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
349
384
  this.#openAnimation.pause();
@@ -397,11 +432,9 @@ export class OpeningAnimation {
397
432
  * Displays the animation control menu and sets up callbacks.
398
433
  * Synchronizes slider and button states with animation.
399
434
  * @public
400
- * @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture.
435
+ * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
401
436
  */
402
- showControls(advancedDynamicTexture) {
403
- this.#advancedDynamicTexture = advancedDynamicTexture;
404
- this.#advancedDynamicTexture.metadata = { animationName: this.name };
437
+ showControls(canvas) {
405
438
  const controlCallbacks = {
406
439
  onGoToOpened: () => {
407
440
  if (this.#state === OpeningAnimation.states.opened) {
@@ -449,10 +482,10 @@ export class OpeningAnimation {
449
482
  this.#menu.animationLoop = this.#loop;
450
483
  },
451
484
  };
452
- this.#menu = new OpeningAnimationMenu(this.#advancedDynamicTexture, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
485
+ this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
453
486
 
454
487
  // Attach to Babylon.js scene render loop for real-time updates
455
- this.#openAnimation._scene.onBeforeRenderObservable.add(this.#updateControlsSlider.bind(this));
488
+ this.#openAnimation._scene.onBeforeRenderObservable.add(this.#handlers.updateControlsSlider);
456
489
  }
457
490
 
458
491
  /**
@@ -463,10 +496,9 @@ export class OpeningAnimation {
463
496
  if (!this.isControlsVisible()) {
464
497
  return;
465
498
  }
466
- // Remove the observer when controls are hidden
467
- this.#openAnimation._scene.onBeforeRenderObservable.removeCallback(this.#updateControlsSlider.bind(this));
468
- this.#advancedDynamicTexture.dispose();
469
- this.#advancedDynamicTexture = null;
499
+ this.#openAnimation?._scene?.onBeforeRenderObservable.removeCallback(this.#handlers.updateControlsSlider);
500
+ this.#menu.dispose();
501
+ this.#menu = null;
470
502
  }
471
503
 
472
504
  /**
@@ -475,7 +507,7 @@ export class OpeningAnimation {
475
507
  * @returns {boolean} True if controls are visible for this animation; otherwise, false.
476
508
  */
477
509
  isControlsVisible() {
478
- return !!(this.#advancedDynamicTexture && this.#advancedDynamicTexture.metadata?.animationName === this.name && this.#menu);
510
+ return !!(this.#menu?.isVisible);
479
511
  }
480
512
 
481
513
  /**
@@ -489,7 +521,6 @@ export class OpeningAnimation {
489
521
  * @public
490
522
  * @returns {number}
491
523
  */
492
-
493
524
  get state() {
494
525
  return this.#state;
495
526
  }
@@ -1,24 +1,31 @@
1
- import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager } from "@babylonjs/core";
2
- import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
1
+ import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointerEventTypes, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
2
+ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
3
3
  import "@babylonjs/loaders";
4
- import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
4
+ import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
5
5
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
6
+ import JSZip from "jszip";
6
7
 
7
8
  import GLTFResolver from "./gltf-resolver.js";
8
9
  import { MaterialData } from "./pref-viewer-3d-data.js";
9
10
  import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
10
11
 
11
12
  /**
12
- * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
13
+ * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, rendering, and user interactions in PrefViewer.
13
14
  *
14
- * Responsibilities:
15
+ * Summary:
16
+ * Provides a high-level API for initializing, loading, displaying, and exporting 3D models and environments using Babylon.js.
17
+ * Manages advanced rendering features, AR integration, asset containers, and user interaction logic.
18
+ *
19
+ * Key Responsibilities:
15
20
  * - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
16
21
  * - Handles loading, replacing, and disposing of 3D models, environments, and materials.
17
22
  * - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
18
23
  * - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
19
- * - Provides methods for downloading models and scenes in GLB, GLTF, and USDZ formats.
24
+ * - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
20
25
  * - Manages camera and material options, container visibility, and user interactions.
21
26
  * - Observes canvas resize events and updates the engine accordingly.
27
+ * - Designed for integration with PrefViewer and GLTFResolver.
28
+ * - All resource management and rendering operations are performed asynchronously for performance.
22
29
  *
23
30
  * Usage:
24
31
  * - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
@@ -26,7 +33,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
26
33
  * - Load assets: await controller.load();
27
34
  * - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
28
35
  * - Control visibility: controller.setContainerVisibility(name, show);
29
- * - Download assets: controller.downloadModelGLB(), controller.downloadModelGLTF(), controller.downloadModelUSDZ(), controller.downloadModelAndSceneGLB(), controller.downloadModelAndSceneGLTF(), controller.downloadModelAndSceneUSDZ();
36
+ * - Download assets: controller.downloadGLB(), controller.downloadGLTF(), controller.downloadUSDZ();
30
37
  * - Disable rendering: controller.disable();
31
38
  *
32
39
  * Public Methods:
@@ -40,30 +47,46 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
40
47
  * - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
41
48
  * - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
42
49
  *
43
- * Private Methods:
50
+ * Private Methods (using ECMAScript private fields):
51
+ * - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
44
52
  * - #configureDracoCompression(): Sets up Draco mesh compression.
45
53
  * - #renderLoop(): Babylon.js render loop callback.
46
54
  * - #addStylesToARButton(): Styles AR button.
47
55
  * - #createXRExperience(): Initializes WebXR AR experience.
48
- * - #createCamera(), #createLights(), #initializeEnvironmentTexture(), #initializeIBLShadows(), #initializeShadows(): Scene setup.
49
- * - #setMaxSimultaneousLights(): Updates max simultaneous lights for materials.
50
- * - #enableInteraction(), #disableInteraction(): Canvas interaction handlers.
51
- * - #disposeEngine(): Disposes engine and resources.
52
- * - #onMouseWheel(event), #onKeyUp(event): Canvas event handlers.
53
- * - #setOptionsMaterial(), #setOptions_Materials(), #setOptions_Camera(): Applies material/camera options.
54
- * - #findContainerByName(), #addContainer(), #removeContainer(), #replaceContainer(): Container management.
55
- * - #getPrefViewer3DComponent(), #getPrefViewerComponent(): Custom element references.
56
- * - #updateVisibilityAttributeInComponents(): Updates parent visibility attributes.
57
- * - #setVisibilityOfWallAndFloorInModel(): Controls wall/floor mesh visibility.
58
- * - #stopRender(), #startRender(): Render loop control.
59
- * - #loadAssetContainer(), #loadContainers(): Asset loading.
60
- * - #downloadZip(): Generates and downloads a ZIP file.
56
+ * - #createCamera(): Creates and configures the main camera.
57
+ * - #createLights(): Creates and configures scene lights and shadows.
58
+ * - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
59
+ * - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
60
+ * - #initializeShadows(): Sets up standard or IBL shadows for meshes.
61
+ * - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
62
+ * - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
63
+ * - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
64
+ * - #onPointerMove(event, pickInfo): Handles pointer move events (e.g., mesh highlighting).
65
+ * - #onMouseWheel(event, pickInfo): Handles mouse wheel events for camera zoom.
66
+ * - #onKeyUp(event): Handles keyup events for download dialog and shortcuts.
67
+ * - #enableInteraction(): Adds canvas and scene interaction event listeners.
68
+ * - #disableInteraction(): Removes canvas and scene interaction event listeners.
69
+ * - #disposeAnimationController(): Disposes the animation controller if it exists.
70
+ * - #disposeXRExperience(): Disposes the Babylon.js WebXR experience if it exists.
71
+ * - #disposeEngine(): Disposes engine and releases all resources.
72
+ * - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
73
+ * - #setOptions_Materials(): Applies all material options from configuration.
74
+ * - #setOptions_Camera(): Applies camera options from configuration.
75
+ * - #findContainerByName(name): Finds a container by its name.
76
+ * - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
77
+ * - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
78
+ * - #replaceContainer(container, newAssetContainer): Replaces a container in the scene.
79
+ * - #getPrefViewer3DComponent(): Caches and retrieves the parent PREF-VIEWER-3D element.
80
+ * - #getPrefViewerComponent(): Caches and retrieves the parent PREF-VIEWER element.
81
+ * - #updateVisibilityAttributeInComponents(name, isVisible): Updates parent visibility attributes.
82
+ * - #setVisibilityOfWallAndFloorInModel(show): Controls wall/floor mesh visibility.
83
+ * - #stopRender(): Stops the Babylon.js render loop.
84
+ * - #startRender(): Starts the Babylon.js render loop.
85
+ * - #loadAssetContainer(container): Loads an asset container asynchronously.
86
+ * - #loadContainers(): Loads all asset containers and adds them to the scene.
87
+ * - #addDateToName(name): Appends the current date/time to a name string.
88
+ * - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
61
89
  * - #openDownloadDialog(): Opens the modal download dialog.
62
- *
63
- * Notes:
64
- * - Designed for integration with PrefViewer and GLTFResolver.
65
- * - Supports advanced Babylon.js features for product visualization and configurators.
66
- * - All resource management and rendering operations are performed asynchronously for performance.
67
90
  */
68
91
  export default class BabylonJSController {
69
92
  // Canvas HTML element
@@ -90,6 +113,12 @@ export default class BabylonJSController {
90
113
  #gltfResolver = null; // GLTFResolver instance
91
114
  #babylonJSAnimationController = null; // AnimationController instance
92
115
 
116
+ #handlers = {
117
+ onKeyUp: null,
118
+ onPointerObservable: null,
119
+ renderLoop: null,
120
+ };
121
+
93
122
  /**
94
123
  * Constructs a new BabylonJSController instance.
95
124
  * Initializes the canvas, asset containers, and options for the Babylon.js scene.
@@ -112,6 +141,18 @@ export default class BabylonJSController {
112
141
  };
113
142
  });
114
143
  this.#options = options;
144
+ this.#bindHandlers();
145
+ }
146
+
147
+ /**
148
+ * Pre-binds reusable event handlers to preserve stable references.
149
+ * @private
150
+ * @returns {void}
151
+ */
152
+ #bindHandlers() {
153
+ this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
154
+ this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
155
+ this.#handlers.renderLoop = this.#renderLoop.bind(this);
115
156
  }
116
157
 
117
158
  /**
@@ -401,13 +442,34 @@ export default class BabylonJSController {
401
442
  }
402
443
 
403
444
  /**
404
- * Sets up interaction handlers for the Babylon.js canvas.
445
+ * Handles pointer events observed on the Babylon.js scene.
446
+ * @private
447
+ * @param {PointerInfo} info - The pointer event information from Babylon.js.
448
+ * @returns {void}
449
+ */
450
+ #onPointerObservable(info) {
451
+ const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
452
+ if (info.type === PointerEventTypes.POINTERUP) {
453
+ this.#onPointerUp(info.event, pickInfo);
454
+ } else if (info.type === PointerEventTypes.POINTERMOVE) {
455
+ this.#onPointerMove(info.event, pickInfo);
456
+ } else if (info.type === PointerEventTypes.POINTERWHEEL) {
457
+ this.#onMouseWheel(info.event, pickInfo);
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Sets up interaction handlers for the Babylon.js canvas and scene.
405
463
  * @private
406
464
  * @returns {void}
407
465
  */
408
466
  #enableInteraction() {
409
- this.#canvas.addEventListener("wheel", this.#onMouseWheel.bind(this));
410
- this.#canvas.addEventListener("keyup", this.#onKeyUp.bind(this));
467
+ if (this.#canvas) {
468
+ this.#canvas.addEventListener("keyup", this.#handlers.onKeyUp);
469
+ }
470
+ if (this.#scene) {
471
+ this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
472
+ }
411
473
  }
412
474
 
413
475
  /**
@@ -416,8 +478,52 @@ export default class BabylonJSController {
416
478
  * @returns {void}
417
479
  */
418
480
  #disableInteraction() {
419
- this.#canvas.removeEventListener("wheel", this.#onMouseWheel.bind(this));
420
- this.#canvas.removeEventListener("keyup", this.#onKeyUp.bind(this));
481
+ if (this.#canvas) {
482
+ this.#canvas.removeEventListener("keyup", this.#handlers.onKeyUp);
483
+ }
484
+ if (this.#scene !== null) {
485
+ this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Disposes the BabylonJSAnimationController instance if it exists.
491
+ * @private
492
+ * @returns {void}
493
+ */
494
+ #disposeAnimationController() {
495
+ if (this.#babylonJSAnimationController) {
496
+ this.#babylonJSAnimationController.dispose();
497
+ this.#babylonJSAnimationController = null;
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Disposes the Babylon.js WebXR experience if it exists.
503
+ * @private
504
+ * @returns {void}
505
+ */
506
+ #disposeXRExperience() {
507
+ if (!this.#XRExperience) {
508
+ return;
509
+ }
510
+
511
+ if (this.#XRExperience.baseExperience.state === WebXRState.IN_XR) {
512
+ this.#XRExperience.baseExperience
513
+ .exitXRAsync()
514
+ .then(() => {
515
+ this.#XRExperience.dispose();
516
+ this.#XRExperience = null;
517
+ })
518
+ .catch((error) => {
519
+ console.warn("Error exiting XR experience:", error);
520
+ this.#XRExperience.dispose();
521
+ this.#XRExperience = null;
522
+ });
523
+ } else {
524
+ this.#XRExperience.dispose();
525
+ this.#XRExperience = null;
526
+ }
421
527
  }
422
528
 
423
529
  /**
@@ -433,21 +539,39 @@ export default class BabylonJSController {
433
539
  this.#engine = this.#scene = this.#camera = null;
434
540
  this.#hemiLight = this.#dirLight = this.#cameraLight = null;
435
541
  this.#shadowGen = null;
436
- this.#XRExperience = null;
542
+ }
543
+
544
+ /**
545
+ * Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
546
+ * @private
547
+ * @param {KeyboardEvent} event - The keyup event.
548
+ * @returns {void}
549
+ */
550
+ #onKeyUp(event) {
551
+ // CTRL + ALT + letter
552
+ if (event.ctrlKey && event.altKey && event.key !== undefined) {
553
+ switch (event.key.toLowerCase()) {
554
+ case "d":
555
+ this.#openDownloadDialog();
556
+ break;
557
+ default:
558
+ break;
559
+ }
560
+ }
437
561
  }
438
562
 
439
563
  /**
440
564
  * Handles mouse wheel events on the Babylon.js canvas for zooming the camera.
441
565
  * @private
442
566
  * @param {WheelEvent} event - The mouse wheel event.
443
- * @returns {void|false}
567
+ * @param {Object} pickInfo - The result of the scene pick operation (not used in this method).
568
+ * @returns {void|false} Returns false if there is no active camera; otherwise, void.
444
569
  */
445
- #onMouseWheel(event) {
446
- if (!this.#scene || !this.#camera) {
570
+ #onMouseWheel(event, pickInfo) {
571
+ if (!this.#scene?.activeCamera) {
447
572
  return false;
448
573
  }
449
- //const pick = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
450
- //this.#camera.target = pick.hit ? pick.pickedPoint.clone() : this.#camera.target;
574
+ //this.#scene.activeCamera.target = pickInfo.hit ? pickInfo.pickedPoint.clone() : this.#scene.activeCamera.target;
451
575
  if (!this.#scene.activeCamera.metadata?.locked) {
452
576
  this.#scene.activeCamera.inertialRadiusOffset -= event.deltaY * this.#scene.activeCamera.wheelPrecision * 0.001;
453
577
  }
@@ -455,24 +579,35 @@ export default class BabylonJSController {
455
579
  }
456
580
 
457
581
  /**
458
- * Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
582
+ * Handles pointer up events on the Babylon.js scene.
459
583
  * @private
460
- * @param {KeyboardEvent} event - The keyup event.
584
+ * @param {PointerEvent} event - The pointer up event.
585
+ * @param {PickInfo} pickInfo - The result of the scene pick operation.
461
586
  * @returns {void}
462
587
  */
463
- #onKeyUp(event) {
464
- // CTRL + ALT + letter
465
- if (event.ctrlKey && event.altKey && event.key !== undefined) {
466
- switch (event.key.toLowerCase()) {
467
- case "d":
468
- this.#openDownloadDialog();
469
- break;
470
- default:
471
- break;
588
+ #onPointerUp(event, pickInfo) {
589
+ if (this.#babylonJSAnimationController) {
590
+ this.#babylonJSAnimationController.hideMenu();
591
+ // Right click for showing animation menu
592
+ if (event.button === 2) {
593
+ this.#babylonJSAnimationController.showMenu(pickInfo);
472
594
  }
473
595
  }
474
596
  }
475
597
 
598
+ /**
599
+ * Handles pointer move events on the Babylon.js scene.
600
+ * @private
601
+ * @param {PointerEvent} event - The pointer move event.
602
+ * @param {PickInfo} pickInfo - The result of the scene pick operation.
603
+ * @returns {void}
604
+ */
605
+ #onPointerMove(event, pickInfo) {
606
+ if (this.#babylonJSAnimationController) {
607
+ this.#babylonJSAnimationController.hightlightMeshes(pickInfo);
608
+ }
609
+ }
610
+
476
611
  /**
477
612
  * Applies material options from the configuration to the relevant meshes.
478
613
  * @private
@@ -739,7 +874,7 @@ export default class BabylonJSController {
739
874
  * @returns {void}
740
875
  */
741
876
  #stopRender() {
742
- this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
877
+ this.#engine.stopRenderLoop(this.#handlers.renderLoop);
743
878
  }
744
879
  /**
745
880
  * Starts the Babylon.js render loop for the current scene.
@@ -749,7 +884,7 @@ export default class BabylonJSController {
749
884
  */
750
885
  async #startRender() {
751
886
  await this.#scene.whenReadyAsync();
752
- this.#engine.runRenderLoop(this.#renderLoop.bind(this));
887
+ this.#engine.runRenderLoop(this.#handlers.renderLoop);
753
888
  }
754
889
 
755
890
  /**
@@ -820,10 +955,7 @@ export default class BabylonJSController {
820
955
 
821
956
  await Promise.allSettled(promiseArray)
822
957
  .then((values) => {
823
- if (this.#babylonJSAnimationController) {
824
- this.#babylonJSAnimationController.dispose();
825
- this.#babylonJSAnimationController = null;
826
- }
958
+ this.#disposeAnimationController();
827
959
  values.forEach((result) => {
828
960
  const container = result.value ? result.value[0] : null;
829
961
  const assetContainer = result.value ? result.value[1] : null;
@@ -893,7 +1025,6 @@ export default class BabylonJSController {
893
1025
  #downloadZip(files, name = "files", comment = "", addDateInName = false) {
894
1026
  name = addDateInName ? this.#addDateToName(name) : name;
895
1027
 
896
- const JSZip = require("jszip");
897
1028
  const zip = new JSZip();
898
1029
  zip.comment = comment;
899
1030
 
@@ -992,7 +1123,7 @@ export default class BabylonJSController {
992
1123
  */
993
1124
  async enable() {
994
1125
  this.#configureDracoCompression();
995
- this.#engine = new Engine(this.#canvas, true, { alpha: true });
1126
+ this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
996
1127
  this.#engine.disableUniformBuffers = true;
997
1128
  this.#scene = new Scene(this.#engine);
998
1129
  this.#scene.clearColor = new Color4(1, 1, 1, 1);
@@ -1014,10 +1145,8 @@ export default class BabylonJSController {
1014
1145
  disable() {
1015
1146
  this.#canvasResizeObserver.disconnect();
1016
1147
  this.#disableInteraction();
1017
- if (this.#babylonJSAnimationController) {
1018
- this.#babylonJSAnimationController.dispose();
1019
- this.#babylonJSAnimationController = null;
1020
- }
1148
+ this.#disposeAnimationController();
1149
+ this.#disposeXRExperience();
1021
1150
  this.#disposeEngine();
1022
1151
  }
1023
1152
 
@@ -174,7 +174,12 @@ export class FileStorage {
174
174
  xhr.onload = () => {
175
175
  if (xhr.status === 200) {
176
176
  const blob = xhr.response;
177
- const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
177
+ const lastModified = xhr.getResponseHeader("Last-Modified");
178
+ let timeStamp = null;
179
+ if (lastModified) {
180
+ const parsed = new Date(lastModified);
181
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
182
+ }
178
183
  file = { blob: blob, timeStamp: timeStamp };
179
184
  resolve(file);
180
185
  } else {
@@ -204,7 +209,11 @@ export class FileStorage {
204
209
  xhr.responseType = "blob";
205
210
  xhr.onload = () => {
206
211
  if (xhr.status === 200) {
207
- timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
212
+ const lastModified = xhr.getResponseHeader("Last-Modified");
213
+ if (lastModified) {
214
+ const parsed = new Date(lastModified);
215
+ timeStamp = Number.isNaN(parsed.valueOf()) ? null : parsed.toISOString();
216
+ }
208
217
  resolve(timeStamp);
209
218
  } else {
210
219
  resolve(timeStamp);