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

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.
@@ -0,0 +1,527 @@
1
+ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
2
+
3
+ /**
4
+ * OpeningAnimation - Manages open/close animations for a model part (e.g., a door) in a Babylon.js scene.
5
+ *
6
+ * Responsibilities:
7
+ * - Controls playback of opening and closing AnimationGroups.
8
+ * - Tracks animation state (paused, closed, opened, opening, closing).
9
+ * - Synchronizes animation progress and UI controls.
10
+ * - Handles loop mode and progress threshold logic.
11
+ * - Provides methods for play, pause, go to opened/closed, and progress control.
12
+ * - Manages the animation control menu (OpeningAnimationMenu) and its callbacks.
13
+ *
14
+ * Public Methods:
15
+ * - dispose(): Disposes the OpeningAnimation instance and releases all associated resources.
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(canvas): 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
+ export default class OpeningAnimation {
44
+ static states = {
45
+ paused: 0,
46
+ closed: 1,
47
+ opened: 2,
48
+ opening: 3,
49
+ closing: 4,
50
+ };
51
+
52
+ name = "";
53
+ #openAnimation = null;
54
+ #closeAnimation = null;
55
+ #nodes = [];
56
+ #menu = null;
57
+
58
+ #state = OpeningAnimation.states.closed;
59
+ #lastPausedFrame = 0;
60
+ #startFrame = 0;
61
+ #endFrame = 0;
62
+ #speedRatio = 1.0;
63
+ #loop = false;
64
+ #progressThreshold = 0.025;
65
+
66
+ #handlers = {
67
+ onOpened: null,
68
+ onClosed: null,
69
+ updateControlsSlider: null,
70
+ };
71
+
72
+ /**
73
+ * Creates a new OpeningAnimation instance for managing open/close animations of a model part.
74
+ * @param {string} name - The identifier for this animation (e.g., door name).
75
+ * @param {AnimationGroup} openAnimationGroup - Babylon.js AnimationGroup for the opening animation.
76
+ * @param {AnimationGroup} closeAnimationGroup - Babylon.js AnimationGroup for the closing animation.
77
+ * @description
78
+ * Initializes internal state, sets up frame ranges, collects affected nodes, and attaches end-of-animation observers.
79
+ */
80
+ constructor(name, openAnimationGroup, closeAnimationGroup) {
81
+ this.name = name;
82
+ this.#openAnimation = openAnimationGroup;
83
+ this.#closeAnimation = closeAnimationGroup;
84
+
85
+ this.#openAnimation.stop();
86
+ this.#openAnimation._loopAnimation = false;
87
+ this.#closeAnimation.stop();
88
+ this.#closeAnimation._loopAnimation = false;
89
+
90
+ this.#startFrame = this.#openAnimation.from;
91
+ this.#endFrame = this.#openAnimation.to;
92
+ this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
93
+
94
+ this.#getNodesFromAnimationGroups();
95
+ this.#bindHandlers();
96
+ this.#openAnimation.onAnimationGroupEndObservable.add(this.#handlers.onOpened);
97
+ this.#closeAnimation.onAnimationGroupEndObservable.add(this.#handlers.onClosed);
98
+ }
99
+
100
+ #bindHandlers() {
101
+ this.#handlers.onOpened = this.#onOpened.bind(this);
102
+ this.#handlers.onClosed = this.#onClosed.bind(this);
103
+ this.#handlers.updateControlsSlider = this.#updateControlsSlider.bind(this);
104
+ }
105
+
106
+ /**
107
+ * Collects node IDs affected by the opening and closing animation groups.
108
+ * Populates the #nodes array with unique node identifiers.
109
+ * @private
110
+ */
111
+ #getNodesFromAnimationGroups() {
112
+ [this.#openAnimation, this.#closeAnimation].forEach((animationGroup) => {
113
+ animationGroup._targetedAnimations.forEach((targetedAnimation) => {
114
+ if (!this.#nodes.includes(targetedAnimation.target.id)) {
115
+ this.#nodes.push(targetedAnimation.target.id);
116
+ }
117
+ });
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Handles the end of the opening animation group.
123
+ * @private
124
+ */
125
+ #onOpened() {
126
+ this.#goToOpened(true);
127
+ }
128
+
129
+ /**
130
+ * Handles the end of the closing animation group.
131
+ * @private
132
+ */
133
+ #onClosed() {
134
+ this.#goToClosed(true);
135
+ }
136
+
137
+ /**
138
+ * Moves the animation to the fully opened state, optionally looping to close.
139
+ * @private
140
+ * @param {boolean} useLoop - If true, starts closing animation after opening.
141
+ */
142
+ #goToOpened(useLoop = false) {
143
+ this.#lastPausedFrame = this.#endFrame;
144
+
145
+ if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused){
146
+ this.#openAnimation.pause();
147
+ }
148
+ this.#openAnimation.goToFrame(this.#endFrame);
149
+
150
+ if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
151
+ this.#closeAnimation.goToFrame(this.#startFrame);
152
+ } else {
153
+ this.#closeAnimation.start();
154
+ this.#closeAnimation.pause();
155
+ this.#closeAnimation.goToFrame(this.#startFrame);
156
+ }
157
+
158
+ this.#state = OpeningAnimation.states.opened;
159
+ this.#updateControls();
160
+
161
+ if (this.#loop && useLoop) {
162
+ this.playClose();
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Moves the animation to the fully closed state, optionally looping to open.
168
+ * @private
169
+ * @param {boolean} useLoop - If true, starts opening animation after closing.
170
+ */
171
+ #goToClosed(useLoop = false) {
172
+ this.#lastPausedFrame = this.#startFrame;
173
+
174
+ if (this.#closeAnimation._isStarted && !this.#closeAnimation._isPaused) {
175
+ this.#closeAnimation.pause();
176
+ }
177
+ this.#closeAnimation.goToFrame(this.#endFrame - this.#startFrame);
178
+
179
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
180
+ this.#openAnimation.goToFrame(this.#startFrame);
181
+ } else {
182
+ this.#openAnimation.start();
183
+ this.#openAnimation.pause();
184
+ this.#openAnimation.goToFrame(this.#startFrame);
185
+ }
186
+
187
+ this.#state = OpeningAnimation.states.closed;
188
+ this.#updateControls();
189
+
190
+ if (this.#loop && useLoop) {
191
+ this.playOpen();
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Moves the animation to a specific frame and pauses.
197
+ * @private
198
+ * @param {number} frame - The frame to go to and pause.
199
+ */
200
+ #goToFrameAndPause(frame) {
201
+ if (!frame) {
202
+ return;
203
+ }
204
+
205
+ if (this.#state === OpeningAnimation.states.opening) {
206
+ this.#openAnimation.pause();
207
+ }
208
+ if (this.#state === OpeningAnimation.states.closing) {
209
+ this.#closeAnimation.pause();
210
+ }
211
+
212
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
213
+ this.#openAnimation.goToFrame(frame);
214
+ } else {
215
+ this.#openAnimation.start();
216
+ this.#openAnimation.pause();
217
+ this.#openAnimation.goToFrame(frame);
218
+ }
219
+
220
+ this.#lastPausedFrame = frame;
221
+ this.#state = OpeningAnimation.states.paused;
222
+ this.#updateControls();
223
+ }
224
+
225
+ /**
226
+ * Gets the current frame of the animation based on its state.
227
+ * @private
228
+ * @returns {number} The current frame.
229
+ */
230
+ #getCurrentFrame() {
231
+ let currentFrame;
232
+ if (this.#state === OpeningAnimation.states.opening) {
233
+ currentFrame = this.#openAnimation.getCurrentFrame();
234
+ } else if (this.#state === OpeningAnimation.states.closing) {
235
+ currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
236
+ } else {
237
+ currentFrame = this.#lastPausedFrame;
238
+ }
239
+
240
+ // Ensure currentFrame is within startFrame and endFrame
241
+ if (currentFrame < this.#startFrame) {
242
+ currentFrame = this.#startFrame;
243
+ } else if (currentFrame > this.#endFrame) {
244
+ currentFrame = this.#endFrame;
245
+ }
246
+
247
+ return currentFrame;
248
+ }
249
+
250
+ /**
251
+ * Calculates the frame number from a normalized progress value (0-1).
252
+ * @private
253
+ * @param {number} progress - Progress value between 0 and 1.
254
+ * @returns {number} The corresponding frame.
255
+ */
256
+ #getFrameFromProgress(progress) {
257
+ const frame = this.#startFrame + (this.#endFrame - this.#startFrame) * progress;
258
+ return frame;
259
+ }
260
+
261
+ /**
262
+ * Calculates the normalized progress (0-1) from the current frame.
263
+ * @private
264
+ * @returns {number} Progress value.
265
+ */
266
+ #getProgress() {
267
+ const currentFrame = this.#getCurrentFrame();
268
+ const progress = (currentFrame - this.#startFrame) / (this.#endFrame - this.#startFrame);
269
+ return progress;
270
+ }
271
+
272
+ /**
273
+ * Applies threshold logic to the progress value to snap to 0 or 1 if near the ends.
274
+ * Prevents floating point errors from leaving the animation in an in-between state.
275
+ * @private
276
+ * @param {number} progress - Progress value.
277
+ * @returns {number} Thresholded progress.
278
+ */
279
+ #checkProgress(progress) {
280
+ if (progress <= this.#progressThreshold) {
281
+ progress = 0;
282
+ } else if (progress >= 1 - this.#progressThreshold) {
283
+ progress = 1;
284
+ }
285
+ return progress;
286
+ }
287
+
288
+ /**
289
+ * Updates the slider value in the animation control menu to match the current progress.
290
+ * @private
291
+ */
292
+ #updateControlsSlider() {
293
+ if (!this.isControlsVisible()) {
294
+ return;
295
+ }
296
+ this.#menu.animationProgress = this.#getProgress();
297
+ }
298
+
299
+ /**
300
+ * Updates all controls in the animation menu (buttons, slider) to reflect the current state and progress.
301
+ * @private
302
+ */
303
+ #updateControls() {
304
+ if (!this.isControlsVisible()) {
305
+ return;
306
+ }
307
+ if (!this.#menu) {
308
+ return;
309
+ }
310
+ this.#menu.animationState = this.#state;
311
+ this.#menu.animationProgress = this.#getProgress();
312
+ }
313
+
314
+ /**
315
+ * ---------------------------
316
+ * Public methods
317
+ * ---------------------------
318
+ */
319
+
320
+ /**
321
+ * Disposes the OpeningAnimation instance and releases all associated resources.
322
+ * @public
323
+ * @returns {void}
324
+ */
325
+ dispose() {
326
+ this.hideControls();
327
+ if (this.#openAnimation) {
328
+ this.#openAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onOpened);
329
+ this.#openAnimation = null;
330
+ }
331
+ if (this.#closeAnimation) {
332
+ this.#closeAnimation.onAnimationGroupEndObservable.removeCallback(this.#handlers.onClosed);
333
+ this.#closeAnimation = null;
334
+ }
335
+ this.#nodes = [];
336
+ this.name = "";
337
+ }
338
+
339
+ /**
340
+ * Checks if the animation affects the given node.
341
+ * @param {string} node - Node identifier.
342
+ * @returns {boolean}
343
+ */
344
+ isAnimationForNode(node) {
345
+ return this.#nodes.includes(node);
346
+ }
347
+
348
+ /**
349
+ * Starts the opening animation.
350
+ * @public
351
+ */
352
+ playOpen() {
353
+ if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
354
+ return;
355
+ }
356
+
357
+ if (this.#state === OpeningAnimation.states.closing) {
358
+ this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
359
+ this.#closeAnimation.pause();
360
+ }
361
+
362
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
363
+ this.#openAnimation.goToFrame(this.#lastPausedFrame);
364
+ this.#openAnimation.restart();
365
+ } else {
366
+ this.#openAnimation.start(false, this.#speedRatio, this.#lastPausedFrame, this.#endFrame, undefined);
367
+ }
368
+
369
+ this.#state = OpeningAnimation.states.opening;
370
+ this.#updateControls();
371
+ }
372
+
373
+ /**
374
+ * Starts the closing animation.
375
+ * @public
376
+ */
377
+ playClose() {
378
+ if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
379
+ return;
380
+ }
381
+
382
+ if (this.#state === OpeningAnimation.states.opening) {
383
+ this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
384
+ this.#openAnimation.pause();
385
+ }
386
+
387
+ if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
388
+ this.#closeAnimation.goToFrame(this.#endFrame - this.#lastPausedFrame);
389
+ this.#closeAnimation.restart();
390
+ } else {
391
+ this.#closeAnimation.start(false, this.#speedRatio, this.#endFrame - this.#lastPausedFrame, this.#endFrame, undefined);
392
+ }
393
+
394
+ this.#state = OpeningAnimation.states.closing;
395
+ this.#updateControls();
396
+ }
397
+
398
+ /**
399
+ * Pauses the current animation.
400
+ * @public
401
+ */
402
+ pause() {
403
+ if (this.#state === OpeningAnimation.states.opening) {
404
+ this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
405
+ this.#openAnimation.pause();
406
+ }
407
+ if (this.#state === OpeningAnimation.states.closing) {
408
+ this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
409
+ this.#closeAnimation.pause();
410
+ }
411
+ this.#state = OpeningAnimation.states.paused;
412
+ this.#updateControls();
413
+ }
414
+
415
+ /**
416
+ * Moves animation to the fully opened state.
417
+ * @public
418
+ */
419
+ goToOpened() {
420
+ this.#goToOpened(false);
421
+ }
422
+
423
+ /**
424
+ * Moves animation to the fully closed state.
425
+ * @public
426
+ */
427
+ goToClosed() {
428
+ this.#goToClosed(false);
429
+ }
430
+
431
+ /**
432
+ * Displays the animation control menu and sets up callbacks.
433
+ * Synchronizes slider and button states with animation.
434
+ * @public
435
+ * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
436
+ */
437
+ showControls(canvas) {
438
+ const controlCallbacks = {
439
+ onGoToOpened: () => {
440
+ if (this.#state === OpeningAnimation.states.opened) {
441
+ return;
442
+ }
443
+ this.goToOpened();
444
+ },
445
+ onOpen: () => {
446
+ if (this.#state === OpeningAnimation.states.opened || this.#state === OpeningAnimation.states.opening) {
447
+ return;
448
+ }
449
+ this.playOpen();
450
+ },
451
+ onPause: () => {
452
+ if (this.#state === OpeningAnimation.states.paused || this.#state === OpeningAnimation.states.closed || this.#state === OpeningAnimation.states.opened) {
453
+ return;
454
+ }
455
+ this.pause();
456
+ },
457
+ onClose: () => {
458
+ if (this.#state === OpeningAnimation.states.closed || this.#state === OpeningAnimation.states.closing) {
459
+ return;
460
+ }
461
+ this.playClose();
462
+ },
463
+ onGoToClosed: () => {
464
+ if (this.#state === OpeningAnimation.states.closed) {
465
+ return;
466
+ }
467
+ this.goToClosed();
468
+ },
469
+ onSetAnimationProgress: (progress) => {
470
+ progress = this.#checkProgress(progress);
471
+ if (progress === 0) {
472
+ this.goToClosed();
473
+ } else if (progress === 1) {
474
+ this.goToOpened();
475
+ } else {
476
+ const frame = this.#getFrameFromProgress(progress);
477
+ this.#goToFrameAndPause(frame);
478
+ }
479
+ },
480
+ onToggleLoop: () => {
481
+ this.#loop = !this.#loop;
482
+ this.#menu.animationLoop = this.#loop;
483
+ },
484
+ };
485
+ this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
486
+
487
+ // Attach to Babylon.js scene render loop for real-time updates
488
+ this.#openAnimation._scene.onBeforeRenderObservable.add(this.#handlers.updateControlsSlider);
489
+ }
490
+
491
+ /**
492
+ * Hides the animation control menu and removes observers.
493
+ * @public
494
+ */
495
+ hideControls() {
496
+ if (!this.isControlsVisible()) {
497
+ return;
498
+ }
499
+ this.#openAnimation?._scene?.onBeforeRenderObservable.removeCallback(this.#handlers.updateControlsSlider);
500
+ this.#menu.dispose();
501
+ this.#menu = null;
502
+ }
503
+
504
+ /**
505
+ * Checks if the animation controls menu is currently visible for this animation instance.
506
+ * @public
507
+ * @returns {boolean} True if controls are visible for this animation; otherwise, false.
508
+ */
509
+ isControlsVisible() {
510
+ return !!(this.#menu?.isVisible);
511
+ }
512
+
513
+ /**
514
+ * ---------------------------
515
+ * Public properties
516
+ * ---------------------------
517
+ */
518
+
519
+ /**
520
+ * Returns the current animation state.
521
+ * @public
522
+ * @returns {number}
523
+ */
524
+ get state() {
525
+ return this.#state;
526
+ }
527
+ }