@preference-sl/pref-viewer 2.11.0-beta.2 → 2.11.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.2",
3
+ "version": "2.11.0-beta.4",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -30,15 +30,15 @@
30
30
  "sideEffects": false,
31
31
  "files": [
32
32
  "src",
33
- "src/models",
34
33
  "index.d.ts"
35
34
  ],
36
35
  "dependencies": {
37
- "@babylonjs/core": "^8.31.3",
38
- "@babylonjs/loaders": "^8.31.3",
39
- "@babylonjs/serializers": "^8.31.3",
36
+ "@babylonjs/core": "^8.36.1",
37
+ "@babylonjs/gui": "^8.36.1",
38
+ "@babylonjs/loaders": "^8.36.1",
39
+ "@babylonjs/serializers": "^8.36.1",
40
40
  "@panzoom/panzoom": "^4.6.0",
41
- "babylonjs-gltf2interface": "^8.31.3",
41
+ "babylonjs-gltf2interface": "^8.36.1",
42
42
  "idb": "^8.0.3",
43
43
  "is-svg": "^6.1.0"
44
44
  },
@@ -0,0 +1,195 @@
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
+ * @class
16
+ */
17
+ export default class BabylonJSAnimationController {
18
+ #scene = null;
19
+ #animatedNodes = [];
20
+ #highlightLayer = null;
21
+ #highlightColor = new Color3(0, 1, 0); // Color para resaltar los elementos animados (Verde)
22
+ #advancedDynamicTexture = null;
23
+ #openingAnimations = [];
24
+
25
+ /**
26
+ * Creates a new BabylonJSAnimationController for a Babylon.js scene.
27
+ * @param {Scene} scene - The Babylon.js scene instance.
28
+ */
29
+ constructor(scene) {
30
+ this.#scene = scene;
31
+ this.#initializeAnimations();
32
+ this.#setupPointerObservers();
33
+ }
34
+
35
+ /**
36
+ * Detects and stores animatable objects and animated nodes in the scene.
37
+ * @private
38
+ */
39
+ #initializeAnimations() {
40
+ if (!this.#scene.animationGroups.length) {
41
+ return;
42
+ }
43
+ this.#getAnimatedNodes();
44
+ this.#getOpeneingAnimations();
45
+ }
46
+
47
+ /**
48
+ * Collects the IDs of all nodes targeted by any animation group in the scene.
49
+ * @private
50
+ */
51
+ #getAnimatedNodes() {
52
+ this.#scene.animationGroups.forEach((animationGroup) => {
53
+ if (!animationGroup._targetedAnimations.length) {
54
+ return;
55
+ }
56
+ animationGroup._targetedAnimations.forEach((targetedAnimation) => {
57
+ if (!this.#animatedNodes.includes(targetedAnimation.target.id)) {
58
+ this.#animatedNodes.push(targetedAnimation.target.id);
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Groups opening and closing animations by node name and creates OpeningAnimation instances.
66
+ * @private
67
+ * @description
68
+ * Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
69
+ */
70
+ #getOpeneingAnimations() {
71
+ const openings = {};
72
+ this.#scene.animationGroups.forEach((animationGroup) => {
73
+ const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
74
+ if (!match) {
75
+ return;
76
+ }
77
+ const [, type, openingName] = match;
78
+ if (!openings[openingName]) {
79
+ openings[openingName] = { name: openingName, animationOpen: null, animationClose: null };
80
+ }
81
+ if (type === "open") {
82
+ openings[openingName].animationOpen = animationGroup;
83
+ } else if (type === "close") {
84
+ openings[openingName].animationClose = animationGroup;
85
+ }
86
+ });
87
+
88
+ Object.values(openings).forEach((opening) => {
89
+ this.#openingAnimations.push(new OpeningAnimation(opening.name, opening.animationOpen, opening.animationClose));
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Finds the OpeningAnimation instance associated with a given node ID.
95
+ * @private
96
+ * @param {string} nodeId - The node identifier.
97
+ * @returns {OpeningAnimation|null} The matching OpeningAnimation instance or null.
98
+ */
99
+ #getOpeningAnimationByNode(nodeId) {
100
+ return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
101
+ }
102
+
103
+ /**
104
+ * Determines if a mesh belongs to a node targeted by an animation.
105
+ * @private
106
+ * @param {Mesh} mesh - The mesh to check.
107
+ * @returns {string|false} The animated node ID if found, otherwise false.
108
+ */
109
+ #getNodeAnimatedByMesh = function (mesh) {
110
+ let nodeId = false;
111
+ let node = mesh;
112
+ while (node.parent !== null && !nodeId) {
113
+ node = node.parent;
114
+ if (this.#animatedNodes.includes(node.id)) {
115
+ nodeId = node.id;
116
+ }
117
+ }
118
+ return nodeId;
119
+ };
120
+
121
+ /**
122
+ * Highlights meshes that are children of an animated node when hovered.
123
+ * @private
124
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
125
+ */
126
+ #hightlightMeshesForAnimation(pickingInfo) {
127
+ if (!this.#highlightLayer) {
128
+ this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
129
+ }
130
+
131
+ this.#highlightLayer.removeAllMeshes();
132
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
133
+ return;
134
+ }
135
+
136
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
137
+ if (!nodeId) {
138
+ return;
139
+ }
140
+
141
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
142
+ const nodeMeshes = transformNode.getChildMeshes();
143
+ if (nodeMeshes.length) {
144
+ nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Sets up pointer observers to highlight animated nodes on hover and show the animation menu on click.
150
+ * @private
151
+ */
152
+ #setupPointerObservers() {
153
+ this.#scene.onPointerObservable.add((pointerInfo) => {
154
+ if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
155
+ const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
156
+ this.#hightlightMeshesForAnimation(pickingInfo);
157
+ }
158
+ if (pointerInfo.type === PointerEventTypes.POINTERUP) {
159
+ // Remove any previously created Babylon GUI
160
+ if (this.#advancedDynamicTexture) {
161
+ this.#advancedDynamicTexture.dispose();
162
+ this.#advancedDynamicTexture = null;
163
+ }
164
+ const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
165
+ this.#showMenu(pickingInfo);
166
+ }
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Displays the animation control menu for the animated node under the pointer.
172
+ * @private
173
+ * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
174
+ * @description
175
+ * Creates the GUI if needed and invokes OpeningAnimation.showControls.
176
+ */
177
+ #showMenu(pickingInfo) {
178
+ if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
179
+ return;
180
+ }
181
+
182
+ const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
183
+ if (!nodeId) {
184
+ return;
185
+ }
186
+ const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
187
+ if (!openingAnimation) {
188
+ return;
189
+ }
190
+ if (!this.#advancedDynamicTexture) {
191
+ this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation");
192
+ }
193
+ openingAnimation.showControls(this.#advancedDynamicTexture);
194
+ }
195
+ }
@@ -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
+ }
@@ -0,0 +1,496 @@
1
+ import { AdvancedDynamicTexture } from "@babylonjs/gui";
2
+ import { OpeningAnimationMenu } from "./babylonjs-animation-opening-menu.js";
3
+
4
+ /**
5
+ * OpeningAnimation - Manages open/close animations for a model part (e.g., a door) in a Babylon.js scene.
6
+ *
7
+ * Responsibilities:
8
+ * - Controls playback of opening and closing AnimationGroups.
9
+ * - Tracks animation state (paused, closed, opened, opening, closing).
10
+ * - Synchronizes animation progress and UI controls.
11
+ * - Handles loop mode and progress threshold logic.
12
+ * - Provides methods for play, pause, go to opened/closed, and progress control.
13
+ * - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
14
+ *
15
+ * Public Methods:
16
+ * - isAnimationForNode(node): Checks if the animation affects the given node.
17
+ * - playOpen(): Starts the opening animation.
18
+ * - playClose(): Starts the closing animation.
19
+ * - pause(): Pauses the current animation.
20
+ * - goToOpened(): Moves animation to the fully opened state.
21
+ * - goToClosed(): Moves animation to the fully closed state.
22
+ * - showControls(advancedDynamicTexture): Displays the animation control menu.
23
+ * - hideControls(): Hides the animation control menu.
24
+ * - isControlsVisible(): Returns true if the control menu is visible for this animation.
25
+ *
26
+ * Public Properties:
27
+ * - state: Returns the current animation state.
28
+ *
29
+ * Private Methods:
30
+ * - #getNodesFromAnimationGroups(): Collects node IDs affected by the animation groups.
31
+ * - #onOpened(): Handles end of opening animation.
32
+ * - #onClosed(): Handles end of closing animation.
33
+ * - #goToOpened(useLoop): Sets state to opened and optionally loops to close.
34
+ * - #goToClosed(useLoop): Sets state to closed and optionally loops to open.
35
+ * - #goToFrameAndPause(frame): Moves to a specific frame and pauses.
36
+ * - #getCurrentFrame(): Gets the current frame based on state.
37
+ * - #getFrameFromProgress(progress): Calculates frame from progress value.
38
+ * - #getProgress(): Calculates progress (0-1) from current frame.
39
+ * - #checkProgress(progress): Applies threshold logic to progress.
40
+ * - #updateControlsSlider(): Updates the slider in the control menu.
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
+ */
51
+ export class OpeningAnimation {
52
+ static states = {
53
+ paused: 0,
54
+ closed: 1,
55
+ opened: 2,
56
+ opening: 3,
57
+ closing: 4,
58
+ };
59
+
60
+ #openAnimation = null;
61
+ #closeAnimation = null;
62
+
63
+ #nodes = [];
64
+ #state = OpeningAnimation.states.closed;
65
+ #lastPausedFrame = 0;
66
+ #startFrame = 0;
67
+ #endFrame = 0;
68
+ #speedRatio = 1.0;
69
+ #loop = false;
70
+
71
+ #advancedDynamicTexture = null;
72
+ #menu = null;
73
+ #progressThreshold = 0.025;
74
+
75
+ /**
76
+ * Creates a new OpeningAnimation instance for managing open/close animations of a model part.
77
+ * @param {string} name - The identifier for this animation (e.g., door name).
78
+ * @param {AnimationGroup} openAnimationGroup - Babylon.js AnimationGroup for the opening animation.
79
+ * @param {AnimationGroup} closeAnimationGroup - Babylon.js AnimationGroup for the closing animation.
80
+ * @description
81
+ * Initializes internal state, sets up frame ranges, collects affected nodes, and attaches end-of-animation observers.
82
+ */
83
+ constructor(name, openAnimationGroup, closeAnimationGroup) {
84
+ this.name = name;
85
+ this.#openAnimation = openAnimationGroup;
86
+ this.#closeAnimation = closeAnimationGroup;
87
+
88
+ this.#openAnimation.stop();
89
+ this.#openAnimation._loopAnimation = false;
90
+ this.#closeAnimation.stop();
91
+ this.#closeAnimation._loopAnimation = false;
92
+
93
+ this.#startFrame = this.#openAnimation.from;
94
+ this.#endFrame = this.#openAnimation.to;
95
+ this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
96
+
97
+ this.#getNodesFromAnimationGroups();
98
+ this.#openAnimation.onAnimationGroupEndObservable.add(this.#onOpened.bind(this));
99
+ this.#closeAnimation.onAnimationGroupEndObservable.add(this.#onClosed.bind(this));
100
+ }
101
+
102
+ /**
103
+ * Collects node IDs affected by the opening and closing animation groups.
104
+ * Populates the #nodes array with unique node identifiers.
105
+ * @private
106
+ */
107
+ #getNodesFromAnimationGroups() {
108
+ [this.#openAnimation, this.#closeAnimation].forEach((animationGroup) => {
109
+ animationGroup._targetedAnimations.forEach((targetedAnimation) => {
110
+ if (!this.#nodes.includes(targetedAnimation.target.id)) {
111
+ this.#nodes.push(targetedAnimation.target.id);
112
+ }
113
+ });
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Handles the end of the opening animation group.
119
+ * @private
120
+ */
121
+ #onOpened() {
122
+ this.#goToOpened(true);
123
+ }
124
+
125
+ /**
126
+ * Handles the end of the closing animation group.
127
+ * @private
128
+ */
129
+ #onClosed() {
130
+ this.#goToClosed(true);
131
+ }
132
+
133
+ /**
134
+ * Moves the animation to the fully opened state, optionally looping to close.
135
+ * @private
136
+ * @param {boolean} useLoop - If true, starts closing animation after opening.
137
+ */
138
+ #goToOpened(useLoop = false) {
139
+ this.#lastPausedFrame = this.#endFrame;
140
+
141
+ if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused) {
142
+ this.#openAnimation.pause();
143
+ }
144
+ this.#openAnimation.goToFrame(this.#endFrame);
145
+
146
+ this.#closeAnimation.pause();
147
+ this.#closeAnimation.goToFrame(this.#startFrame);
148
+
149
+ this.#state = OpeningAnimation.states.opened;
150
+ this.#updateControls();
151
+
152
+ if (this.#loop && useLoop) {
153
+ this.playClose();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Moves the animation to the fully closed state, optionally looping to open.
159
+ * @private
160
+ * @param {boolean} useLoop - If true, starts opening animation after closing.
161
+ */
162
+ #goToClosed(useLoop = false) {
163
+ this.#lastPausedFrame = this.#startFrame;
164
+
165
+ if (this.#closeAnimation._isStarted && !this.#closeAnimation._isPaused) {
166
+ this.#closeAnimation.pause();
167
+ }
168
+ this.#closeAnimation.goToFrame(this.#endFrame - this.#startFrame);
169
+
170
+ this.#openAnimation.pause();
171
+ this.#openAnimation.goToFrame(this.#startFrame);
172
+
173
+ this.#state = OpeningAnimation.states.closed;
174
+ this.#updateControls();
175
+
176
+ if (this.#loop && useLoop) {
177
+ this.playOpen();
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Moves the animation to a specific frame and pauses.
183
+ * @private
184
+ * @param {number} frame - The frame to go to and pause.
185
+ */
186
+ #goToFrameAndPause(frame) {
187
+ if (!frame) {
188
+ return;
189
+ }
190
+
191
+ if (this.#state === OpeningAnimation.states.opening) {
192
+ this.#openAnimation.pause();
193
+ }
194
+ if (this.#state === OpeningAnimation.states.closing) {
195
+ this.#closeAnimation.pause();
196
+ }
197
+
198
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
199
+ this.#openAnimation.goToFrame(frame);
200
+ } else {
201
+ this.#openAnimation.start();
202
+ this.#openAnimation.pause();
203
+ this.#openAnimation.goToFrame(frame);
204
+ }
205
+
206
+ this.#lastPausedFrame = frame;
207
+ this.#state = OpeningAnimation.states.paused;
208
+ this.#updateControls();
209
+ }
210
+
211
+ /**
212
+ * Gets the current frame of the animation based on its state.
213
+ * @private
214
+ * @returns {number} The current frame.
215
+ */
216
+ #getCurrentFrame() {
217
+ let currentFrame;
218
+ if (this.#state === OpeningAnimation.states.opening) {
219
+ currentFrame = this.#openAnimation.getCurrentFrame();
220
+ } else if (this.#state === OpeningAnimation.states.closing) {
221
+ currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
222
+ } else {
223
+ currentFrame = this.#lastPausedFrame;
224
+ }
225
+
226
+ // Ensure currentFrame is within startFrame and endFrame
227
+ if (currentFrame < this.#startFrame) {
228
+ currentFrame = this.#startFrame;
229
+ } else if (currentFrame > this.#endFrame) {
230
+ currentFrame = this.#endFrame;
231
+ }
232
+
233
+ return currentFrame;
234
+ }
235
+
236
+ /**
237
+ * Calculates the frame number from a normalized progress value (0-1).
238
+ * @private
239
+ * @param {number} progress - Progress value between 0 and 1.
240
+ * @returns {number} The corresponding frame.
241
+ */
242
+ #getFrameFromProgress(progress) {
243
+ const frame = this.#startFrame + (this.#endFrame - this.#startFrame) * progress;
244
+ return frame;
245
+ }
246
+
247
+ /**
248
+ * Calculates the normalized progress (0-1) from the current frame.
249
+ * @private
250
+ * @returns {number} Progress value.
251
+ */
252
+ #getProgress() {
253
+ const currentFrame = this.#getCurrentFrame();
254
+ const progress = (currentFrame - this.#startFrame) / (this.#endFrame - this.#startFrame);
255
+ return progress;
256
+ }
257
+
258
+ /**
259
+ * Applies threshold logic to the progress value to snap to 0 or 1 if near the ends.
260
+ * Prevents floating point errors from leaving the animation in an in-between state.
261
+ * @private
262
+ * @param {number} progress - Progress value.
263
+ * @returns {number} Thresholded progress.
264
+ */
265
+ #checkProgress(progress) {
266
+ if (progress <= this.#progressThreshold) {
267
+ progress = 0;
268
+ } else if (progress >= 1 - this.#progressThreshold) {
269
+ progress = 1;
270
+ }
271
+ return progress;
272
+ }
273
+
274
+ /**
275
+ * Updates the slider value in the animation control menu to match the current progress.
276
+ * @private
277
+ */
278
+ #updateControlsSlider() {
279
+ if (!this.isControlsVisible()) {
280
+ return;
281
+ }
282
+ this.#menu.animationProgress = this.#getProgress();
283
+ }
284
+
285
+ /**
286
+ * Updates all controls in the animation menu (buttons, slider) to reflect the current state and progress.
287
+ * @private
288
+ */
289
+ #updateControls() {
290
+ if (!this.isControlsVisible()) {
291
+ return;
292
+ }
293
+ if (!this.#menu) {
294
+ return;
295
+ }
296
+ this.#menu.animationState = this.#state;
297
+ this.#menu.animationProgress = this.#getProgress();
298
+ }
299
+
300
+ /**
301
+ * ---------------------------
302
+ * Public methods
303
+ * ---------------------------
304
+ */
305
+
306
+ /**
307
+ * Checks if the animation affects the given node.
308
+ * @param {string} node - Node identifier.
309
+ * @returns {boolean}
310
+ */
311
+ isAnimationForNode(node) {
312
+ return this.#nodes.includes(node);
313
+ }
314
+
315
+ /**
316
+ * Starts the opening animation.
317
+ * @public
318
+ */
319
+ playOpen() {
320
+ if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
321
+ return;
322
+ }
323
+ if (this.#state === OpeningAnimation.states.closing) {
324
+ this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
325
+ this.#closeAnimation.pause();
326
+ }
327
+
328
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
329
+ this.#openAnimation.goToFrame(this.#lastPausedFrame);
330
+ this.#openAnimation.restart();
331
+ } else {
332
+ this.#openAnimation.start(false, this.#speedRatio, this.#lastPausedFrame, this.#endFrame, undefined);
333
+ }
334
+
335
+ this.#state = OpeningAnimation.states.opening;
336
+ this.#updateControls();
337
+ }
338
+
339
+ /**
340
+ * Starts the closing animation.
341
+ * @public
342
+ */
343
+ playClose() {
344
+ if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
345
+ return;
346
+ }
347
+ if (this.#state === OpeningAnimation.states.opening) {
348
+ this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
349
+ this.#openAnimation.pause();
350
+ }
351
+
352
+ if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
353
+ this.#closeAnimation.goToFrame(this.#endFrame - this.#lastPausedFrame);
354
+ this.#closeAnimation.restart();
355
+ } else {
356
+ this.#closeAnimation.start(false, this.#speedRatio, this.#endFrame - this.#lastPausedFrame, this.#endFrame, undefined);
357
+ }
358
+
359
+ this.#state = OpeningAnimation.states.closing;
360
+ this.#updateControls();
361
+ }
362
+
363
+ /**
364
+ * Pauses the current animation.
365
+ * @public
366
+ */
367
+ pause() {
368
+ if (this.#state === OpeningAnimation.states.opening) {
369
+ this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
370
+ this.#openAnimation.pause();
371
+ }
372
+ if (this.#state === OpeningAnimation.states.closing) {
373
+ this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
374
+ this.#closeAnimation.pause();
375
+ }
376
+ this.#state = OpeningAnimation.states.paused;
377
+ this.#updateControls();
378
+ }
379
+
380
+ /**
381
+ * Moves animation to the fully opened state.
382
+ * @public
383
+ */
384
+ goToOpened() {
385
+ this.#goToOpened(false);
386
+ }
387
+
388
+ /**
389
+ * Moves animation to the fully closed state.
390
+ * @public
391
+ */
392
+ goToClosed() {
393
+ this.#goToClosed(false);
394
+ }
395
+
396
+ /**
397
+ * Displays the animation control menu and sets up callbacks.
398
+ * Synchronizes slider and button states with animation.
399
+ * @public
400
+ * @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture.
401
+ */
402
+ showControls(advancedDynamicTexture) {
403
+ this.#advancedDynamicTexture = advancedDynamicTexture;
404
+ this.#advancedDynamicTexture.metadata = { animationName: this.name };
405
+ const controlCallbacks = {
406
+ onGoToOpened: () => {
407
+ if (this.#state === OpeningAnimation.states.opened) {
408
+ return;
409
+ }
410
+ this.goToOpened();
411
+ },
412
+ onOpen: () => {
413
+ if (this.#state === OpeningAnimation.states.opened || this.#state === OpeningAnimation.states.opening) {
414
+ return;
415
+ }
416
+ this.playOpen();
417
+ },
418
+ onPause: () => {
419
+ if (this.#state === OpeningAnimation.states.paused || this.#state === OpeningAnimation.states.closed || this.#state === OpeningAnimation.states.opened) {
420
+ return;
421
+ }
422
+ this.pause();
423
+ },
424
+ onClose: () => {
425
+ if (this.#state === OpeningAnimation.states.closed || this.#state === OpeningAnimation.states.closing) {
426
+ return;
427
+ }
428
+ this.playClose();
429
+ },
430
+ onGoToClosed: () => {
431
+ if (this.#state === OpeningAnimation.states.closed) {
432
+ return;
433
+ }
434
+ this.goToClosed();
435
+ },
436
+ onSetAnimationProgress: (progress) => {
437
+ progress = this.#checkProgress(progress);
438
+ if (progress === 0) {
439
+ this.goToClosed();
440
+ } else if (progress === 1) {
441
+ this.goToOpened();
442
+ } else {
443
+ const frame = this.#getFrameFromProgress(progress);
444
+ this.#goToFrameAndPause(frame);
445
+ }
446
+ },
447
+ onToggleLoop: () => {
448
+ this.#loop = !this.#loop;
449
+ this.#menu.animationLoop = this.#loop;
450
+ },
451
+ };
452
+ this.#menu = new OpeningAnimationMenu(this.#advancedDynamicTexture, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
453
+
454
+ // Attach to Babylon.js scene render loop for real-time updates
455
+ this.#openAnimation._scene.onBeforeRenderObservable.add(this.#updateControlsSlider.bind(this));
456
+ }
457
+
458
+ /**
459
+ * Hides the animation control menu and removes observers.
460
+ * @public
461
+ */
462
+ hideControls() {
463
+ if (!this.isControlsVisible()) {
464
+ return;
465
+ }
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;
470
+ }
471
+
472
+ /**
473
+ * Checks if the animation controls menu is currently visible for this animation instance.
474
+ * @public
475
+ * @returns {boolean} True if controls are visible for this animation; otherwise, false.
476
+ */
477
+ isControlsVisible() {
478
+ return !!(this.#advancedDynamicTexture && this.#advancedDynamicTexture.metadata?.animationName === this.name && this.#menu);
479
+ }
480
+
481
+ /**
482
+ * ---------------------------
483
+ * Public properties
484
+ * ---------------------------
485
+ */
486
+
487
+ /**
488
+ * Returns the current animation state.
489
+ * @public
490
+ * @returns {number}
491
+ */
492
+
493
+ get state() {
494
+ return this.#state;
495
+ }
496
+ }
@@ -1,9 +1,12 @@
1
- import { Engine, Scene, ArcRotateCamera, Vector3, Color4, HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, LoadAssetContainerAsync, Tools, WebXRSessionManager, WebXRDefaultExperience, MeshBuilder, WebXRFeatureName, HDRCubeTexture, IblShadowsRenderPipeline } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager } from "@babylonjs/core";
2
2
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression";
3
3
  import "@babylonjs/loaders";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression";
