@preference-sl/pref-viewer 2.11.0-beta.2 → 2.11.0-beta.3
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 +7 -6
- package/src/babylonjs-animation-controller.js +536 -0
- package/src/babylonjs-controller.js +9 -5
- package/src/images/icon-pause.svg +1 -0
- package/src/images/icon-play-backwards.svg +1 -0
- package/src/images/icon-play.svg +1 -0
- package/src/images/icon-skip-backward.svg +1 -0
- package/src/images/icon-skip-forward.svg +1 -0
- package/src/index.js +6 -4
- package/src/pref-viewer-2d.js +10 -9
- package/src/pref-viewer-3d.js +9 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.11.0-beta.
|
|
3
|
+
"version": "2.11.0-beta.3",
|
|
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,16 @@
|
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"files": [
|
|
32
32
|
"src",
|
|
33
|
-
"src/
|
|
33
|
+
"src/images",
|
|
34
34
|
"index.d.ts"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@babylonjs/core": "^8.
|
|
38
|
-
"@babylonjs/
|
|
39
|
-
"@babylonjs/
|
|
37
|
+
"@babylonjs/core": "^8.36.1",
|
|
38
|
+
"@babylonjs/gui": "^8.36.1",
|
|
39
|
+
"@babylonjs/loaders": "^8.36.1",
|
|
40
|
+
"@babylonjs/serializers": "^8.36.1",
|
|
40
41
|
"@panzoom/panzoom": "^4.6.0",
|
|
41
|
-
"babylonjs-gltf2interface": "^8.
|
|
42
|
+
"babylonjs-gltf2interface": "^8.36.1",
|
|
42
43
|
"idb": "^8.0.3",
|
|
43
44
|
"is-svg": "^6.1.0"
|
|
44
45
|
},
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { Color3, PointerEventTypes, HighlightLayer } from "@babylonjs/core";
|
|
2
|
+
import { AdvancedDynamicTexture, StackPanel, Control, Button, Image } from "@babylonjs/gui";
|
|
3
|
+
|
|
4
|
+
// https://doc.babylonjs.com/typedoc/classes/BABYLON.AnimationGroup
|
|
5
|
+
class OpeningAnimationController {
|
|
6
|
+
static states = {
|
|
7
|
+
paused: 0,
|
|
8
|
+
closed: 1,
|
|
9
|
+
opened: 2,
|
|
10
|
+
opening: 3,
|
|
11
|
+
closing: 4,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
#openAnimation = null;
|
|
15
|
+
#closeAnimation = null;
|
|
16
|
+
|
|
17
|
+
#nodes = [];
|
|
18
|
+
#state = OpeningAnimationController.states.closed;
|
|
19
|
+
#currentFrame = 0;
|
|
20
|
+
#startFrame = 0;
|
|
21
|
+
#endFrame = 0;
|
|
22
|
+
#speedRatio = 1.0;
|
|
23
|
+
|
|
24
|
+
#advancedDynamicTexture = null;
|
|
25
|
+
#menu = null;
|
|
26
|
+
|
|
27
|
+
constructor(name, openAnimationGroup, closeAnimationGroup) {
|
|
28
|
+
this.name = name;
|
|
29
|
+
this.#openAnimation = openAnimationGroup;
|
|
30
|
+
this.#closeAnimation = closeAnimationGroup;
|
|
31
|
+
|
|
32
|
+
this.#openAnimation.stop();
|
|
33
|
+
this.#openAnimation._loopAnimation = false;
|
|
34
|
+
this.#closeAnimation.stop();
|
|
35
|
+
this.#closeAnimation._loopAnimation = false;
|
|
36
|
+
|
|
37
|
+
this.#startFrame = this.#openAnimation.from;
|
|
38
|
+
this.#endFrame = this.#openAnimation.to;
|
|
39
|
+
this.#speedRatio = this.#openAnimation.speedRatio || 1.0;
|
|
40
|
+
|
|
41
|
+
this.#getNodesFromAnimationGroups();
|
|
42
|
+
this.#openAnimation.onAnimationGroupEndObservable.add(this.#onOpened.bind(this));
|
|
43
|
+
this.#closeAnimation.onAnimationGroupEndObservable.add(this.#onClosed.bind(this));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#getNodesFromAnimationGroups() {
|
|
47
|
+
[this.#openAnimation, this.#closeAnimation].forEach((animationGroup) => {
|
|
48
|
+
animationGroup._targetedAnimations.forEach((targetedAnimation) => {
|
|
49
|
+
if (!this.#nodes.includes(targetedAnimation.target.id)) {
|
|
50
|
+
this.#nodes.push(targetedAnimation.target.id);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#onOpened() {
|
|
57
|
+
this.goToOpened();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#onClosed() {
|
|
61
|
+
this.goToClosed();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ---------------------------
|
|
66
|
+
* Public methods
|
|
67
|
+
* ---------------------------
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
isAnimationForNode(node) {
|
|
71
|
+
return this.#nodes.includes(node);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
playOpen() {
|
|
75
|
+
if (this.#state === OpeningAnimationController.states.opening || this.#state === OpeningAnimationController.states.opened) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (this.#state === OpeningAnimationController.states.closing) {
|
|
79
|
+
this.#currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
|
|
80
|
+
this.#closeAnimation.pause();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
|
|
84
|
+
this.#openAnimation.goToFrame(this.#currentFrame);
|
|
85
|
+
this.#openAnimation.restart();
|
|
86
|
+
} else {
|
|
87
|
+
this.#openAnimation.start(false, this.#speedRatio, this.#currentFrame, this.#endFrame, undefined);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.#state = OpeningAnimationController.states.opening;
|
|
91
|
+
this.updateControls();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
playClose() {
|
|
95
|
+
if (this.#state === OpeningAnimationController.states.closing || this.#state === OpeningAnimationController.states.closed) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (this.#state === OpeningAnimationController.states.opening) {
|
|
99
|
+
this.#currentFrame = this.#openAnimation.getCurrentFrame();
|
|
100
|
+
this.#openAnimation.pause();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
|
|
104
|
+
this.#closeAnimation.goToFrame(this.#endFrame - this.#currentFrame);
|
|
105
|
+
this.#closeAnimation.restart();
|
|
106
|
+
} else {
|
|
107
|
+
this.#closeAnimation.start(false, this.#speedRatio, this.#endFrame - this.#currentFrame, this.#endFrame, undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.#state = OpeningAnimationController.states.closing;
|
|
111
|
+
this.updateControls();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pause() {
|
|
115
|
+
if (this.#state === OpeningAnimationController.states.opening) {
|
|
116
|
+
this.#currentFrame = this.#openAnimation.getCurrentFrame();
|
|
117
|
+
this.#openAnimation.pause();
|
|
118
|
+
}
|
|
119
|
+
if (this.#state === OpeningAnimationController.states.closing) {
|
|
120
|
+
this.#currentFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
|
|
121
|
+
this.#closeAnimation.pause();
|
|
122
|
+
}
|
|
123
|
+
this.#state = OpeningAnimationController.states.paused;
|
|
124
|
+
this.updateControls();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
goToOpened() {
|
|
128
|
+
this.#currentFrame = this.#endFrame;
|
|
129
|
+
|
|
130
|
+
if (this.#openAnimation._isStarted) {
|
|
131
|
+
this.#openAnimation.start();
|
|
132
|
+
}
|
|
133
|
+
this.#openAnimation.pause();
|
|
134
|
+
this.#openAnimation.goToFrame(this.#endFrame);
|
|
135
|
+
|
|
136
|
+
if (!this.#closeAnimation._isStarted) {
|
|
137
|
+
this.#closeAnimation.start();
|
|
138
|
+
}
|
|
139
|
+
this.#closeAnimation.pause();
|
|
140
|
+
this.#closeAnimation.goToFrame(this.#startFrame);
|
|
141
|
+
|
|
142
|
+
this.#state = OpeningAnimationController.states.opened;
|
|
143
|
+
this.updateControls();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
goToClosed() {
|
|
147
|
+
this.#currentFrame = this.#startFrame;
|
|
148
|
+
if (this.#openAnimation._isStarted) {
|
|
149
|
+
this.#openAnimation.start();
|
|
150
|
+
}
|
|
151
|
+
this.#openAnimation.pause();
|
|
152
|
+
this.#openAnimation.goToFrame(this.#startFrame);
|
|
153
|
+
|
|
154
|
+
if (this.#closeAnimation._isStarted) {
|
|
155
|
+
this.#closeAnimation.start();
|
|
156
|
+
}
|
|
157
|
+
this.#closeAnimation.pause();
|
|
158
|
+
this.#closeAnimation.goToFrame(this.#endFrame);
|
|
159
|
+
|
|
160
|
+
this.#state = OpeningAnimationController.states.closed;
|
|
161
|
+
this.updateControls();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
showControls(advancedDynamicTexture, mesh) {
|
|
165
|
+
this.#advancedDynamicTexture = advancedDynamicTexture;
|
|
166
|
+
this.#advancedDynamicTexture.metadata = { name: this.name };
|
|
167
|
+
const controlCallbacks = {
|
|
168
|
+
onGoToOpened: () => {
|
|
169
|
+
if (this.#state === OpeningAnimationController.states.opened) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.goToOpened();
|
|
173
|
+
this.hideControls();
|
|
174
|
+
},
|
|
175
|
+
onOpen: () => {
|
|
176
|
+
if (this.#state === OpeningAnimationController.states.opened || this.#state === OpeningAnimationController.states.opening) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.playOpen();
|
|
180
|
+
this.hideControls();
|
|
181
|
+
},
|
|
182
|
+
onPause: () => {
|
|
183
|
+
if (this.#state === OpeningAnimationController.states.paused || this.#state === OpeningAnimationController.states.closed || this.#state === OpeningAnimationController.states.opened) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.pause();
|
|
187
|
+
this.hideControls();
|
|
188
|
+
},
|
|
189
|
+
onClose: () => {
|
|
190
|
+
if (this.#state === OpeningAnimationController.states.closed || this.#state === OpeningAnimationController.states.closing) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.playClose();
|
|
194
|
+
this.hideControls();
|
|
195
|
+
},
|
|
196
|
+
onGoToClosed: () => {
|
|
197
|
+
if (this.#state === OpeningAnimationController.states.closed) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.goToClosed();
|
|
201
|
+
this.hideControls();
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
this.#menu = new AnimationMenu(this.#advancedDynamicTexture, mesh, this.#state, controlCallbacks);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
hideControls() {
|
|
208
|
+
if (!this.isControlsVisible()) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.#advancedDynamicTexture.dispose();
|
|
212
|
+
this.#advancedDynamicTexture = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateControls() {
|
|
216
|
+
if (!this.isControlsVisible()) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!this.#menu) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.#menu.animationState = this.#state;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
isControlsVisible() {
|
|
226
|
+
return this.#advancedDynamicTexture !== null || this.#advancedDynamicTexture?.metadata?.name === this.name;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* ---------------------------
|
|
231
|
+
* Public properties
|
|
232
|
+
* ---------------------------
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
get state() {
|
|
236
|
+
return this.#state;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* AnimationMenu - Models and renders a popup menu for controlling animation playback in a Babylon.js 3D scene.
|
|
242
|
+
*
|
|
243
|
+
* Responsibilities:
|
|
244
|
+
* - Creates a Babylon.js GUI panel with buttons for animation actions (open, close, pause, go to opened/closed).
|
|
245
|
+
* - Handles button states (enabled/disabled, active/inactive) based on animation state.
|
|
246
|
+
* - Attaches the menu to a mesh and disposes it when an action is taken.
|
|
247
|
+
* - Allows external callbacks for each action.
|
|
248
|
+
*
|
|
249
|
+
* Usage:
|
|
250
|
+
* const menu = new AnimationMenu(advancedDynamicTexture, mesh, animationState, {
|
|
251
|
+
* onOpen: () => { ... },
|
|
252
|
+
* onClose: () => { ... },
|
|
253
|
+
* onPause: () => { ... },
|
|
254
|
+
* onGoToOpened: () => { ... },
|
|
255
|
+
* onGoToClosed: () => { ... }
|
|
256
|
+
* });
|
|
257
|
+
* menu.show();
|
|
258
|
+
* menu.hide();
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
class AnimationMenu {
|
|
262
|
+
#advancedDynamicTexture = null;
|
|
263
|
+
#animationState = OpeningAnimationController.states.closed;
|
|
264
|
+
#mesh = null;
|
|
265
|
+
#callbacks = null;
|
|
266
|
+
#panel = null;
|
|
267
|
+
#colorActive = "#6BA53A";
|
|
268
|
+
#colorEnabled = "#333333";
|
|
269
|
+
#colorDisabled = "#777777";
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture.
|
|
273
|
+
* @param {BABYLON.Mesh} mesh - Mesh to attach the menu to.
|
|
274
|
+
* @param {number} animationState - Current animation state (enum).
|
|
275
|
+
* @param {object} callbacks - Callback functions for menu actions.
|
|
276
|
+
*/
|
|
277
|
+
constructor(advancedDynamicTexture, mesh, animationState, callbacks) {
|
|
278
|
+
this.#advancedDynamicTexture = advancedDynamicTexture;
|
|
279
|
+
this.#mesh = mesh;
|
|
280
|
+
this.#animationState = animationState;
|
|
281
|
+
this.#callbacks = callbacks;
|
|
282
|
+
|
|
283
|
+
this.#createMenu(animationState);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Renders the menu and attaches it to the mesh.
|
|
288
|
+
*/
|
|
289
|
+
#createMenu() {
|
|
290
|
+
if (!this.#advancedDynamicTexture || !this.#mesh) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.#panel = new StackPanel();
|
|
294
|
+
this.#panel.isVertical = false;
|
|
295
|
+
this.#panel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
|
|
296
|
+
this.#panel.left = 0;
|
|
297
|
+
this.#panel.top = 0;
|
|
298
|
+
this.#advancedDynamicTexture.addControl(this.#panel);
|
|
299
|
+
this.#panel.linkWithMesh(this.#mesh);
|
|
300
|
+
|
|
301
|
+
this.#createButtons();
|
|
302
|
+
this.#setButtonsState();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Internal helper to add a button to the menu.
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
#addButton(name, imageURL, callback) {
|
|
310
|
+
const button = Button.CreateImageOnlyButton(`button_animation_${name}`, imageURL);
|
|
311
|
+
button.image.stretch = Image.STRETCH_UNIFORM;
|
|
312
|
+
button.color = "white";
|
|
313
|
+
button.hoverCursor = "pointer";
|
|
314
|
+
button.width = "28px";
|
|
315
|
+
button.height = "28px";
|
|
316
|
+
button.cornerRadius = 0;
|
|
317
|
+
button.background = this.#colorEnabled;
|
|
318
|
+
button.onPointerUpObservable.add(() => {
|
|
319
|
+
if (callback) {
|
|
320
|
+
callback();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
this.#panel.addControl(button);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#createButtons() {
|
|
327
|
+
this.#addButton("closed", "../src/images/icon-skip-backward.svg", this.#callbacks.onGoToClosed);
|
|
328
|
+
this.#addButton("close", "../src/images/icon-play-backwards.svg", this.#callbacks.onClose);
|
|
329
|
+
this.#addButton("pause", "../src/images/icon-pause.svg", this.#callbacks.onPause);
|
|
330
|
+
this.#addButton("open", "../src/images/icon-play.svg", this.#callbacks.onOpen);
|
|
331
|
+
this.#addButton("opened", "../src/images/icon-skip-forward.svg", this.#callbacks.onGoToOpened);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#setButtonState(name, enabled, active) {
|
|
335
|
+
const button = this.#advancedDynamicTexture.getControlByName(`button_animation_${name}`);
|
|
336
|
+
if (!button) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
button.background = active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#setButtonsState() {
|
|
343
|
+
const goToOpenedButtonEnabled = this.#animationState !== OpeningAnimationController.states.opened;
|
|
344
|
+
const goToOpenedButtonActive = false;
|
|
345
|
+
const openButtonEnabled = this.#animationState !== OpeningAnimationController.states.opened && this.#animationState !== OpeningAnimationController.states.opening;
|
|
346
|
+
const openButtonActive = this.#animationState === OpeningAnimationController.states.opening;
|
|
347
|
+
const pauseButtonEnabled = this.#animationState !== OpeningAnimationController.states.paused && this.#animationState !== OpeningAnimationController.states.closed && this.#animationState !== OpeningAnimationController.states.opened;
|
|
348
|
+
const pauseButtonActive = this.#animationState === OpeningAnimationController.states.paused;
|
|
349
|
+
const closeButtonEnabled = this.#animationState !== OpeningAnimationController.states.closed && this.#animationState !== OpeningAnimationController.states.closing;
|
|
350
|
+
const closeButtonActive = this.#animationState === OpeningAnimationController.states.closing;
|
|
351
|
+
const goToClosedButtonEnabled = this.#animationState !== OpeningAnimationController.states.closed;
|
|
352
|
+
const goToClosedButtonActive = false;
|
|
353
|
+
|
|
354
|
+
this.#setButtonState("opened", goToOpenedButtonEnabled, goToOpenedButtonActive);
|
|
355
|
+
this.#setButtonState("open", openButtonEnabled, openButtonActive);
|
|
356
|
+
this.#setButtonState("pause", pauseButtonEnabled, pauseButtonActive);
|
|
357
|
+
this.#setButtonState("close", closeButtonEnabled, closeButtonActive);
|
|
358
|
+
this.#setButtonState("closed", goToClosedButtonEnabled, goToClosedButtonActive);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
set animationState(state) {
|
|
362
|
+
this.#animationState = state;
|
|
363
|
+
this.#setButtonsState();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* BabylonJSAnimationController - Manages animation playback and interactive highlighting for model containers in Babylon.js scenes.
|
|
369
|
+
*
|
|
370
|
+
* Responsibilities:
|
|
371
|
+
* - Detects if the loaded model container contains animations.
|
|
372
|
+
* - Controls playback state: playing forward, playing backward, paused.
|
|
373
|
+
* - Tracks and manages animated transformation nodes.
|
|
374
|
+
* - Highlights animated nodes when hovered by the cursor.
|
|
375
|
+
* - Provides API for play, pause, reverse, and highlight operations.
|
|
376
|
+
*/
|
|
377
|
+
export default class BabylonJSAnimationController {
|
|
378
|
+
#scene = null;
|
|
379
|
+
#assetContainer = null;
|
|
380
|
+
#animatedNodes = [];
|
|
381
|
+
#highlightLayer = null;
|
|
382
|
+
#highlightColor = new Color3(0, 1, 0); // Color para resaltar los elementos animados (Verde)
|
|
383
|
+
#advancedDynamicTexture = null;
|
|
384
|
+
#openingAnimations = [];
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @param {BABYLON.Scene} scene - The Babylon.js scene instance.
|
|
388
|
+
* @param {BABYLON.AssetContainer} assetContainer - The loaded asset container.
|
|
389
|
+
*/
|
|
390
|
+
constructor(scene, assetContainer) {
|
|
391
|
+
this.#scene = scene;
|
|
392
|
+
this.#assetContainer = assetContainer;
|
|
393
|
+
this.#initializeAnimations();
|
|
394
|
+
this.#setupPointerObservers();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Detects and stores animatable objects and animated nodes in the model container.
|
|
399
|
+
* @private
|
|
400
|
+
*/
|
|
401
|
+
#initializeAnimations() {
|
|
402
|
+
if (!this.#assetContainer.animationGroups.length) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.#getAnimatedNodes();
|
|
407
|
+
this.#getOpeneingAnimations();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#getAnimatedNodes() {
|
|
411
|
+
this.#assetContainer.animationGroups.forEach((animationGroup) => {
|
|
412
|
+
if (!animationGroup._targetedAnimations.length) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
animationGroup._targetedAnimations.forEach((targetedAnimation) => {
|
|
416
|
+
if (!this.#animatedNodes.includes(targetedAnimation.target.id)) {
|
|
417
|
+
this.#animatedNodes.push(targetedAnimation.target.id);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#getOpeneingAnimations() {
|
|
424
|
+
const openings = {};
|
|
425
|
+
this.#assetContainer.animationGroups.forEach((animationGroup) => {
|
|
426
|
+
const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
|
|
427
|
+
if (!match) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const [, type, openingName] = match;
|
|
431
|
+
if (!openings[openingName]) {
|
|
432
|
+
openings[openingName] = { name: openingName, animationOpen: null, animationClose: null };
|
|
433
|
+
}
|
|
434
|
+
if (type === "open") {
|
|
435
|
+
openings[openingName].animationOpen = animationGroup;
|
|
436
|
+
} else if (type === "close") {
|
|
437
|
+
openings[openingName].animationClose = animationGroup;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
Object.values(openings).forEach((opening) => {
|
|
442
|
+
this.#openingAnimations.push(new OpeningAnimationController(opening.name, opening.animationOpen, opening.animationClose));
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#getOpeningAnimationByNode(nodeId) {
|
|
447
|
+
return this.#openingAnimations.find((openingAnimation) => openingAnimation.isAnimationForNode(nodeId));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Busca si una malla pertenece a un nodo que es objetivo de alguna animación
|
|
452
|
+
* @param {BABYLON.Mesh} mesh Malla
|
|
453
|
+
* @param {Array} targetNodeIds Array con los identificadores de los nodos que son objetivo de alguna animación
|
|
454
|
+
* @returns {Array} Array de 2 posiciones: [0] Identificador del nodo que es objetivo de alguna animación (o false si no lo es), [1] Array con las mallas hijas del nodo que es objetivo de alguna animación (o vacío si no lo es)
|
|
455
|
+
* @description
|
|
456
|
+
* El segundo elemento del array devuelto es se necesita para poder resaltar esas mallas hijas cuando el puntero del ratón está sobre alguna de ellas
|
|
457
|
+
*/
|
|
458
|
+
#getNodeAnimatedByMesh = function (mesh) {
|
|
459
|
+
let nodeId = false;
|
|
460
|
+
let node = mesh;
|
|
461
|
+
while (node.parent !== null && !nodeId) {
|
|
462
|
+
node = node.parent;
|
|
463
|
+
if (this.#animatedNodes.includes(node.id)) {
|
|
464
|
+
nodeId = node.id;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return nodeId;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Ilumina las mallas que son objetivo de alguna animación si el puntero del ratón está sobre una de ellas
|
|
472
|
+
* @param {BABYLON.PickingInfo} pickingInfo Información del trazado del rayo desde la cámara activa hasta el puntero del ratón
|
|
473
|
+
*/
|
|
474
|
+
#hightlightMeshesForAnimation(pickingInfo) {
|
|
475
|
+
if (!this.#highlightLayer) {
|
|
476
|
+
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.#highlightLayer.removeAllMeshes();
|
|
480
|
+
if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
|
|
485
|
+
if (!nodeId) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
490
|
+
const nodeMeshes = transformNode.getChildMeshes();
|
|
491
|
+
if (nodeMeshes.length) {
|
|
492
|
+
nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Sets up pointer observers to highlight animated nodes on hover.
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
#setupPointerObservers() {
|
|
501
|
+
this.#scene.onPointerObservable.add((pointerInfo) => {
|
|
502
|
+
if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
|
|
503
|
+
const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
|
|
504
|
+
this.#hightlightMeshesForAnimation(pickingInfo);
|
|
505
|
+
}
|
|
506
|
+
if (pointerInfo.type === PointerEventTypes.POINTERUP) {
|
|
507
|
+
// Eliminar cualquier Babylon GUI que se haya creado anteriormente
|
|
508
|
+
if (this.#advancedDynamicTexture) {
|
|
509
|
+
this.#advancedDynamicTexture.dispose();
|
|
510
|
+
this.#advancedDynamicTexture = null;
|
|
511
|
+
}
|
|
512
|
+
const pickingInfo = this.#scene.pick(pointerInfo.event.clientX, pointerInfo.event.clientY);
|
|
513
|
+
this.#showMenu(pickingInfo);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#showMenu(pickingInfo) {
|
|
519
|
+
if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
|
|
524
|
+
if (!nodeId) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
|
|
528
|
+
if (!openingAnimation) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!this.#advancedDynamicTexture) {
|
|
532
|
+
this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation");
|
|
533
|
+
}
|
|
534
|
+
openingAnimation.showControls(this.#advancedDynamicTexture, pickingInfo.pickedMesh);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
@@ -3,7 +3,9 @@ import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompre
|
|
|
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 BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* BabylonJSController - Main controller for managing Babylon.js 3D scenes, assets, and interactions.
|
|
@@ -76,6 +78,7 @@ export default class BabylonJSController {
|
|
|
76
78
|
#options = {};
|
|
77
79
|
|
|
78
80
|
#gltfResolver = null; // GLTFResolver instance
|
|
81
|
+
#babylonJSAnimationController = null; // AnimationController instance
|
|
79
82
|
|
|
80
83
|
/**
|
|
81
84
|
* Constructs a new BabylonJSController instance.
|
|
@@ -629,7 +632,7 @@ export default class BabylonJSController {
|
|
|
629
632
|
* @private
|
|
630
633
|
* @param {object} container - The container object containing asset state and metadata.
|
|
631
634
|
* @param {object} newAssetContainer - The new asset container to add to the scene.
|
|
632
|
-
* @returns {boolean} True if the container was replaced, false otherwise.
|
|
635
|
+
* @returns {boolean} True if the container was replaced and added, false otherwise.
|
|
633
636
|
*/
|
|
634
637
|
#replaceContainer(container, newAssetContainer) {
|
|
635
638
|
if (container.assetContainer) {
|
|
@@ -639,8 +642,7 @@ export default class BabylonJSController {
|
|
|
639
642
|
}
|
|
640
643
|
this.#scene.getEngine().releaseEffects();
|
|
641
644
|
container.assetContainer = newAssetContainer;
|
|
642
|
-
this.#addContainer(container);
|
|
643
|
-
return true;
|
|
645
|
+
return this.#addContainer(container);
|
|
644
646
|
}
|
|
645
647
|
/**
|
|
646
648
|
* Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
|
|
@@ -663,7 +665,6 @@ export default class BabylonJSController {
|
|
|
663
665
|
* @private
|
|
664
666
|
* @returns {void}
|
|
665
667
|
*/
|
|
666
|
-
#stopR;
|
|
667
668
|
#stopRender() {
|
|
668
669
|
this.#engine.stopRenderLoop(this.#renderLoop.bind(this));
|
|
669
670
|
}
|
|
@@ -753,7 +754,10 @@ export default class BabylonJSController {
|
|
|
753
754
|
if (container.state.name === "model") {
|
|
754
755
|
assetContainer.lights = [];
|
|
755
756
|
}
|
|
756
|
-
this.#replaceContainer(container, assetContainer);
|
|
757
|
+
const replacedAndAdded = this.#replaceContainer(container, assetContainer);
|
|
758
|
+
if (replacedAndAdded && container.state.name === "model") {
|
|
759
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene, assetContainer);
|
|
760
|
+
}
|
|
757
761
|
container.state.setSuccess(true);
|
|
758
762
|
} else {
|
|
759
763
|
if (container.assetContainer && container.state.mustBeShown !== container.state.isVisible && container.state.name === "materials") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="pause" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M14,19H18V5H14M6,19H10V5H6V19Z" /></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="play_backwards" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M16,18.86V4.86l-11,7,11,7Z" /></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="play" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M8,5.14v14l11-7-11-7Z" /></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="skip-backward" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12" /></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="skip-forward" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12" /></svg>
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
340
|
+
cancelable: true,
|
|
339
341
|
composed: true,
|
|
340
342
|
detail: event.detail,
|
|
341
343
|
};
|
package/src/pref-viewer-2d.js
CHANGED
|
@@ -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:
|
|
226
|
-
composed:
|
|
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:
|
|
242
|
-
composed:
|
|
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:
|
|
262
|
-
composed:
|
|
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:
|
|
289
|
-
composed:
|
|
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
|
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -303,9 +303,9 @@ export class PrefViewer3D extends HTMLElement {
|
|
|
303
303
|
*/
|
|
304
304
|
#onLoading() {
|
|
305
305
|
const customEventOptions = {
|
|
306
|
-
bubbles: true,
|
|
307
|
-
cancelable:
|
|
308
|
-
composed:
|
|
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:
|
|
355
|
-
composed:
|
|
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:
|
|
379
|
-
composed:
|
|
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:
|
|
420
|
-
composed:
|
|
419
|
+
cancelable: true,
|
|
420
|
+
composed: false,
|
|
421
421
|
detail: detail,
|
|
422
422
|
};
|
|
423
423
|
this.dispatchEvent(new CustomEvent("scene-set-options", customEventOptions));
|