@preference-sl/pref-viewer 2.11.0-beta.13 → 2.11.0-beta.15
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 +5 -6
- package/src/babylonjs-animation-controller.js +14 -20
- package/src/babylonjs-animation-opening-menu.js +181 -153
- package/src/babylonjs-animation-opening.js +24 -34
- package/src/babylonjs-controller.js +58 -21
- package/src/styles.js +141 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.11.0-beta.
|
|
3
|
+
"version": "2.11.0-beta.15",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -35,12 +35,11 @@
|
|
|
35
35
|
"index.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@babylonjs/core": "^8.39.
|
|
39
|
-
"@babylonjs/
|
|
40
|
-
"@babylonjs/
|
|
41
|
-
"@babylonjs/serializers": "^8.39.0",
|
|
38
|
+
"@babylonjs/core": "^8.39.2",
|
|
39
|
+
"@babylonjs/loaders": "^8.39.2",
|
|
40
|
+
"@babylonjs/serializers": "^8.39.2",
|
|
42
41
|
"@panzoom/panzoom": "^4.6.0",
|
|
43
|
-
"babylonjs-gltf2interface": "^8.39.
|
|
42
|
+
"babylonjs-gltf2interface": "^8.39.2",
|
|
44
43
|
"buffer": "^6.0.3",
|
|
45
44
|
"idb": "^8.0.3",
|
|
46
45
|
"is-svg": "^6.1.0",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
|
|
2
|
-
import { AdvancedDynamicTexture } from "@babylonjs/gui";
|
|
3
2
|
import { PrefViewerColors } from "./styles.js";
|
|
4
3
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
5
4
|
|
|
@@ -7,30 +6,30 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
|
7
6
|
* BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
|
|
8
7
|
*
|
|
9
8
|
* Summary:
|
|
10
|
-
* This class detects, groups, and manages opening/closing animations for scene nodes, provides interactive highlighting of animated nodes and their meshes, and displays a
|
|
9
|
+
* This class detects, groups, and manages opening/closing animations for scene nodes, provides interactive highlighting of animated nodes and their meshes, and displays a menu for animation control. It is designed for integration with product configurators and interactive 3D applications using Babylon.js.
|
|
11
10
|
*
|
|
12
11
|
* Key features:
|
|
13
12
|
* - Detects and groups opening/closing animations in the scene.
|
|
14
13
|
* - Tracks animated transformation nodes and their relationships to meshes.
|
|
15
14
|
* - Highlights animated nodes and their child meshes on pointer hover.
|
|
16
|
-
* - Displays and disposes the animation control menu
|
|
15
|
+
* - Displays and disposes the animation control menu for animated nodes.
|
|
17
16
|
* - Provides public API for highlighting, showing the animation menu, and disposing resources.
|
|
18
17
|
* - Cleans up all resources and observers to prevent memory leaks.
|
|
19
18
|
*
|
|
20
19
|
* Public Methods:
|
|
21
20
|
* - dispose(): Disposes all resources managed by the animation controller.
|
|
22
21
|
* - hightlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
|
|
23
|
-
* - hideMenu(): Hides and disposes the animation control menu
|
|
22
|
+
* - hideMenu(): Hides and disposes the animation control menu if it exists.
|
|
24
23
|
* - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
|
|
25
24
|
*
|
|
26
25
|
* @class
|
|
27
26
|
*/
|
|
28
27
|
export default class BabylonJSAnimationController {
|
|
29
28
|
#scene = null;
|
|
29
|
+
#canvas = null;
|
|
30
30
|
#animatedNodes = [];
|
|
31
31
|
#highlightLayer = null;
|
|
32
32
|
#highlightColor = Color3.FromHexString(PrefViewerColors.primary);
|
|
33
|
-
#advancedDynamicTexture = null;
|
|
34
33
|
#openingAnimations = [];
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -39,6 +38,7 @@ export default class BabylonJSAnimationController {
|
|
|
39
38
|
*/
|
|
40
39
|
constructor(scene) {
|
|
41
40
|
this.#scene = scene;
|
|
41
|
+
this.#canvas = this.#scene._engine._renderingCanvas;
|
|
42
42
|
this.#initializeAnimations();
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -47,11 +47,12 @@ export default class BabylonJSAnimationController {
|
|
|
47
47
|
* @private
|
|
48
48
|
*/
|
|
49
49
|
#initializeAnimations() {
|
|
50
|
+
this.hideMenu(); // Clean up any existing menus
|
|
50
51
|
if (!this.#scene.animationGroups.length) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
this.#getAnimatedNodes();
|
|
54
|
-
this.#
|
|
55
|
+
this.#getOpeningAnimations();
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
@@ -78,7 +79,7 @@ export default class BabylonJSAnimationController {
|
|
|
78
79
|
* @description
|
|
79
80
|
* Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
|
|
80
81
|
*/
|
|
81
|
-
#
|
|
82
|
+
#getOpeningAnimations() {
|
|
82
83
|
const openings = {};
|
|
83
84
|
this.#scene.animationGroups.forEach((animationGroup) => {
|
|
84
85
|
const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
|
|
@@ -137,7 +138,7 @@ export default class BabylonJSAnimationController {
|
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
140
|
* Disposes all resources managed by the animation controller.
|
|
140
|
-
* Cleans up the highlight layer,
|
|
141
|
+
* Cleans up the highlight layer, animation menu, and internal animation/node lists.
|
|
141
142
|
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
142
143
|
* @public
|
|
143
144
|
*/
|
|
@@ -183,24 +184,19 @@ export default class BabylonJSAnimationController {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
* Hides and disposes the animation control menu
|
|
187
|
+
* Hides and disposes the animation control menu if it exists.
|
|
187
188
|
* @public
|
|
188
189
|
* @returns {void}
|
|
189
190
|
*/
|
|
190
191
|
hideMenu() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.#advancedDynamicTexture.dispose();
|
|
194
|
-
this.#advancedDynamicTexture = null;
|
|
195
|
-
}
|
|
192
|
+
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.hideControls());
|
|
193
|
+
this.#canvas.parentElement.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
/**
|
|
199
197
|
* Displays the animation control menu for the animated node under the pointer.
|
|
200
198
|
* @public
|
|
201
199
|
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
202
|
-
* @description
|
|
203
|
-
* Creates the GUI if needed and invokes OpeningAnimation.showControls.
|
|
204
200
|
*/
|
|
205
201
|
showMenu(pickingInfo) {
|
|
206
202
|
if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
|
|
@@ -217,9 +213,7 @@ export default class BabylonJSAnimationController {
|
|
|
217
213
|
if (!openingAnimation) {
|
|
218
214
|
return;
|
|
219
215
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
openingAnimation.showControls(this.#advancedDynamicTexture);
|
|
216
|
+
|
|
217
|
+
openingAnimation.showControls(this.#canvas);
|
|
224
218
|
}
|
|
225
219
|
}
|
|
@@ -1,24 +1,36 @@
|
|
|
1
|
-
import { AdvancedDynamicTexture, Button, Control, Image, Slider, StackPanel } from "@babylonjs/gui";
|
|
2
1
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
3
|
-
import {
|
|
2
|
+
import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* OpeningAnimationMenu - Manages and renders the animation control menu for opening/closing animations in a Babylon.js scene.
|
|
5
|
+
* OpeningAnimationMenu - Manages and renders the interactive animation control menu for opening/closing animations in a Babylon.js scene.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Summary:
|
|
8
|
+
* Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons and a progress slider.
|
|
9
|
+
* Handles user interaction, updates UI state based on animation state, and invokes external callbacks for animation actions.
|
|
10
|
+
* Designed for integration with Babylon.js product configurators and interactive 3D applications.
|
|
11
|
+
*
|
|
12
|
+
* Key Features:
|
|
13
|
+
* - Renders a menu with buttons for controlling animation playback (open, close, pause, go to opened/closed, loop).
|
|
10
14
|
* - Updates button states and slider position based on animation state, progress, and loop mode.
|
|
11
15
|
* - Handles user interactions and invokes provided callbacks for animation actions.
|
|
12
16
|
* - Synchronizes the slider value with animation progress, avoiding callback loops.
|
|
17
|
+
* - Provides setters to update animation state, progress, and loop mode from external controllers.
|
|
18
|
+
* - Disposes and cleans up all DOM resources when no longer needed.
|
|
13
19
|
*
|
|
14
20
|
* Public Setters:
|
|
15
21
|
* - set animationState(state): Updates the animation state and button states.
|
|
16
22
|
* - set animationProgress(progress): Updates the animation progress and slider value.
|
|
17
23
|
* - set animationLoop(loop): Updates the loop mode and loop button states.
|
|
18
24
|
*
|
|
25
|
+
* Public Methods:
|
|
26
|
+
* - dispose(): Disposes the animation menu and releases all associated DOM resources.
|
|
27
|
+
*
|
|
28
|
+
* Public Getters:
|
|
29
|
+
* - isVisible: Returns whether the animation menu is currently visible in the DOM.
|
|
30
|
+
*
|
|
19
31
|
* Private Methods:
|
|
20
32
|
* - #createMenu(): Initializes and renders the menu UI.
|
|
21
|
-
* - #addButton(name,
|
|
33
|
+
* - #addButton(name, imageSVG, enabled, active, visible, callback): Adds a button to the menu with specified properties.
|
|
22
34
|
* - #createButtons(): Creates all control buttons and sets their initial states.
|
|
23
35
|
* - #getButtonByName(name): Retrieves a button control by its name.
|
|
24
36
|
* - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
|
|
@@ -28,9 +40,10 @@ import { PrefViewerColors } from "./styles.js";
|
|
|
28
40
|
* - #setPlayerButtonsState(): Updates all playback control buttons.
|
|
29
41
|
* - #setLoopButtonsState(): Updates all loop control buttons.
|
|
30
42
|
* - #createSlider(): Creates and configures the animation progress slider.
|
|
43
|
+
* - #onSliderInput(event): Handles the slider input event for animation progress.
|
|
31
44
|
*
|
|
32
45
|
* Usage Example:
|
|
33
|
-
* const menu = new OpeningAnimationMenu(
|
|
46
|
+
* const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, {
|
|
34
47
|
* onOpen: () => { ... },
|
|
35
48
|
* onClose: () => { ... },
|
|
36
49
|
* onPause: () => { ... },
|
|
@@ -49,45 +62,35 @@ export default class OpeningAnimationMenu {
|
|
|
49
62
|
#animationLoop = false;
|
|
50
63
|
#callbacks = null;
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
65
|
+
#name = "";
|
|
66
|
+
#canvas = null;
|
|
67
|
+
#containerMain = null;
|
|
68
|
+
#containerButtons = null;
|
|
56
69
|
#slider = null;
|
|
57
70
|
|
|
58
|
-
// Style properties
|
|
59
|
-
#buttonSize = 28;
|
|
60
|
-
#buttonLoopPaddingLeft = 3;
|
|
61
|
-
#colorActive = PrefViewerColors.primary;
|
|
62
|
-
#colorEnabled = "#333333";
|
|
63
|
-
#colorDisabled = "#777777";
|
|
64
|
-
#colorIcon = "#FFFFFF";
|
|
65
|
-
#colorBorder = "#FFFFFF";
|
|
66
|
-
#sliderThumbWidth = 20;
|
|
67
|
-
#sliderBarOffset = 10;
|
|
68
71
|
#icon = {
|
|
69
|
-
close: `<svg id="play-backwards" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
70
|
-
closed: `<svg id="skip-backward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
71
|
-
open: `<svg id="play" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
72
|
-
opened: `<svg id="skip-forward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
73
|
-
pause: `<svg id="pause" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
74
|
-
repeat: `<svg id="repeat" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
75
|
-
repeatOff: `<svg id="repeat-off" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path
|
|
72
|
+
close: `<svg id="play-backwards" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M16,18.86V4.86l-11,7,11,7Z"/></svg>`,
|
|
73
|
+
closed: `<svg id="skip-backward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12"/></svg>`,
|
|
74
|
+
open: `<svg id="play" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M8,5.14v14l11-7-11-7Z"/></svg>`,
|
|
75
|
+
opened: `<svg id="skip-forward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12"/></svg>`,
|
|
76
|
+
pause: `<svg id="pause" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M14,19H18V5H14M6,19H10V5H6V19Z"/></svg>`,
|
|
77
|
+
repeat: `<svg id="repeat" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z"/></svg>`,
|
|
78
|
+
repeatOff: `<svg id="repeat-off" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z"/></svg>`,
|
|
76
79
|
};
|
|
77
80
|
|
|
78
|
-
#isSettingSliderValue = false;
|
|
79
|
-
|
|
80
81
|
/**
|
|
81
82
|
* Constructs the OpeningAnimationMenu and initializes the menu UI.
|
|
82
|
-
* @param {
|
|
83
|
+
* @param {string} name - The name of the animation.
|
|
84
|
+
* @param {HTMLCanvasElement} canvas - The canvas element for rendering.
|
|
83
85
|
* @param {number} animationState - Current animation state (enum).
|
|
84
86
|
* @param {number} animationProgress - Current animation progress (0 to 1).
|
|
85
87
|
* @param {boolean} animationLoop - Whether the animation is set to loop.
|
|
86
88
|
* @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
|
|
87
89
|
* @public
|
|
88
90
|
*/
|
|
89
|
-
constructor(
|
|
90
|
-
this.#
|
|
91
|
+
constructor(name, canvas, animationState, animationProgress, animationLoop, callbacks) {
|
|
92
|
+
this.#name = name;
|
|
93
|
+
this.#canvas = canvas;
|
|
91
94
|
this.#animationState = animationState;
|
|
92
95
|
this.#animationProgress = animationProgress;
|
|
93
96
|
this.#animationLoop = animationLoop;
|
|
@@ -96,29 +99,6 @@ export default class OpeningAnimationMenu {
|
|
|
96
99
|
this.#createMenu(animationState);
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
/**
|
|
100
|
-
* Initializes and renders the menu UI, including buttons and slider.
|
|
101
|
-
* @private
|
|
102
|
-
*/
|
|
103
|
-
#createMenu() {
|
|
104
|
-
if (!this.#advancedDynamicTexture) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
this.#mainPanel = new StackPanel();
|
|
108
|
-
this.#mainPanel.isVertical = true;
|
|
109
|
-
this.#secondaryPanel = new StackPanel();
|
|
110
|
-
this.#secondaryPanel.isVertical = false;
|
|
111
|
-
this.#secondaryPanel.height = `${this.#buttonSize}px`;
|
|
112
|
-
this.#mainPanel.addControl(this.#secondaryPanel);
|
|
113
|
-
|
|
114
|
-
this.#advancedDynamicTexture.addControl(this.#mainPanel);
|
|
115
|
-
this.#mainPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
|
116
|
-
this.#mainPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
|
|
117
|
-
|
|
118
|
-
this.#createButtons();
|
|
119
|
-
this.#createSlider();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
102
|
/**
|
|
123
103
|
* Internal helper to add a button to the menu.
|
|
124
104
|
* Sets button appearance and attaches the callback for user interaction.
|
|
@@ -130,25 +110,21 @@ export default class OpeningAnimationMenu {
|
|
|
130
110
|
* @param {boolean} visible - Whether the button is visible.
|
|
131
111
|
* @param {function} callback - Callback to invoke on button click.
|
|
132
112
|
*/
|
|
133
|
-
#addButton(name,
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
};
|
|
143
|
-
const button = Button.CreateImageOnlyButton(`button_animation_${name}`, imageURL);
|
|
144
|
-
Object.assign(button, buttonProps);
|
|
145
|
-
button.image.stretch = Image.STRETCH_UNIFORM;
|
|
146
|
-
button.onPointerUpObservable.add(() => {
|
|
113
|
+
#addButton(name, imageSVG, enabled = true, active = false, visible = true, callback) {
|
|
114
|
+
const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
|
|
115
|
+
const visibilityClass = visible ? "visible" : "hidden";
|
|
116
|
+
|
|
117
|
+
const button = document.createElement("button");
|
|
118
|
+
button.classList.add(stateClass, visibilityClass);
|
|
119
|
+
button.setAttribute("name", `button_animation_${name}`);
|
|
120
|
+
button.innerHTML = imageSVG;
|
|
121
|
+
button.addEventListener("click", (event) => {
|
|
147
122
|
if (callback && typeof callback === "function") {
|
|
148
123
|
callback();
|
|
149
124
|
}
|
|
150
125
|
});
|
|
151
|
-
|
|
126
|
+
|
|
127
|
+
this.#containerButtons.appendChild(button);
|
|
152
128
|
}
|
|
153
129
|
|
|
154
130
|
/**
|
|
@@ -157,19 +133,53 @@ export default class OpeningAnimationMenu {
|
|
|
157
133
|
*/
|
|
158
134
|
#createButtons() {
|
|
159
135
|
const buttonsState = this.#getButtonsState();
|
|
160
|
-
this.#addButton("closed",
|
|
161
|
-
this.#addButton("close",
|
|
162
|
-
this.#addButton("pause",
|
|
163
|
-
this.#addButton("open",
|
|
164
|
-
this.#addButton("opened",
|
|
165
|
-
this.#addButton("repeat",
|
|
166
|
-
this.#addButton("repeatOff",
|
|
136
|
+
this.#addButton("closed", this.#icon.closed, buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible, this.#callbacks.onGoToClosed);
|
|
137
|
+
this.#addButton("close", this.#icon.close, buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible, this.#callbacks.onClose);
|
|
138
|
+
this.#addButton("pause", this.#icon.pause, buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible, this.#callbacks.onPause);
|
|
139
|
+
this.#addButton("open", this.#icon.open, buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible, this.#callbacks.onOpen);
|
|
140
|
+
this.#addButton("opened", this.#icon.opened, buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible, this.#callbacks.onGoToOpened);
|
|
141
|
+
this.#addButton("repeat", this.#icon.repeat, buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible, this.#callbacks.onToggleLoop);
|
|
142
|
+
this.#addButton("repeatOff", this.#icon.repeatOff, buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible, this.#callbacks.onToggleLoop);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates and configures the animation progress slider.
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
#createSlider() {
|
|
150
|
+
this.#slider = document.createElement("input");
|
|
151
|
+
this.#slider.setAttribute("type", "range");
|
|
152
|
+
this.#slider.setAttribute("min", "0");
|
|
153
|
+
this.#slider.setAttribute("max", "1");
|
|
154
|
+
this.#slider.setAttribute("step", "0.01");
|
|
155
|
+
this.#slider.setAttribute("value", this.#animationProgress.toString());
|
|
156
|
+
this.#slider.classList.add("animation-menu-slider");
|
|
157
|
+
this.#slider.addEventListener("input", this.#onSliderInput.bind(this));
|
|
158
|
+
|
|
159
|
+
this.#containerMain.appendChild(this.#slider);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Initializes and renders the menu UI, including buttons and slider.
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
#createMenu() {
|
|
167
|
+
this.#containerMain = document.createElement("div");
|
|
168
|
+
this.#containerMain.classList.add("pref-viewer-3d");
|
|
169
|
+
this.#containerMain.classList.add("animation-menu");
|
|
170
|
+
this.#containerMain.setAttribute("data-animation-name", this.#name);
|
|
171
|
+
this.#canvas.parentElement.prepend(this.#containerMain);
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this.#
|
|
171
|
-
|
|
172
|
-
this.#
|
|
173
|
+
const style = document.createElement("style");
|
|
174
|
+
style.textContent = PrefViewer3DAnimationMenuStyles;
|
|
175
|
+
this.#containerMain.appendChild(style);
|
|
176
|
+
|
|
177
|
+
this.#containerButtons = document.createElement("div");
|
|
178
|
+
this.#containerButtons.classList.add("animation-menu-buttons");
|
|
179
|
+
this.#containerMain.appendChild(this.#containerButtons);
|
|
180
|
+
|
|
181
|
+
this.#createButtons();
|
|
182
|
+
this.#createSlider();
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
/**
|
|
@@ -179,24 +189,37 @@ export default class OpeningAnimationMenu {
|
|
|
179
189
|
* @returns {Button|null} The button control or null if not found.
|
|
180
190
|
*/
|
|
181
191
|
#getButtonByName(name) {
|
|
182
|
-
return this.#
|
|
192
|
+
return this.#containerButtons.querySelector(`button[name="button_animation_${name}"]`);
|
|
183
193
|
}
|
|
184
194
|
|
|
185
195
|
/**
|
|
186
|
-
*
|
|
196
|
+
* Combines player and loop button states.
|
|
187
197
|
* @private
|
|
188
|
-
* @
|
|
189
|
-
* @param {boolean} enabled
|
|
190
|
-
* @param {boolean} active
|
|
191
|
-
* @param {boolean} visible
|
|
198
|
+
* @returns {object}
|
|
192
199
|
*/
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
#getButtonsState() {
|
|
201
|
+
return Object.assign(this.#getPlayerButtonsState(), this.#getLoopButtonsState());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns the state for loop control buttons.
|
|
206
|
+
* @private
|
|
207
|
+
* @returns {object}
|
|
208
|
+
*/
|
|
209
|
+
#getLoopButtonsState() {
|
|
210
|
+
const buttonsState = {
|
|
211
|
+
repeat: {
|
|
212
|
+
enabled: this.#animationLoop,
|
|
213
|
+
active: false,
|
|
214
|
+
visible: this.#animationLoop,
|
|
215
|
+
},
|
|
216
|
+
repeatOff: {
|
|
217
|
+
enabled: !this.#animationLoop,
|
|
218
|
+
active: false,
|
|
219
|
+
visible: !this.#animationLoop,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
return buttonsState;
|
|
200
223
|
}
|
|
201
224
|
|
|
202
225
|
/**
|
|
@@ -236,33 +259,33 @@ export default class OpeningAnimationMenu {
|
|
|
236
259
|
}
|
|
237
260
|
|
|
238
261
|
/**
|
|
239
|
-
*
|
|
262
|
+
* Updates the visual state of a button (enabled, active, visible).
|
|
240
263
|
* @private
|
|
241
|
-
* @
|
|
264
|
+
* @param {string} name - Button identifier.
|
|
265
|
+
* @param {boolean} enabled
|
|
266
|
+
* @param {boolean} active
|
|
267
|
+
* @param {boolean} visible
|
|
242
268
|
*/
|
|
243
|
-
#
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
visible: !this.#animationLoop,
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
return buttonsState;
|
|
269
|
+
#setButtonState(name, enabled, active, visible = true) {
|
|
270
|
+
const button = this.#getButtonByName(name);
|
|
271
|
+
if (!button) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const classToRemove = ["active", "enabled", "disabled", "hidden", "visible"];
|
|
275
|
+
button.classList.remove(...classToRemove);
|
|
276
|
+
const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
|
|
277
|
+
const visibilityClass = visible ? "visible" : "hidden";
|
|
278
|
+
button.classList.add(stateClass, visibilityClass);
|
|
257
279
|
}
|
|
258
280
|
|
|
259
281
|
/**
|
|
260
|
-
*
|
|
282
|
+
* Updates all loop control buttons according to current loop mode.
|
|
261
283
|
* @private
|
|
262
|
-
* @returns {object}
|
|
263
284
|
*/
|
|
264
|
-
#
|
|
265
|
-
|
|
285
|
+
#setLoopButtonsState() {
|
|
286
|
+
const buttonsState = this.#getLoopButtonsState();
|
|
287
|
+
this.#setButtonState("repeat", buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible);
|
|
288
|
+
this.#setButtonState("repeatOff", buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible);
|
|
266
289
|
}
|
|
267
290
|
|
|
268
291
|
/**
|
|
@@ -279,46 +302,37 @@ export default class OpeningAnimationMenu {
|
|
|
279
302
|
}
|
|
280
303
|
|
|
281
304
|
/**
|
|
282
|
-
*
|
|
305
|
+
* Handles the slider input event for animation progress.
|
|
283
306
|
* @private
|
|
307
|
+
* @param {Event} event - The input event from the slider.
|
|
308
|
+
* @returns {void}
|
|
309
|
+
* @description
|
|
310
|
+
* If the slider value is being set programmatically, the event is ignored to prevent callback loops.
|
|
284
311
|
*/
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
#onSliderInput(event) {
|
|
313
|
+
if (this.#callbacks && typeof this.#callbacks.onSetAnimationProgress === "function") {
|
|
314
|
+
this.#callbacks.onSetAnimationProgress(event.target.value);
|
|
315
|
+
}
|
|
289
316
|
}
|
|
290
317
|
|
|
291
318
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
319
|
+
* ---------------------------
|
|
320
|
+
* Public methods
|
|
321
|
+
* ---------------------------
|
|
294
322
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
thumbColor: this.#colorEnabled,
|
|
309
|
-
};
|
|
310
|
-
this.#slider = new Slider("slider_animation_progress");
|
|
311
|
-
Object.assign(this.#slider, sliderProps);
|
|
312
|
-
this.#slider.onValueChangedObservable.add((value) => {
|
|
313
|
-
if (this.#isSettingSliderValue) {
|
|
314
|
-
this.#isSettingSliderValue = false;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (this.#callbacks && typeof this.#callbacks.onSetAnimationProgress === "function") {
|
|
318
|
-
this.#callbacks.onSetAnimationProgress(value);
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
this.#mainPanel.addControl(this.#slider);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Disposes the animation menu and releases all associated DOM resources.
|
|
326
|
+
* @public
|
|
327
|
+
* @returns {void}
|
|
328
|
+
*/
|
|
329
|
+
dispose() {
|
|
330
|
+
if (this.isVisible) {
|
|
331
|
+
this.#containerMain.remove();
|
|
332
|
+
}
|
|
333
|
+
this.#containerMain = null;
|
|
334
|
+
this.#containerButtons = null;
|
|
335
|
+
this.#slider = null;
|
|
322
336
|
}
|
|
323
337
|
|
|
324
338
|
/**
|
|
@@ -339,14 +353,12 @@ export default class OpeningAnimationMenu {
|
|
|
339
353
|
|
|
340
354
|
/**
|
|
341
355
|
* Sets the animation progress value and updates the slider position.
|
|
342
|
-
* When called, the slider value is updated programmatically without triggering the slider's value change callback.
|
|
343
356
|
* @public
|
|
344
357
|
* @param {number} progress - The new animation progress value (between 0 and 1).
|
|
345
358
|
*/
|
|
346
359
|
set animationProgress(progress) {
|
|
347
360
|
this.#animationProgress = progress;
|
|
348
|
-
this.#
|
|
349
|
-
this.#slider.value = progress;
|
|
361
|
+
this.#slider.value = progress.toString();
|
|
350
362
|
}
|
|
351
363
|
|
|
352
364
|
/**
|
|
@@ -358,4 +370,20 @@ export default class OpeningAnimationMenu {
|
|
|
358
370
|
this.#animationState = state;
|
|
359
371
|
this.#setPlayerButtonsState();
|
|
360
372
|
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* ---------------------------
|
|
376
|
+
* Public getters
|
|
377
|
+
* ---------------------------
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Returns whether the animation menu is currently visible in the DOM.
|
|
382
|
+
* @public
|
|
383
|
+
* @returns {boolean} True if the menu is visible, false otherwise.
|
|
384
|
+
*/
|
|
385
|
+
get isVisible() {
|
|
386
|
+
const menuElement = this.#canvas.parentElement.querySelector(`div.animation-menu[data-animation-name="${this.#name}"]`);
|
|
387
|
+
return !!menuElement;
|
|
388
|
+
}
|
|
361
389
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AdvancedDynamicTexture } from "@babylonjs/gui";
|
|
2
1
|
import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -19,7 +18,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
19
18
|
* - pause(): Pauses the current animation.
|
|
20
19
|
* - goToOpened(): Moves animation to the fully opened state.
|
|
21
20
|
* - goToClosed(): Moves animation to the fully closed state.
|
|
22
|
-
* - showControls(
|
|
21
|
+
* - showControls(canvas): Displays the animation control menu.
|
|
23
22
|
* - hideControls(): Hides the animation control menu.
|
|
24
23
|
* - isControlsVisible(): Returns true if the control menu is visible for this animation.
|
|
25
24
|
*
|
|
@@ -39,14 +38,6 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
|
|
|
39
38
|
* - #checkProgress(progress): Applies threshold logic to progress.
|
|
40
39
|
* - #updateControlsSlider(): Updates the slider in the control menu.
|
|
41
40
|
* - #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
41
|
*/
|
|
51
42
|
export default class OpeningAnimation {
|
|
52
43
|
static states = {
|
|
@@ -68,7 +59,6 @@ export default class OpeningAnimation {
|
|
|
68
59
|
#speedRatio = 1.0;
|
|
69
60
|
#loop = false;
|
|
70
61
|
|
|
71
|
-
#advancedDynamicTexture = null;
|
|
72
62
|
#menu = null;
|
|
73
63
|
#progressThreshold = 0.025;
|
|
74
64
|
|
|
@@ -138,17 +128,18 @@ export default class OpeningAnimation {
|
|
|
138
128
|
#goToOpened(useLoop = false) {
|
|
139
129
|
this.#lastPausedFrame = this.#endFrame;
|
|
140
130
|
|
|
141
|
-
if (!this.#openAnimation.
|
|
142
|
-
this.#openAnimation.
|
|
131
|
+
if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused){
|
|
132
|
+
this.#openAnimation.pause();
|
|
143
133
|
}
|
|
144
|
-
|
|
145
|
-
this.#openAnimation.goToFrame(this.#endFrame);
|
|
134
|
+
this.#openAnimation.goToFrame(this.#endFrame);
|
|
146
135
|
|
|
147
|
-
if (
|
|
136
|
+
if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
|
|
137
|
+
this.#closeAnimation.goToFrame(this.#startFrame);
|
|
138
|
+
} else {
|
|
148
139
|
this.#closeAnimation.start();
|
|
140
|
+
this.#closeAnimation.pause();
|
|
141
|
+
this.#closeAnimation.goToFrame(this.#startFrame);
|
|
149
142
|
}
|
|
150
|
-
this.#closeAnimation.pause();
|
|
151
|
-
this.#closeAnimation.goToFrame(this.#startFrame);
|
|
152
143
|
|
|
153
144
|
this.#state = OpeningAnimation.states.opened;
|
|
154
145
|
this.#updateControls();
|
|
@@ -166,17 +157,18 @@ export default class OpeningAnimation {
|
|
|
166
157
|
#goToClosed(useLoop = false) {
|
|
167
158
|
this.#lastPausedFrame = this.#startFrame;
|
|
168
159
|
|
|
169
|
-
if (!this.#closeAnimation.
|
|
170
|
-
this.#closeAnimation.
|
|
160
|
+
if (this.#closeAnimation._isStarted && !this.#closeAnimation._isPaused) {
|
|
161
|
+
this.#closeAnimation.pause();
|
|
171
162
|
}
|
|
172
|
-
this.#closeAnimation.pause();
|
|
173
163
|
this.#closeAnimation.goToFrame(this.#endFrame - this.#startFrame);
|
|
174
164
|
|
|
175
|
-
if (
|
|
165
|
+
if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
|
|
166
|
+
this.#openAnimation.goToFrame(this.#startFrame);
|
|
167
|
+
} else {
|
|
176
168
|
this.#openAnimation.start();
|
|
169
|
+
this.#openAnimation.pause();
|
|
170
|
+
this.#openAnimation.goToFrame(this.#startFrame);
|
|
177
171
|
}
|
|
178
|
-
this.#openAnimation.pause();
|
|
179
|
-
this.#openAnimation.goToFrame(this.#startFrame);
|
|
180
172
|
|
|
181
173
|
this.#state = OpeningAnimation.states.closed;
|
|
182
174
|
this.#updateControls();
|
|
@@ -328,6 +320,7 @@ export default class OpeningAnimation {
|
|
|
328
320
|
if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
|
|
329
321
|
return;
|
|
330
322
|
}
|
|
323
|
+
|
|
331
324
|
if (this.#state === OpeningAnimation.states.closing) {
|
|
332
325
|
this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
|
|
333
326
|
this.#closeAnimation.pause();
|
|
@@ -352,6 +345,7 @@ export default class OpeningAnimation {
|
|
|
352
345
|
if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
|
|
353
346
|
return;
|
|
354
347
|
}
|
|
348
|
+
|
|
355
349
|
if (this.#state === OpeningAnimation.states.opening) {
|
|
356
350
|
this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
|
|
357
351
|
this.#openAnimation.pause();
|
|
@@ -405,11 +399,9 @@ export default class OpeningAnimation {
|
|
|
405
399
|
* Displays the animation control menu and sets up callbacks.
|
|
406
400
|
* Synchronizes slider and button states with animation.
|
|
407
401
|
* @public
|
|
408
|
-
* @param {
|
|
402
|
+
* @param {HTMLCanvasElement} canvas - The canvas element for rendering.
|
|
409
403
|
*/
|
|
410
|
-
showControls(
|
|
411
|
-
this.#advancedDynamicTexture = advancedDynamicTexture;
|
|
412
|
-
this.#advancedDynamicTexture.metadata = { animationName: this.name };
|
|
404
|
+
showControls(canvas) {
|
|
413
405
|
const controlCallbacks = {
|
|
414
406
|
onGoToOpened: () => {
|
|
415
407
|
if (this.#state === OpeningAnimation.states.opened) {
|
|
@@ -457,7 +449,7 @@ export default class OpeningAnimation {
|
|
|
457
449
|
this.#menu.animationLoop = this.#loop;
|
|
458
450
|
},
|
|
459
451
|
};
|
|
460
|
-
this.#menu = new OpeningAnimationMenu(this
|
|
452
|
+
this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
|
|
461
453
|
|
|
462
454
|
// Attach to Babylon.js scene render loop for real-time updates
|
|
463
455
|
this.#openAnimation._scene.onBeforeRenderObservable.add(this.#updateControlsSlider.bind(this));
|
|
@@ -471,10 +463,9 @@ export default class OpeningAnimation {
|
|
|
471
463
|
if (!this.isControlsVisible()) {
|
|
472
464
|
return;
|
|
473
465
|
}
|
|
474
|
-
// Remove the observer when controls are hidden
|
|
475
466
|
this.#openAnimation._scene.onBeforeRenderObservable.removeCallback(this.#updateControlsSlider.bind(this));
|
|
476
|
-
this.#
|
|
477
|
-
this.#
|
|
467
|
+
this.#menu.dispose();
|
|
468
|
+
this.#menu = null;
|
|
478
469
|
}
|
|
479
470
|
|
|
480
471
|
/**
|
|
@@ -483,7 +474,7 @@ export default class OpeningAnimation {
|
|
|
483
474
|
* @returns {boolean} True if controls are visible for this animation; otherwise, false.
|
|
484
475
|
*/
|
|
485
476
|
isControlsVisible() {
|
|
486
|
-
return !!(this.#
|
|
477
|
+
return !!(this.#menu?.isVisible);
|
|
487
478
|
}
|
|
488
479
|
|
|
489
480
|
/**
|
|
@@ -497,7 +488,6 @@ export default class OpeningAnimation {
|
|
|
497
488
|
* @public
|
|
498
489
|
* @returns {number}
|
|
499
490
|
*/
|
|
500
|
-
|
|
501
491
|
get state() {
|
|
502
492
|
return this.#state;
|
|
503
493
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointerEventTypes, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager } from "@babylonjs/core";
|
|
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
2
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
3
|
import "@babylonjs/loaders";
|
|
4
4
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -9,16 +9,22 @@ import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
|
9
9
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
|
|
12
|
+
* BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, rendering, and user interactions in PrefViewer.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Summary:
|
|
15
|
+
* Provides a high-level API for initializing, loading, displaying, and exporting 3D models and environments using Babylon.js.
|
|
16
|
+
* Manages advanced rendering features, AR integration, asset containers, and user interaction logic.
|
|
17
|
+
*
|
|
18
|
+
* Key Responsibilities:
|
|
15
19
|
* - Initializes and manages the Babylon.js engine, scene, camera, lights, and asset containers.
|
|
16
20
|
* - Handles loading, replacing, and disposing of 3D models, environments, and materials.
|
|
17
21
|
* - Configures advanced rendering features such as Draco mesh compression, shadows, and image-based lighting (IBL).
|
|
18
22
|
* - Integrates Babylon.js WebXR experience for augmented reality (AR) support.
|
|
19
|
-
* - Provides methods for downloading models and scenes in GLB,
|
|
23
|
+
* - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
|
|
20
24
|
* - Manages camera and material options, container visibility, and user interactions.
|
|
21
25
|
* - Observes canvas resize events and updates the engine accordingly.
|
|
26
|
+
* - Designed for integration with PrefViewer and GLTFResolver.
|
|
27
|
+
* - All resource management and rendering operations are performed asynchronously for performance.
|
|
22
28
|
*
|
|
23
29
|
* Usage:
|
|
24
30
|
* - Instantiate: const controller = new BabylonJSController(canvas, containers, options);
|
|
@@ -26,7 +32,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
26
32
|
* - Load assets: await controller.load();
|
|
27
33
|
* - Set camera/material options: controller.setCameraOptions(), controller.setMaterialOptions();
|
|
28
34
|
* - Control visibility: controller.setContainerVisibility(name, show);
|
|
29
|
-
* - Download assets: controller.
|
|
35
|
+
* - Download assets: controller.downloadGLB(), controller.downloadGLTF(), controller.downloadUSDZ();
|
|
30
36
|
* - Disable rendering: controller.disable();
|
|
31
37
|
*
|
|
32
38
|
* Public Methods:
|
|
@@ -40,7 +46,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
40
46
|
* - downloadGLTF(content): Downloads the current scene, model, or environment as a glTF ZIP file.
|
|
41
47
|
* - downloadUSDZ(content): Downloads the current scene, model, or environment as a USDZ file.
|
|
42
48
|
*
|
|
43
|
-
* Private Methods:
|
|
49
|
+
* Private Methods (using ECMAScript private fields):
|
|
44
50
|
* - #configureDracoCompression(): Sets up Draco mesh compression.
|
|
45
51
|
* - #renderLoop(): Babylon.js render loop callback.
|
|
46
52
|
* - #addStylesToARButton(): Styles AR button.
|
|
@@ -58,6 +64,8 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
58
64
|
* - #onKeyUp(event): Handles keyup events for download dialog and shortcuts.
|
|
59
65
|
* - #enableInteraction(): Adds canvas and scene interaction event listeners.
|
|
60
66
|
* - #disableInteraction(): Removes canvas and scene interaction event listeners.
|
|
67
|
+
* - #disposeAnimationController(): Disposes the animation controller if it exists.
|
|
68
|
+
* - #disposeXRExperience(): Disposes the Babylon.js WebXR experience if it exists.
|
|
61
69
|
* - #disposeEngine(): Disposes engine and releases all resources.
|
|
62
70
|
* - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
|
|
63
71
|
* - #setOptions_Materials(): Applies all material options from configuration.
|
|
@@ -77,11 +85,6 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
77
85
|
* - #addDateToName(name): Appends the current date/time to a name string.
|
|
78
86
|
* - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
|
|
79
87
|
* - #openDownloadDialog(): Opens the modal download dialog.
|
|
80
|
-
*
|
|
81
|
-
* Notes:
|
|
82
|
-
* - Designed for integration with PrefViewer and GLTFResolver.
|
|
83
|
-
* - Supports advanced Babylon.js features for product visualization and configurators.
|
|
84
|
-
* - All resource management and rendering operations are performed asynchronously for performance.
|
|
85
88
|
*/
|
|
86
89
|
export default class BabylonJSController {
|
|
87
90
|
// Canvas HTML element
|
|
@@ -463,6 +466,46 @@ export default class BabylonJSController {
|
|
|
463
466
|
}
|
|
464
467
|
}
|
|
465
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Disposes the BabylonJSAnimationController instance if it exists.
|
|
471
|
+
* @private
|
|
472
|
+
* @returns {void}
|
|
473
|
+
*/
|
|
474
|
+
#disposeAnimationController() {
|
|
475
|
+
if (this.#babylonJSAnimationController) {
|
|
476
|
+
this.#babylonJSAnimationController.dispose();
|
|
477
|
+
this.#babylonJSAnimationController = null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Disposes the Babylon.js WebXR experience if it exists.
|
|
483
|
+
* @private
|
|
484
|
+
* @returns {void}
|
|
485
|
+
*/
|
|
486
|
+
#disposeXRExperience() {
|
|
487
|
+
if (!this.#XRExperience) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (this.#XRExperience.baseExperience.state === WebXRState.IN_XR) {
|
|
492
|
+
this.#XRExperience.baseExperience
|
|
493
|
+
.exitXRAsync()
|
|
494
|
+
.then(() => {
|
|
495
|
+
this.#XRExperience.dispose();
|
|
496
|
+
this.#XRExperience = null;
|
|
497
|
+
})
|
|
498
|
+
.catch((error) => {
|
|
499
|
+
console.warn("Error exiting XR experience:", error);
|
|
500
|
+
this.#XRExperience.dispose();
|
|
501
|
+
this.#XRExperience = null;
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
this.#XRExperience.dispose();
|
|
505
|
+
this.#XRExperience = null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
466
509
|
/**
|
|
467
510
|
* Disposes the Babylon.js engine and releases all associated resources.
|
|
468
511
|
* @private
|
|
@@ -476,7 +519,6 @@ export default class BabylonJSController {
|
|
|
476
519
|
this.#engine = this.#scene = this.#camera = null;
|
|
477
520
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
478
521
|
this.#shadowGen = null;
|
|
479
|
-
this.#XRExperience = null;
|
|
480
522
|
}
|
|
481
523
|
|
|
482
524
|
/**
|
|
@@ -893,10 +935,7 @@ export default class BabylonJSController {
|
|
|
893
935
|
|
|
894
936
|
await Promise.allSettled(promiseArray)
|
|
895
937
|
.then((values) => {
|
|
896
|
-
|
|
897
|
-
this.#babylonJSAnimationController.dispose();
|
|
898
|
-
this.#babylonJSAnimationController = null;
|
|
899
|
-
}
|
|
938
|
+
this.#disposeAnimationController();
|
|
900
939
|
values.forEach((result) => {
|
|
901
940
|
const container = result.value ? result.value[0] : null;
|
|
902
941
|
const assetContainer = result.value ? result.value[1] : null;
|
|
@@ -1065,7 +1104,7 @@ export default class BabylonJSController {
|
|
|
1065
1104
|
*/
|
|
1066
1105
|
async enable() {
|
|
1067
1106
|
this.#configureDracoCompression();
|
|
1068
|
-
this.#engine = new Engine(this.#canvas, true, { alpha: true });
|
|
1107
|
+
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1069
1108
|
this.#engine.disableUniformBuffers = true;
|
|
1070
1109
|
this.#scene = new Scene(this.#engine);
|
|
1071
1110
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
@@ -1087,10 +1126,8 @@ export default class BabylonJSController {
|
|
|
1087
1126
|
disable() {
|
|
1088
1127
|
this.#canvasResizeObserver.disconnect();
|
|
1089
1128
|
this.#disableInteraction();
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
this.#babylonJSAnimationController = null;
|
|
1093
|
-
}
|
|
1129
|
+
this.#disposeAnimationController();
|
|
1130
|
+
this.#disposeXRExperience();
|
|
1094
1131
|
this.#disposeEngine();
|
|
1095
1132
|
}
|
|
1096
1133
|
|
package/src/styles.js
CHANGED
|
@@ -88,6 +88,144 @@ export const PrefViewer3DStyles = `
|
|
|
88
88
|
}
|
|
89
89
|
`;
|
|
90
90
|
|
|
91
|
+
export const PrefViewer3DAnimationMenuStyles = `
|
|
92
|
+
div.pref-viewer-3d.animation-menu, div.pref-viewer-3d.animation-menu * {
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
padding: 0;
|
|
95
|
+
margin: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
div.pref-viewer-3d.animation-menu {
|
|
99
|
+
--color-primary: ${PrefViewerColors.primary};
|
|
100
|
+
--color-active: var(--color-primary);
|
|
101
|
+
--color-active-hover: color-mix(in oklab, var(--color-active), black 10%);
|
|
102
|
+
--color-enabled: #7a7a7a;
|
|
103
|
+
--color-enabled-hover: color-mix(in oklab, var(--color-enabled), black 15%);
|
|
104
|
+
--color-disabled: #bdbdbd;
|
|
105
|
+
--color-icon: #FFFFFF;
|
|
106
|
+
--color-border: #FFFFFF;
|
|
107
|
+
--button-size: 32px;
|
|
108
|
+
--icon-size: 24px;
|
|
109
|
+
--button-loop-margin-left: 3px;
|
|
110
|
+
--button-spacing: 2px;
|
|
111
|
+
--slider-thumb-width: 20px;
|
|
112
|
+
--slider-bar-height: 8px;
|
|
113
|
+
--slider-bar-offset: 10px;
|
|
114
|
+
|
|
115
|
+
display: block;
|
|
116
|
+
position: absolute;
|
|
117
|
+
bottom: 10px;
|
|
118
|
+
|
|
119
|
+
right: calc(50% - ((6 * var(--button-size) + 5 * var(--button-spacing) + var(--button-loop-margin-left)) / 2));
|
|
120
|
+
z-index: 1000;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons {
|
|
124
|
+
padding: 0;
|
|
125
|
+
margin: 0;
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: var(--button-spacing);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
padding: 0;
|
|
135
|
+
margin: 0;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
border: 1px solid var(--color-border);
|
|
138
|
+
width: var(--button-size);
|
|
139
|
+
height: var(--button-size);
|
|
140
|
+
background: var(--color-enabled);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button>svg {
|
|
144
|
+
fill: var(--color-icon);
|
|
145
|
+
width: 100%;
|
|
146
|
+
height: 100%;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.active {
|
|
150
|
+
background: var(--color-active);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.active:hover {
|
|
154
|
+
background: var(--color-active-hover);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.enabled {
|
|
158
|
+
background: var(--color-enabled);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.enabled:hover {
|
|
162
|
+
background: var(--color-enabled-hover);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.disabled {
|
|
166
|
+
background: var(--color-disabled);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.visible {
|
|
170
|
+
display: block-flex;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.hidden {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button[name="button_animation_repeat"],
|
|
178
|
+
div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button[name="button_animation_repeatOff"] {
|
|
179
|
+
margin-left: var(--button-loop-margin-left);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
div.pref-viewer-3d.animation-menu>input[type="range"] {
|
|
183
|
+
-webkit-appearance: none;
|
|
184
|
+
width: 100%;
|
|
185
|
+
height: var(--slider-bar-height);
|
|
186
|
+
border: 1px solid var(--color-border);
|
|
187
|
+
outline: none;
|
|
188
|
+
accent-color: var(--color-active);
|
|
189
|
+
background: var(--color-enabled);
|
|
190
|
+
border-radius: var(--slider-bar-height);
|
|
191
|
+
-webkit-transition: .2s;
|
|
192
|
+
transition: background .2s;
|
|
193
|
+
margin: var(--slider-bar-offset) 0px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
div.pref-viewer-3d.animation-menu>input[type="range"]::-webkit-slider-thumb {
|
|
197
|
+
-webkit-appearance: none;
|
|
198
|
+
appearance: none;
|
|
199
|
+
height: var(--slider-thumb-width);
|
|
200
|
+
width: var(--slider-thumb-width);
|
|
201
|
+
border-radius: 50%;
|
|
202
|
+
border: 1px solid var(--color-border);
|
|
203
|
+
background: var(--color-active);
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
-webkit-transition: .2s;
|
|
206
|
+
transition: background .2s;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
div.pref-viewer-3d.animation-menu>input[type="range"]::-webkit-slider-thumb:hover {
|
|
210
|
+
background: var(--color-active-hover);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
div.pref-viewer-3d.animation-menu>input[type="range"]::-moz-range-track {
|
|
214
|
+
height: var(--slider-thumb-width);
|
|
215
|
+
width: var(--slider-thumb-width);
|
|
216
|
+
border-radius: 50%;
|
|
217
|
+
border: 1px solid var(--color-border);
|
|
218
|
+
background: var(--color-active);
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
-webkit-transition: .2s;
|
|
221
|
+
transition: background .2s;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
div.pref-viewer-3d.animation-menu>input[type="range"]::-moz-range-track:hover {
|
|
225
|
+
background: var(--color-active-hover);
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
|
|
91
229
|
export const PrefViewerDialogStyles = `
|
|
92
230
|
pref-viewer-dialog {
|
|
93
231
|
--color-primary: ${PrefViewerColors.primary};
|
|
@@ -98,9 +236,9 @@ export const PrefViewerDialogStyles = `
|
|
|
98
236
|
--dialog-border-radius: 8px;
|
|
99
237
|
--dialog-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
|
|
100
238
|
--button-default-bg-color: #bbbbbb;
|
|
101
|
-
--button-default-bg-color-hover:
|
|
102
|
-
--button-primary-bg-color:
|
|
103
|
-
--button-primary-bg-color-hover: var(--
|
|
239
|
+
--button-default-bg-color-hover: color-mix(in oklab, var(--button-default-bg-color), black 10%);
|
|
240
|
+
--button-primary-bg-color: var(--color-primary);
|
|
241
|
+
--button-primary-bg-color-hover: color-mix(in oklab, var(--button-primary-bg-color), black 10%);
|
|
104
242
|
--button-border-radius: 4px;
|
|
105
243
|
--button-padding-horizontal: 16px;
|
|
106
244
|
--button-padding-vertical: 8px;
|