@preference-sl/pref-viewer 2.11.0-beta.1 → 2.11.0-beta.11
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 +12 -8
- package/src/babylonjs-animation-controller.js +235 -0
- package/src/babylonjs-animation-opening-menu.js +360 -0
- package/src/babylonjs-animation-opening.js +496 -0
- package/src/babylonjs-controller.js +343 -86
- package/src/css/pref-viewer-2d.css +39 -0
- package/src/css/pref-viewer-3d.css +28 -0
- package/src/css/pref-viewer-dialog.css +105 -0
- package/src/css/pref-viewer.css +11 -0
- package/src/file-storage.js +166 -39
- package/src/index.js +318 -81
- package/src/pref-viewer-2d.js +67 -47
- package/src/pref-viewer-3d.js +328 -84
- package/src/pref-viewer-dialog.js +140 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.11.0-beta.
|
|
3
|
+
"version": "2.11.0-beta.11",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"copy-css": "cpx \"src/css/**/*\" dist/css/",
|
|
7
8
|
"release": "standard-version --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
|
|
8
9
|
"release:beta": "standard-version --prerelease beta --releaseCommitMessageFormat \"chore(release): {{currentTag}} [skip ci]\"",
|
|
9
|
-
"build": "esbuild src/index.js --bundle --platform=node --outfile=dist/bundle.js --sourcemap",
|
|
10
|
+
"build": "npm run copy-css && esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/bundle.js --sourcemap",
|
|
10
11
|
"start": "npm run build && http-server -c-1 . -p 8080",
|
|
11
12
|
"test": "vitest"
|
|
12
13
|
},
|
|
@@ -30,19 +31,22 @@
|
|
|
30
31
|
"sideEffects": false,
|
|
31
32
|
"files": [
|
|
32
33
|
"src",
|
|
33
|
-
"src/
|
|
34
|
+
"src/css",
|
|
34
35
|
"index.d.ts"
|
|
35
36
|
],
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@babylonjs/core": "^8.
|
|
38
|
-
"@babylonjs/
|
|
39
|
-
"@babylonjs/
|
|
38
|
+
"@babylonjs/core": "^8.36.1",
|
|
39
|
+
"@babylonjs/gui": "^8.36.1",
|
|
40
|
+
"@babylonjs/loaders": "^8.36.1",
|
|
41
|
+
"@babylonjs/serializers": "^8.36.1",
|
|
40
42
|
"@panzoom/panzoom": "^4.6.0",
|
|
41
|
-
"babylonjs-gltf2interface": "^8.
|
|
43
|
+
"babylonjs-gltf2interface": "^8.36.1",
|
|
42
44
|
"idb": "^8.0.3",
|
|
43
|
-
"is-svg": "^6.1.0"
|
|
45
|
+
"is-svg": "^6.1.0",
|
|
46
|
+
"jszip": "^3.10.1"
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
49
|
+
"cpx": "^1.5.0",
|
|
46
50
|
"esbuild": "^0.25.10",
|
|
47
51
|
"http-server": "^14.1.1",
|
|
48
52
|
"jsdom": "^26.1.0",
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Color3, HighlightLayer, Mesh, PickingInfo, PointerEventTypes, Scene } from "@babylonjs/core";
|
|
2
|
+
import { AdvancedDynamicTexture } from "@babylonjs/gui";
|
|
3
|
+
import { OpeningAnimation } from "./babylonjs-animation-opening.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BabylonJSAnimationController - Manages animation playback and interactive highlighting for model containers in Babylon.js scenes.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Detects and groups opening/closing animations in the scene.
|
|
10
|
+
* - Tracks animated transformation nodes and their relationships to meshes.
|
|
11
|
+
* - Highlights animated nodes and their child meshes when hovered.
|
|
12
|
+
* - Displays and disposes the animation control menu (GUI) for animated nodes.
|
|
13
|
+
* - Provides API for interaction and animation control.
|
|
14
|
+
*
|
|
15
|
+
* Public Methods:
|
|
16
|
+
* - dispose(): Disposes all resources managed by the animation controller.
|
|
17
|
+
*
|
|
18
|
+
* @class
|
|
19
|
+
*/
|
|
20
|
+
export default class BabylonJSAnimationController {
|
|
21
|
+
#scene = null;
|
|
22
|
+
#animatedNodes = [];
|
|
23
|
+
#highlightLayer = null;
|
|
24
|
+
#highlightColor = new Color3(0, 1, 0); // Color para resaltar los elementos animados (Verde)
|
|
25
|
+
#advancedDynamicTexture = null;
|
|
26
|
+
#openingAnimations = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new BabylonJSAnimationController for a Babylon.js scene.
|
|
30
|
+
* @param {Scene} scene - The Babylon.js scene instance.
|
|
31
|
+
*/
|
|
32
|
+
constructor(scene) {
|
|
33
|
+
this.#scene = scene;
|
|
34
|
+
this.#initializeAnimations();
|
|
35
|
+
this.#setupPointerObservers();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detects and stores animatable objects and animated nodes in the scene.
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
#initializeAnimations() {
|
|
43
|
+
if (!this.#scene.animationGroups.length) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.#getAnimatedNodes();
|
|
47
|
+
this.#getOpeneingAnimations();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collects the IDs of all nodes targeted by any animation group in the scene.
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
#getAnimatedNodes() {
|
|
55
|
+
this.#scene.animationGroups.forEach((animationGroup) => {
|
|
56
|
+
animationGroup.stop();
|
|
57
|
+
if (!animationGroup._targetedAnimations.length) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
animationGroup._targetedAnimations.forEach((targetedAnimation) => {
|
|
61
|
+
if (!this.#animatedNodes.includes(targetedAnimation.target.id)) {
|
|
62
|
+
this.#animatedNodes.push(targetedAnimation.target.id);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Groups opening and closing animations by node name and creates OpeningAnimation instances.
|
|
70
|
+
* @private
|
|
71
|
+
* @description
|
|
72
|
+
* Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
|
|
73
|
+
*/
|
|
74
|
+
#getOpeneingAnimations() {
|
|
75
|
+
const openings = {};
|
|
76
|
+
this.#scene.animationGroups.forEach((animationGroup) => {
|
|
77
|
+
const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const [, type, openingName] = match;
|
|
82
|
+
if (!openings[openingName]) {
|
|
83
|
+
openings[openingName] = { name: openingName, animationOpen: null, animationClose: null };
|
|
84
|
+
}
|
|
85
|
+
if (type === "open") {
|
|
86
|
+
openings[openingName].animationOpen = animationGroup;
|
|
87
|
+
} else if (type === "close") {
|
|
88
|
+
openings[openingName].animationClose = animationGroup;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
Object.values(openings).forEach((opening) => {
|
|
93
|
+
this.#openingAnimations.push(new OpeningAnimation(opening.name, opening.animationOpen, opening.animationClose));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Finds the OpeningAnimation instance associated with a given node ID.
|
|
99
|
+
* @private
|
|
100
|
+
* @param {string} nodeId - The node identifier.
|
|
101
|
+
* @returns {OpeningAnimation|null} The matching OpeningAnimation instance or null.
|
|
102
|
+
*/
|
|
103
|
+
#getOpeningAnimationByNode(nodeId) {
|
|
104
|
+
return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Determines if a mesh belongs to a node targeted by an animation.
|
|
109
|
+
* @private
|
|
110
|
+
* @param {Mesh} mesh - The mesh to check.
|
|
111
|
+
* @returns {string|false} The animated node ID if found, otherwise false.
|
|
112
|
+
*/
|
|
113
|
+
#getNodeAnimatedByMesh = function (mesh) {
|
|
114
|
+
let nodeId = false;
|
|
115
|
+
let node = mesh;
|
|
116
|
+
while (node.parent !== null && !nodeId) {
|
|
117
|
+
node = node.parent;
|
|
118
|
+
if (this.#animatedNodes.includes(node.id)) {
|
|
119
|
+
nodeId = node.id;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return nodeId;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Highlights meshes that are children of an animated node when hovered.
|
|
127
|
+
* @private
|
|
128
|
+
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
129
|
+
*/
|
|
130
|
+
#hightlightMeshesForAnimation(pickingInfo) {
|
|
131
|
+
if (!this.#highlightLayer) {
|
|
132
|
+
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.#highlightLayer.removeAllMeshes();
|
|
136
|
+
if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
|
|
141
|
+
if (!nodeId) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
146
|
+
const nodeMeshes = transformNode.getChildMeshes();
|
|
147
|
+
if (nodeMeshes.length) {
|
|
148
|
+
nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Sets up pointer observers to highlight animated nodes on hover and show the animation menu on click.
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
#setupPointerObservers() {
|
|
157
|
+
if (this.#openingAnimations.length === 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#scene.onPointerObservable.add(this.#onAnimationPointerObservable.bind(this));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handles pointer events in the Babylon.js scene for animation interaction.
|
|
165
|
+
* On pointer move, highlights meshes belonging to animated nodes under the cursor.
|
|
166
|
+
* On pointer up (click), disposes any existing GUI and displays the animation control menu for the selected node.
|
|
167
|
+
*
|
|
168
|
+
* @private
|
|
169
|
+
* @param {PointerInfo} pointerInfo - The pointer event information from Babylon.js.
|
|
170
|
+
*/
|
|
171
|
+
#onAnimationPointerObservable(pointerInfo) {
|
|
172
|
+
if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
|
|
173
|
+
const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
|
|
174
|
+
this.#hightlightMeshesForAnimation(pickingInfo);
|
|
175
|
+
}
|
|
176
|
+
if (pointerInfo.type === PointerEventTypes.POINTERUP) {
|
|
177
|
+
// Remove any previously created Babylon GUI
|
|
178
|
+
if (this.#advancedDynamicTexture) {
|
|
179
|
+
this.#advancedDynamicTexture.dispose();
|
|
180
|
+
this.#advancedDynamicTexture = null;
|
|
181
|
+
}
|
|
182
|
+
const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
|
|
183
|
+
this.#showMenu(pickingInfo);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Displays the animation control menu for the animated node under the pointer.
|
|
189
|
+
* @private
|
|
190
|
+
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
191
|
+
* @description
|
|
192
|
+
* Creates the GUI if needed and invokes OpeningAnimation.showControls.
|
|
193
|
+
*/
|
|
194
|
+
#showMenu(pickingInfo) {
|
|
195
|
+
if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
|
|
200
|
+
if (!nodeId) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
|
|
204
|
+
if (!openingAnimation) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!this.#advancedDynamicTexture) {
|
|
208
|
+
this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation");
|
|
209
|
+
}
|
|
210
|
+
openingAnimation.showControls(this.#advancedDynamicTexture);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Disposes all resources managed by the animation controller.
|
|
215
|
+
* Cleans up the highlight layer, GUI texture, and internal animation/node lists.
|
|
216
|
+
* Should be called when the controller is no longer needed to prevent memory leaks.
|
|
217
|
+
* @public
|
|
218
|
+
*/
|
|
219
|
+
dispose() {
|
|
220
|
+
if (this.#highlightLayer) {
|
|
221
|
+
this.#highlightLayer.dispose();
|
|
222
|
+
this.#highlightLayer = null;
|
|
223
|
+
}
|
|
224
|
+
if (this.#advancedDynamicTexture) {
|
|
225
|
+
this.#advancedDynamicTexture.dispose();
|
|
226
|
+
this.#advancedDynamicTexture = null;
|
|
227
|
+
}
|
|
228
|
+
this.#animatedNodes = [];
|
|
229
|
+
this.#openingAnimations = [];
|
|
230
|
+
const observer = this.#scene.onPointerObservable._observers.find((observer) => observer.callback.name.includes("#onAnimationPointerObservable"));
|
|
231
|
+
if (observer) {
|
|
232
|
+
this.#scene.onPointerObservable.remove(observer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { AdvancedDynamicTexture, Button, Control, Image, Slider, StackPanel } from "@babylonjs/gui";
|
|
2
|
+
import { OpeningAnimation } from "./babylonjs-animation-opening.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OpeningAnimationMenu - Manages and renders the animation control menu for opening/closing animations in a Babylon.js scene.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* - Renders a GUI menu with buttons for controlling animation playback (open, close, pause, go to opened/closed, loop).
|
|
9
|
+
* - Updates button states and slider position based on animation state, progress, and loop mode.
|
|
10
|
+
* - Handles user interactions and invokes provided callbacks for animation actions.
|
|
11
|
+
* - Synchronizes the slider value with animation progress, avoiding callback loops.
|
|
12
|
+
*
|
|
13
|
+
* Public Setters:
|
|
14
|
+
* - set animationState(state): Updates the animation state and button states.
|
|
15
|
+
* - set animationProgress(progress): Updates the animation progress and slider value.
|
|
16
|
+
* - set animationLoop(loop): Updates the loop mode and loop button states.
|
|
17
|
+
*
|
|
18
|
+
* Private Methods:
|
|
19
|
+
* - #createMenu(): Initializes and renders the menu UI.
|
|
20
|
+
* - #addButton(name, imageURL, enabled, active, visible, callback): Adds a button to the menu with specified properties.
|
|
21
|
+
* - #createButtons(): Creates all control buttons and sets their initial states.
|
|
22
|
+
* - #getButtonByName(name): Retrieves a button control by its name.
|
|
23
|
+
* - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
|
|
24
|
+
* - #getPlayerButtonsState(): Returns the state (enabled, active, visible) for playback control buttons.
|
|
25
|
+
* - #getLoopButtonsState(): Returns the state for loop control buttons.
|
|
26
|
+
* - #getButtonsState(): Combines player and loop button states.
|
|
27
|
+
* - #setPlayerButtonsState(): Updates all playback control buttons.
|
|
28
|
+
* - #setLoopButtonsState(): Updates all loop control buttons.
|
|
29
|
+
* - #createSlider(): Creates and configures the animation progress slider.
|
|
30
|
+
*
|
|
31
|
+
* Usage Example:
|
|
32
|
+
* const menu = new OpeningAnimationMenu(adt, state, progress, loop, {
|
|
33
|
+
* onOpen: () => { ... },
|
|
34
|
+
* onClose: () => { ... },
|
|
35
|
+
* onPause: () => { ... },
|
|
36
|
+
* onGoToOpened: () => { ... },
|
|
37
|
+
* onGoToClosed: () => { ... },
|
|
38
|
+
* onToggleLoop: () => { ... },
|
|
39
|
+
* onSetAnimationProgress: (progress) => { ... }
|
|
40
|
+
* });
|
|
41
|
+
* menu.animationState = OpeningAnimation.states.opening;
|
|
42
|
+
* menu.animationProgress = 0.5;
|
|
43
|
+
* menu.animationLoop = true;
|
|
44
|
+
*/
|
|
45
|
+
export class OpeningAnimationMenu {
|
|
46
|
+
#animationState = OpeningAnimation.states.closed;
|
|
47
|
+
#animationProgress = 0;
|
|
48
|
+
#animationLoop = false;
|
|
49
|
+
#callbacks = null;
|
|
50
|
+
|
|
51
|
+
// GUI Elements
|
|
52
|
+
#advancedDynamicTexture = null;
|
|
53
|
+
#mainPanel = null;
|
|
54
|
+
#secondaryPanel = null;
|
|
55
|
+
#slider = null;
|
|
56
|
+
|
|
57
|
+
// Style properties
|
|
58
|
+
#buttonSize = 28;
|
|
59
|
+
#buttonLoopPaddingLeft = 3;
|
|
60
|
+
#colorActive = "#6BA53A";
|
|
61
|
+
#colorEnabled = "#333333";
|
|
62
|
+
#colorDisabled = "#777777";
|
|
63
|
+
#colorIcon = "#FFFFFF";
|
|
64
|
+
#colorBorder = "#FFFFFF";
|
|
65
|
+
#sliderThumbWidth = 20;
|
|
66
|
+
#sliderBarOffset = 10;
|
|
67
|
+
#icon = {
|
|
68
|
+
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 fill="${this.#colorIcon}" d="M16,18.86V4.86l-11,7,11,7Z"/></svg>`,
|
|
69
|
+
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 fill="${this.#colorIcon}" d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12"/></svg>`,
|
|
70
|
+
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 fill="${this.#colorIcon}" d="M8,5.14v14l11-7-11-7Z"/></svg>`,
|
|
71
|
+
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 fill="${this.#colorIcon}" d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12"/></svg>`,
|
|
72
|
+
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 fill="${this.#colorIcon}" d="M14,19H18V5H14M6,19H10V5H6V19Z"/></svg>`,
|
|
73
|
+
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 fill="${this.#colorIcon}" d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z"/></svg>`,
|
|
74
|
+
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 fill="${this.#colorIcon}" 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>`,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
#isSettingSliderValue = false;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Constructs the OpeningAnimationMenu and initializes the menu UI.
|
|
81
|
+
* @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture for rendering controls.
|
|
82
|
+
* @param {number} animationState - Current animation state (enum).
|
|
83
|
+
* @param {number} animationProgress - Current animation progress (0 to 1).
|
|
84
|
+
* @param {boolean} animationLoop - Whether the animation is set to loop.
|
|
85
|
+
* @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
constructor(advancedDynamicTexture, animationState, animationProgress, animationLoop, callbacks) {
|
|
89
|
+
this.#advancedDynamicTexture = advancedDynamicTexture;
|
|
90
|
+
this.#animationState = animationState;
|
|
91
|
+
this.#animationProgress = animationProgress;
|
|
92
|
+
this.#animationLoop = animationLoop;
|
|
93
|
+
this.#callbacks = callbacks;
|
|
94
|
+
|
|
95
|
+
this.#createMenu(animationState);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Initializes and renders the menu UI, including buttons and slider.
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
#createMenu() {
|
|
103
|
+
if (!this.#advancedDynamicTexture) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.#mainPanel = new StackPanel();
|
|
107
|
+
this.#mainPanel.isVertical = true;
|
|
108
|
+
this.#secondaryPanel = new StackPanel();
|
|
109
|
+
this.#secondaryPanel.isVertical = false;
|
|
110
|
+
this.#secondaryPanel.height = `${this.#buttonSize}px`;
|
|
111
|
+
this.#mainPanel.addControl(this.#secondaryPanel);
|
|
112
|
+
|
|
113
|
+
this.#advancedDynamicTexture.addControl(this.#mainPanel);
|
|
114
|
+
this.#mainPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
|
115
|
+
this.#mainPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
|
|
116
|
+
|
|
117
|
+
this.#createButtons();
|
|
118
|
+
this.#createSlider();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Internal helper to add a button to the menu.
|
|
123
|
+
* Sets button appearance and attaches the callback for user interaction.
|
|
124
|
+
* @private
|
|
125
|
+
* @param {string} name - Button identifier.
|
|
126
|
+
* @param {string} imageURL - SVG image data URL for the button icon.
|
|
127
|
+
* @param {boolean} enabled - Whether the button is enabled.
|
|
128
|
+
* @param {boolean} active - Whether the button is visually active.
|
|
129
|
+
* @param {boolean} visible - Whether the button is visible.
|
|
130
|
+
* @param {function} callback - Callback to invoke on button click.
|
|
131
|
+
*/
|
|
132
|
+
#addButton(name, imageURL, enabled = true, active = false, visible = true, callback) {
|
|
133
|
+
const buttonProps = {
|
|
134
|
+
background: active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled,
|
|
135
|
+
color: this.#colorBorder,
|
|
136
|
+
cornerRadius: 0,
|
|
137
|
+
height: `${this.#buttonSize}px`,
|
|
138
|
+
hoverCursor: "pointer",
|
|
139
|
+
width: `${this.#buttonSize}px`,
|
|
140
|
+
isVisible: visible,
|
|
141
|
+
};
|
|
142
|
+
const button = Button.CreateImageOnlyButton(`button_animation_${name}`, imageURL);
|
|
143
|
+
Object.assign(button, buttonProps);
|
|
144
|
+
button.image.stretch = Image.STRETCH_UNIFORM;
|
|
145
|
+
button.onPointerUpObservable.add(() => {
|
|
146
|
+
if (callback && typeof callback === "function") {
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
this.#secondaryPanel.addControl(button);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates all control buttons and sets their initial states.
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
#createButtons() {
|
|
158
|
+
const buttonsState = this.#getButtonsState();
|
|
159
|
+
this.#addButton("closed", `data:image/svg+xml,${encodeURIComponent(this.#icon.closed)}`, buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible, this.#callbacks.onGoToClosed);
|
|
160
|
+
this.#addButton("close", `data:image/svg+xml,${encodeURIComponent(this.#icon.close)}`, buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible, this.#callbacks.onClose);
|
|
161
|
+
this.#addButton("pause", `data:image/svg+xml,${encodeURIComponent(this.#icon.pause)}`, buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible, this.#callbacks.onPause);
|
|
162
|
+
this.#addButton("open", `data:image/svg+xml,${encodeURIComponent(this.#icon.open)}`, buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible, this.#callbacks.onOpen);
|
|
163
|
+
this.#addButton("opened", `data:image/svg+xml,${encodeURIComponent(this.#icon.opened)}`, buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible, this.#callbacks.onGoToOpened);
|
|
164
|
+
this.#addButton("repeat", `data:image/svg+xml,${encodeURIComponent(this.#icon.repeat)}`, buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible, this.#callbacks.onToggleLoop);
|
|
165
|
+
this.#addButton("repeatOff", `data:image/svg+xml,${encodeURIComponent(this.#icon.repeatOff)}`, buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible, this.#callbacks.onToggleLoop);
|
|
166
|
+
|
|
167
|
+
// Adjust padding for loop buttons
|
|
168
|
+
this.#getButtonByName("repeat").paddingLeft = `${this.#buttonLoopPaddingLeft}px`;
|
|
169
|
+
this.#getButtonByName("repeat").width = `${this.#buttonSize + this.#buttonLoopPaddingLeft}px`;
|
|
170
|
+
this.#getButtonByName("repeatOff").paddingLeft = `${this.#buttonLoopPaddingLeft}px`;
|
|
171
|
+
this.#getButtonByName("repeatOff").width = `${this.#buttonSize + this.#buttonLoopPaddingLeft}px`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Retrieves a button control by its name.
|
|
176
|
+
* @private
|
|
177
|
+
* @param {string} name - Button identifier.
|
|
178
|
+
* @returns {Button|null} The button control or null if not found.
|
|
179
|
+
*/
|
|
180
|
+
#getButtonByName(name) {
|
|
181
|
+
return this.#advancedDynamicTexture.getControlByName(`button_animation_${name}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Updates the visual state of a button (enabled, active, visible).
|
|
186
|
+
* @private
|
|
187
|
+
* @param {string} name - Button identifier.
|
|
188
|
+
* @param {boolean} enabled
|
|
189
|
+
* @param {boolean} active
|
|
190
|
+
* @param {boolean} visible
|
|
191
|
+
*/
|
|
192
|
+
#setButtonState(name, enabled, active, visible = true) {
|
|
193
|
+
const button = this.#getButtonByName(name);
|
|
194
|
+
if (!button) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
button.background = active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled;
|
|
198
|
+
button.isVisible = visible;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the state (enabled, active, visible) for playback control buttons.
|
|
203
|
+
* @private
|
|
204
|
+
* @returns {object}
|
|
205
|
+
*/
|
|
206
|
+
#getPlayerButtonsState() {
|
|
207
|
+
const buttonsState = {
|
|
208
|
+
opened: {
|
|
209
|
+
enabled: this.#animationState !== OpeningAnimation.states.opened,
|
|
210
|
+
active: false,
|
|
211
|
+
visible: true,
|
|
212
|
+
},
|
|
213
|
+
open: {
|
|
214
|
+
enabled: this.#animationState !== OpeningAnimation.states.opened && this.#animationState !== OpeningAnimation.states.opening,
|
|
215
|
+
active: this.#animationState === OpeningAnimation.states.opening,
|
|
216
|
+
visible: true,
|
|
217
|
+
},
|
|
218
|
+
pause: {
|
|
219
|
+
enabled: this.#animationState !== OpeningAnimation.states.paused && this.#animationState !== OpeningAnimation.states.closed && this.#animationState !== OpeningAnimation.states.opened,
|
|
220
|
+
active: this.#animationState === OpeningAnimation.states.paused,
|
|
221
|
+
visible: true,
|
|
222
|
+
},
|
|
223
|
+
close: {
|
|
224
|
+
enabled: this.#animationState !== OpeningAnimation.states.closed && this.#animationState !== OpeningAnimation.states.closing,
|
|
225
|
+
active: this.#animationState === OpeningAnimation.states.closing,
|
|
226
|
+
visible: true,
|
|
227
|
+
},
|
|
228
|
+
closed: {
|
|
229
|
+
enabled: this.#animationState !== OpeningAnimation.states.closed,
|
|
230
|
+
active: false,
|
|
231
|
+
visible: true,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
return buttonsState;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Returns the state for loop control buttons.
|
|
239
|
+
* @private
|
|
240
|
+
* @returns {object}
|
|
241
|
+
*/
|
|
242
|
+
#getLoopButtonsState() {
|
|
243
|
+
const buttonsState = {
|
|
244
|
+
repeat: {
|
|
245
|
+
enabled: this.#animationLoop,
|
|
246
|
+
active: false,
|
|
247
|
+
visible: this.#animationLoop,
|
|
248
|
+
},
|
|
249
|
+
repeatOff: {
|
|
250
|
+
enabled: !this.#animationLoop,
|
|
251
|
+
active: false,
|
|
252
|
+
visible: !this.#animationLoop,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
return buttonsState;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Combines player and loop button states.
|
|
260
|
+
* @private
|
|
261
|
+
* @returns {object}
|
|
262
|
+
*/
|
|
263
|
+
#getButtonsState() {
|
|
264
|
+
return Object.assign(this.#getPlayerButtonsState(), this.#getLoopButtonsState());
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Updates all playback control buttons according to current animation state.
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
#setPlayerButtonsState() {
|
|
272
|
+
const buttonsState = this.#getPlayerButtonsState();
|
|
273
|
+
this.#setButtonState("opened", buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible);
|
|
274
|
+
this.#setButtonState("open", buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible);
|
|
275
|
+
this.#setButtonState("pause", buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible);
|
|
276
|
+
this.#setButtonState("close", buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible);
|
|
277
|
+
this.#setButtonState("closed", buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Updates all loop control buttons according to current loop mode.
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
#setLoopButtonsState() {
|
|
285
|
+
const buttonsState = this.#getLoopButtonsState();
|
|
286
|
+
this.#setButtonState("repeat", buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible);
|
|
287
|
+
this.#setButtonState("repeatOff", buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Creates and configures the animation progress slider.
|
|
292
|
+
* @private
|
|
293
|
+
*/
|
|
294
|
+
#createSlider() {
|
|
295
|
+
const sliderProps = {
|
|
296
|
+
minimum: 0,
|
|
297
|
+
maximum: 1,
|
|
298
|
+
value: this.#animationProgress,
|
|
299
|
+
height: `${this.#buttonSize}px`,
|
|
300
|
+
width: `${this.#buttonSize * 7 + this.#buttonLoopPaddingLeft}px`, // Width based on number of buttons visible
|
|
301
|
+
barOffset: `${this.#sliderBarOffset}px`,
|
|
302
|
+
isThumbCircle: true,
|
|
303
|
+
thumbWidth: `${this.#sliderThumbWidth}px`,
|
|
304
|
+
background: this.#colorDisabled,
|
|
305
|
+
color: this.#colorEnabled,
|
|
306
|
+
borderColor: this.#colorBorder,
|
|
307
|
+
thumbColor: this.#colorEnabled,
|
|
308
|
+
};
|
|
309
|
+
this.#slider = new Slider("slider_animation_progress");
|
|
310
|
+
Object.assign(this.#slider, sliderProps);
|
|
311
|
+
this.#slider.onValueChangedObservable.add((value) => {
|
|
312
|
+
if (this.#isSettingSliderValue) {
|
|
313
|
+
this.#isSettingSliderValue = false;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (this.#callbacks && typeof this.#callbacks.onSetAnimationProgress === "function") {
|
|
317
|
+
this.#callbacks.onSetAnimationProgress(value);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
this.#mainPanel.addControl(this.#slider);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* ---------------------------
|
|
325
|
+
* Public setters
|
|
326
|
+
* ---------------------------
|
|
327
|
+
*/
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Sets the animation loop mode and updates loop button states.
|
|
331
|
+
* @public
|
|
332
|
+
* @param {boolean} loop
|
|
333
|
+
*/
|
|
334
|
+
set animationLoop(loop) {
|
|
335
|
+
this.#animationLoop = loop;
|
|
336
|
+
this.#setLoopButtonsState();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Sets the animation progress value and updates the slider position.
|
|
341
|
+
* When called, the slider value is updated programmatically without triggering the slider's value change callback.
|
|
342
|
+
* @public
|
|
343
|
+
* @param {number} progress - The new animation progress value (between 0 and 1).
|
|
344
|
+
*/
|
|
345
|
+
set animationProgress(progress) {
|
|
346
|
+
this.#animationProgress = progress;
|
|
347
|
+
this.#isSettingSliderValue = true;
|
|
348
|
+
this.#slider.value = progress;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Sets the animation state and updates playback button states.
|
|
353
|
+
* @public
|
|
354
|
+
* @param {number} state - The new animation state (enum).
|
|
355
|
+
*/
|
|
356
|
+
set animationState(state) {
|
|
357
|
+
this.#animationState = state;
|
|
358
|
+
this.#setPlayerButtonsState();
|
|
359
|
+
}
|
|
360
|
+
}
|