5
5
  import { USDZExportAsync, GLTF2Export } from "@babylonjs/serializers";
6
+
6
7
  import GLTFResolver from "./gltf-resolver.js";
8
+ import { ContainerData, MaterialData } from "./pref-viewer-3d-data.js";
9
+ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
7
10
 
8
11
  /**
9
12
  * BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
@@ -76,6 +79,7 @@ export default class BabylonJSController {
76
79
  #options = {};
77
80
 
78
81
  #gltfResolver = null; // GLTFResolver instance
82
+ #babylonJSAnimationController = null; // AnimationController instance
79
83
 
80
84
  /**
81
85
  * Constructs a new BabylonJSController instance.
@@ -414,7 +418,7 @@ export default class BabylonJSController {
414
418
  /**
415
419
  * Applies material options from the configuration to the relevant meshes.
416
420
  * @private
417
- * @param {object} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
421
+ * @param {MaterialData} optionMaterial - Material option containing value, nodePrefixes, nodeNames, and state.
418
422
  * @returns {boolean} True if any mesh material was set, false otherwise.
419
423
  */
420
424
  #setOptionsMaterial(optionMaterial) {
@@ -542,7 +546,7 @@ export default class BabylonJSController {
542
546
  * Finds and returns the asset container object by its name.
543
547
  * @private
544
548
  * @param {string} name - The name of the container to find.
545
- * @returns {object|null} The matching container object, or null if not found.
549
+ * @returns {ContainerData|null} The matching container object, or null if not found.
546
550
  */
547
551
  #findContainerByName(name) {
548
552
  return Object.values(this.#containers).find((container) => container.state.name === name) || null;
@@ -595,7 +599,7 @@ export default class BabylonJSController {
595
599
  /**
596
600
  * Adds the asset container to the Babylon.js scene if it should be shown and is not already visible.
597
601
  * @private
598
- * @param {object} container - The container object containing asset state and metadata.
602
+ * @param {ContainerData} container - The container object containing asset state and metadata.
599
603
  * @returns {boolean} True if the container was added, false otherwise.
600
604
  */
601
605
  #addContainer(container) {
@@ -611,7 +615,7 @@ export default class BabylonJSController {
611
615
  /**
612
616
  * Removes the asset container from the Babylon.js scene if it is currently visible.
613
617
  * @private
614
- * @param {object} container - The container object containing asset state and metadata.
618
+ * @param {ContainerData} container - The container object containing asset state and metadata.
615
619
  * @returns {boolean} True if the container was removed, false otherwise.
616
620
  */
617
621
  #removeContainer(container) {
@@ -627,9 +631,9 @@ export default class BabylonJSController {
627
631
  /**
628
632
  * Replaces the asset container in the Babylon.js scene with a new one.
629
633
  * @private
630
- * @param {object} container - The container object containing asset state and metadata.
631
- * @param {object} newAssetContainer - The new asset container to add to the scene.
632
- * @returns {boolean} True if the container was replaced, false otherwise.
634
+ * @param {ContainerData} container - The container object containing asset state and metadata.
635
+ * @param {AssetContainer} newAssetContainer - The new asset container to add to the scene.
636
+ * @returns {boolean} True if the container was replaced and added, false otherwise.
633
637
  */
634
638
  #replaceContainer(container, newAssetContainer) {
635
639
  if (container.assetContainer) {
@@ -639,8 +643,7 @@ export default class BabylonJSController {
639
643
  }
640
644
  this.#scene.getEngine().releaseEffects();
641
645
  container.assetContainer = newAssetContainer;
642
- this.#addContainer(container);
643
- return true;
646
+ return this.#addContainer(container);
644
647
  }
645
648
  /**
646
649
  * Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
@@ -663,7 +666,6 @@ export default class BabylonJSController {
663
666
  * @private
664
667
  * @returns {void}
665
668
  */
666
- #stopR;
667
669
  #stopRender() {
668
670
  this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
669
671
  }
@@ -681,8 +683,8 @@ export default class BabylonJSController {
681
683
  /**
682
684
  * Loads an asset container (model, environment, materials, etc.) using the provided container state.
683
685
  * @private
684
- * @param {object} container - The container object containing asset state and metadata.
685
- * @returns {Promise<[object, AssetContainer|boolean]>} Resolves to an array with the container and the loaded asset container, or false if loading fails.
686
+ * @param {ContainerData} container - The container object containing asset state and metadata.
687
+ * @returns {Promise<[ContainerData, AssetContainer|boolean]>} Resolves to an array with the container and the loaded asset container, or false if loading fails.
686
688
  * @description
687
689
  * Resolves the asset source using GLTFResolver, prepares plugin options, and loads the asset into the Babylon.js scene.
688
690
  * Updates the container's cache data and returns the container along with the loaded asset container or false if loading fails.
@@ -753,7 +755,10 @@ export default class BabylonJSController {
753
755
  if (container.state.name === "model") {
754
756
  assetContainer.lights = [];
755
757
  }
756
- this.#replaceContainer(container, assetContainer);
758
+ const replacedAndAdded = this.#replaceContainer(container, assetContainer);
759
+ if (replacedAndAdded && container.state.name === "model") {
760
+ this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
761
+ }
757
762
  container.state.setSuccess(true);
758
763
  } else {
759
764
  if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
package/src/index.js CHANGED
@@ -270,7 +270,7 @@ class PrefViewer extends HTMLElement {
270
270
 
271
271
  const customEventOptions = {
272
272
  bubbles: true,
273
- cancelable: false,
273
+ cancelable: true,
274
274
  composed: true,
275
275
  };
276
276
  if (detail) {
@@ -294,7 +294,7 @@ class PrefViewer extends HTMLElement {
294
294
 
295
295
  const customEventOptions = {
296
296
  bubbles: true,
297
- cancelable: false,
297
+ cancelable: true,
298
298
  composed: true,
299
299
  };
300
300
  this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
@@ -316,7 +316,7 @@ class PrefViewer extends HTMLElement {
316
316
 
317
317
  const customEventOptions = {
318
318
  bubbles: true,
319
- cancelable: false,
319
+ cancelable: true,
320
320
  composed: true,
321
321
  };
322
322
  if (detail) {
@@ -333,9 +333,11 @@ class PrefViewer extends HTMLElement {
333
333
  * @returns {void}
334
334
  */
335
335
  #on2DZoomChanged(event) {
336
+ event.stopPropagation();
337
+ event.preventDefault();
336
338
  const customEventOptions = {
337
339
  bubbles: true,
338
- cancelable: false,
340
+ cancelable: true,
339
341
  composed: true,
340
342
  detail: event.detail,
341
343
  };
@@ -221,9 +221,9 @@ export class PrefViewer2D extends HTMLElement {
221
221
  const error = "PrefViewer2D: Error in SVG provided for loading. Ensure the SVG is a URL to an SVG file, a string containing a SVG, or a string containing a base64-encoded SVG.";
222
222
  const detail = { error: new Error(error) };
223
223
  const customEventOptions = {
224
- bubbles: true,
225
- cancelable: false,
226
- composed: true,
224
+ bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
225
+ cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
226
+ composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
227
227
  detail: detail,
228
228
  };
229
229
  this.dispatchEvent(new CustomEvent("drawing-error", customEventOptions));
@@ -238,8 +238,8 @@ export class PrefViewer2D extends HTMLElement {
238
238
  #onLoaded() {
239
239
  const customEventOptions = {
240
240
  bubbles: true,
241
- cancelable: false,
242
- composed: true,
241
+ cancelable: true,
242
+ composed: false,
243
243
  };
244
244
  this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
245
245
 
@@ -258,8 +258,8 @@ export class PrefViewer2D extends HTMLElement {
258
258
  #onLoading() {
259
259
  const customEventOptions = {
260
260
  bubbles: true,
261
- cancelable: false,
262
- composed: true,
261
+ cancelable: true,
262
+ composed: false,
263
263
  };
264
264
  this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
265
265
 
@@ -285,10 +285,11 @@ export class PrefViewer2D extends HTMLElement {
285
285
  #onPanzoomChanged(state) {
286
286
  const customEventOptions = {
287
287
  bubbles: true,
288
- cancelable: false,
289
- composed: true,
288
+ cancelable: true,
289
+ composed: false,
290
290
  detail: state,
291
291
  };
292
+ customEventOptions.detail.desde2d = "true";
292
293
  this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
293
294
  }
294
295
 
@@ -1,4 +1,4 @@
1
- import { ContainerData, MaterialData, CameraData } from "./pref-viewer-3d-data.js";
1
+ import { CameraData, ContainerData, MaterialData } from "./pref-viewer-3d-data.js";
2
2
  import BabylonJSController from "./babylonjs-controller.js";
3
3
 
4
4
  /**
@@ -303,9 +303,9 @@ export class PrefViewer3D extends HTMLElement {
303
303
  */
304
304
  #onLoading() {
305
305
  const customEventOptions = {
306
- bubbles: true,
307
- cancelable: false,
308
- composed: true,
306
+ bubbles: true, // indicates whether the event bubbles up through the DOM tree or not
307
+ cancelable: true, // indicates whether the event can be canceled, and therefore prevented as if the event never happened
308
+ composed: false, // indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM
309
309
  };
310
310
  this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
311
311
 
@@ -351,8 +351,8 @@ export class PrefViewer3D extends HTMLElement {
351
351
 
352
352
  const customEventOptions = {
353
353
  bubbles: true,
354
- cancelable: false,
355
- composed: true,
354
+ cancelable: true,
355
+ composed: false,
356
356
  detail: detail,
357
357
  };
358
358
  this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
@@ -375,8 +375,8 @@ export class PrefViewer3D extends HTMLElement {
375
375
  #onSettingOptions() {
376
376
  const customEventOptions = {
377
377
  bubbles: true,
378
- cancelable: false,
379
- composed: true,
378
+ cancelable: true,
379
+ composed: false,
380
380
  };
381
381
  this.dispatchEvent(new CustomEvent("scene-setting-options", customEventOptions));
382
382
 
@@ -416,8 +416,8 @@ export class PrefViewer3D extends HTMLElement {
416
416
 
417
417
  const customEventOptions = {
418
418
  bubbles: true,
419
- cancelable: false,
420
- composed: true,
419
+ cancelable: true,
420
+ composed: false,
421
421
  detail: detail,
422
422
  };
423
423
  this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions));