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

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",
3
+ "version": "2.12.0-beta.2",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -113,18 +113,18 @@ export default class BabylonJSAnimationController {
113
113
  }
114
114
 
115
115
  /**
116
- * Determines if a mesh belongs to a node targeted by an animation.
116
+ * Finds all animated node IDs associated with a given mesh by traversing its parent hierarchy.
117
117
  * @private
118
118
  * @param {Mesh} mesh - The mesh to check.
119
- * @returns {string|false} The animated node ID if found, otherwise false.
119
+ * @returns {Array<string>} Array of animated node IDs associated with the mesh.
120
120
  */
121
- #getNodeAnimatedByMesh = function (mesh) {
122
- let nodeId = false;
121
+ #getNodesAnimatedByMesh = function (mesh) {
122
+ let nodeId = [];
123
123
  let node = mesh;
124
- while (node.parent !== null && !nodeId) {
124
+ while (node.parent !== null) {
125
125
  node = node.parent;
126
- if (this.#animatedNodes.includes(node.id)) {
127
- nodeId = node.id;
126
+ if (this.#animatedNodes.includes(node.id) && !nodeId.includes(node.id)) {
127
+ nodeId.push(node.id);
128
128
  }
129
129
  }
130
130
  return nodeId;
@@ -169,16 +169,29 @@ export default class BabylonJSAnimationController {
169
169
  return;
170
170
  }
171
171
 
172
- const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
173
- if (!nodeId) {
172
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
173
+ if (!nodeIds.length) {
174
174
  return;
175
175
  }
176
176
 
177
- const transformNode = this.#scene.getTransformNodeByID(nodeId);
178
- const nodeMeshes = transformNode.getChildMeshes();
179
- if (nodeMeshes.length) {
180
- nodeMeshes.forEach((mesh) => this.#highlightLayer.addMesh(mesh, this.#highlightColor));
181
- }
177
+ const transformNodes = [];
178
+ nodeIds.forEach((nodeId) => {
179
+ const transformNode = this.#scene.getTransformNodeByID(nodeId);
180
+ if (transformNode) {
181
+ transformNodes.push(transformNode);
182
+ }
183
+ });
184
+
185
+ transformNodes.forEach((transformNode) => {
186
+ const nodeMeshes = transformNode.getChildMeshes();
187
+ if (nodeMeshes.length) {
188
+ nodeMeshes.forEach((mesh) => {
189
+ if (!this.#highlightLayer.hasMesh(mesh)) {
190
+ this.#highlightLayer.addMesh(mesh, this.#highlightColor);
191
+ }
192
+ });
193
+ }
194
+ });
182
195
  }
183
196
 
184
197
  /**
@@ -203,15 +216,20 @@ export default class BabylonJSAnimationController {
203
216
 
204
217
  this.hideMenu();
205
218
 
206
- const nodeId = this.#getNodeAnimatedByMesh(pickingInfo.pickedMesh);
207
- if (!nodeId) {
208
- return;
209
- }
210
- const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
211
- if (!openingAnimation) {
219
+ const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
220
+ if (!nodeIds.length) {
212
221
  return;
213
222
  }
223
+ const openingAnimations = [];
224
+ nodeIds.forEach((nodeId) => {
225
+ const openingAnimation = this.#getOpeningAnimationByNode(nodeId);
226
+ if (!openingAnimation) {
227
+ return;
228
+ }
229
+ openingAnimations.push(openingAnimation);
230
+ });
214
231
 
215
- openingAnimation.showControls(this.#canvas);
232
+ const startedAnimation = openingAnimations.find((animation) => animation.state !== OpeningAnimation.states.closed);
233
+ startedAnimation ? startedAnimation.showControls(this.#canvas, openingAnimations) : openingAnimations[0].showControls(this.#canvas, openingAnimations);
216
234
  }
217
235
  }
@@ -5,17 +5,19 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
5
5
  * OpeningAnimationMenu - Manages and renders the interactive animation control menu for opening/closing animations in a Babylon.js scene.
6
6
  *
7
7
  * Summary:
8
- * Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons and a progress slider.
8
+ * Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons, a progress slider, and a selector for switching between multiple animations.
9
9
  * Handles user interaction, updates UI state based on animation state, and invokes external callbacks for animation actions.
10
10
  * Designed for integration with Babylon.js product configurators and interactive 3D applications.
11
11
  *
12
12
  * Key Features:
13
13
  * - Renders a menu with buttons for controlling animation playback (open, close, pause, go to opened/closed, loop).
14
+ * - Displays a selector UI for switching between multiple opening animations, showing only the animation name without prefix.
14
15
  * - Updates button states and slider position based on animation state, progress, and loop mode.
15
16
  * - Handles user interactions and invokes provided callbacks for animation actions.
16
17
  * - Synchronizes the slider value with animation progress, avoiding callback loops.
17
18
  * - Provides setters to update animation state, progress, and loop mode from external controllers.
18
19
  * - Disposes and cleans up all DOM resources when no longer needed.
20
+ * - Resets other animation menus when switching between animations.
19
21
  *
20
22
  * Public Setters:
21
23
  * - set animationState(state): Updates the animation state and button states.
@@ -29,9 +31,10 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
29
31
  * - isVisible: Returns whether the animation menu is currently visible in the DOM.
30
32
  *
31
33
  * Private Methods:
32
- * - #createMenu(): Initializes and renders the menu UI.
34
+ * - #createMenu(): Initializes and renders the menu UI, including the selector, buttons, and slider.
33
35
  * - #addButton(name, imageSVG, enabled, active, visible, callback): Adds a button to the menu with specified properties.
34
36
  * - #createButtons(): Creates all control buttons and sets their initial states.
37
+ * - #createSelector(): Creates a selector UI element for switching between available opening animations, omitting the prefix in the display name.
35
38
  * - #getButtonByName(name): Retrieves a button control by its name.
36
39
  * - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
37
40
  * - #getPlayerButtonsState(): Returns the state (enabled, active, visible) for playback control buttons.
@@ -41,16 +44,18 @@ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
41
44
  * - #setLoopButtonsState(): Updates all loop control buttons.
42
45
  * - #createSlider(): Creates and configures the animation progress slider.
43
46
  * - #onSliderInput(event): Handles the slider input event for animation progress.
47
+ * - #resetOtherAnimations(): Resets all other opening animations except the current one.
44
48
  *
45
49
  * Usage Example:
46
- * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, {
50
+ * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, openingAnimations, {
47
51
  * onOpen: () => { ... },
48
52
  * onClose: () => { ... },
49
53
  * onPause: () => { ... },
50
54
  * onGoToOpened: () => { ... },
51
55
  * onGoToClosed: () => { ... },
52
56
  * onToggleLoop: () => { ... },
53
- * onSetAnimationProgress: (progress) => { ... }
57
+ * onSetAnimationProgress: (progress) => { ... },
58
+ * onChangeAnimation: () => { ... }
54
59
  * });
55
60
  * menu.animationState = OpeningAnimation.states.opening;
56
61
  * menu.animationProgress = 0.5;
@@ -60,6 +65,7 @@ export default class OpeningAnimationMenu {
60
65
  #animationState = OpeningAnimation.states.closed;
61
66
  #animationProgress = 0;
62
67
  #animationLoop = false;
68
+ #openingAnimations = [];
63
69
  #callbacks = null;
64
70
 
65
71
  #name = "";
@@ -89,17 +95,20 @@ export default class OpeningAnimationMenu {
89
95
  * @param {number} animationState - Current animation state (enum).
90
96
  * @param {number} animationProgress - Current animation progress (0 to 1).
91
97
  * @param {boolean} animationLoop - Whether the animation is set to loop.
98
+ * @param {Array<OpeningAnimation>} openingAnimations - Array of OpeningAnimation instances to manage.
92
99
  * @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
93
100
  * @public
94
101
  */
95
- constructor(name, canvas, animationState, animationProgress, animationLoop, callbacks) {
102
+ constructor(name, canvas, animationState, animationProgress, animationLoop, openingAnimations, callbacks) {
96
103
  this.#name = name;
97
104
  this.#canvas = canvas;
98
105
  this.#animationState = animationState;
99
106
  this.#animationProgress = animationProgress;
100
107
  this.#animationLoop = animationLoop;
108
+ this.#openingAnimations = openingAnimations;
101
109
  this.#callbacks = callbacks;
102
110
 
111
+ this.#resetOtherAnimations();
103
112
  this.#createMenu(animationState);
104
113
  }
105
114
 
@@ -131,6 +140,46 @@ export default class OpeningAnimationMenu {
131
140
  this.#containerButtons.appendChild(button);
132
141
  }
133
142
 
143
+ /**
144
+ * Creates a selector UI element for switching between available opening animations.
145
+ * @private
146
+ * @returns {void}
147
+ */
148
+ #createSelector() {
149
+ if (!Array.isArray(this.#openingAnimations) || this.#openingAnimations.length < 2) {
150
+ return; // No selector needed if only one animation
151
+ }
152
+
153
+ const selector = document.createElement("div");
154
+ selector.classList.add("animation-menu-selector");
155
+
156
+ this.#openingAnimations.forEach((animation) => {
157
+ const button = document.createElement("button");
158
+ button.classList.add("button-selector");
159
+ // Remove prefix before "_" for display
160
+ const nameParts = animation.name.split("_");
161
+ button.textContent = nameParts.length > 1 ? nameParts.slice(1).join("_") : animation.name;
162
+
163
+ // Highlight the current animation
164
+ if (animation.name === this.#name) {
165
+ button.classList.add("active");
166
+ }
167
+
168
+ button.addEventListener("click", (event) => {
169
+ if (animation.name !== this.#name) {
170
+ if (this.#callbacks.onChangeAnimation && typeof this.#callbacks.onChangeAnimation === "function") {
171
+ this.#callbacks.onChangeAnimation();
172
+ }
173
+ animation.showControls(this.#canvas, this.#openingAnimations);
174
+ }
175
+ });
176
+
177
+ selector.appendChild(button);
178
+ });
179
+
180
+ this.#containerMain.prepend(selector);
181
+ }
182
+
134
183
  /**
135
184
  * Creates all control buttons and sets their initial states.
136
185
  * @private
@@ -183,6 +232,7 @@ export default class OpeningAnimationMenu {
183
232
  this.#containerButtons.classList.add("animation-menu-buttons");
184
233
  this.#containerMain.appendChild(this.#containerButtons);
185
234
 
235
+ this.#createSelector();
186
236
  this.#createButtons();
187
237
  this.#createSlider();
188
238
  }
@@ -263,6 +313,20 @@ export default class OpeningAnimationMenu {
263
313
  return buttonsState;
264
314
  }
265
315
 
316
+ /**
317
+ * Resets all other opening animations except the current one.
318
+ * @private
319
+ * @returns {void}
320
+ */
321
+ #resetOtherAnimations() {
322
+ this.#openingAnimations.forEach((animation) => {
323
+ if (animation.name !== this.#name) {
324
+ animation.hideControls();
325
+ animation.goToClosed();
326
+ }
327
+ });
328
+ }
329
+
266
330
  /**
267
331
  * Updates the visual state of a button (enabled, active, visible).
268
332
  * @private
@@ -19,7 +19,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
19
19
  * - pause(): Pauses the current animation.
20
20
  * - goToOpened(): Moves animation to the fully opened state.
21
21
  * - goToClosed(): Moves animation to the fully closed state.
22
- * - showControls(canvas): Displays the animation control menu.
22
+ * - showControls(canvas, openingAnimations): Displays the animation control menu and sets up callbacks.
23
23
  * - hideControls(): Hides the animation control menu.
24
24
  * - isControlsVisible(): Returns true if the control menu is visible for this animation.
25
25
  *
@@ -433,8 +433,9 @@ export default class OpeningAnimation {
433
433
  * Synchronizes slider and button states with animation.
434
434
  * @public
435
435
  * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
436
+ * @param {Array<OpeningAnimation>} openingAnimations - Array of OpeningAnimation instances to manage.
436
437
  */
437
- showControls(canvas) {
438
+ showControls(canvas, openingAnimations = []) {
438
439
  const controlCallbacks = {
439
440
  onGoToOpened: () => {
440
441
  if (this.#state === OpeningAnimation.states.opened) {
@@ -481,8 +482,14 @@ export default class OpeningAnimation {
481
482
  this.#loop = !this.#loop;
482
483
  this.#menu.animationLoop = this.#loop;
483
484
  },
485
+ onChangeAnimation: () => {
486
+ if (this.#state !== OpeningAnimation.states.closed) {
487
+ this.goToClosed();
488
+ }
489
+ this.hideControls();
490
+ },
484
491
  };
485
- this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
492
+ this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, openingAnimations, controlCallbacks);
486
493
 
487
494
  // Attach to Babylon.js scene render loop for real-time updates
488
495
  this.#openAnimation._scene.onBeforeRenderObservable.add(this.#handlers.updateControlsSlider);
@@ -24,6 +24,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
24
24
  * - Provides methods for downloading models and scenes in GLB, glTF (ZIP), and USDZ formats.
25
25
  * - Manages camera and material options, container visibility, and user interactions.
26
26
  * - Observes canvas resize events and updates the engine accordingly.
27
+ * - Applies metadata-driven scene adjustments (e.g., inner floor translation) after asset reloads.
27
28
  * - Designed for integration with PrefViewer and GLTFResolver.
28
29
  * - All resource management and rendering operations are performed asynchronously for performance.
29
30
  *
@@ -84,6 +85,9 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
84
85
  * - #startRender(): Starts the Babylon.js render loop.
85
86
  * - #loadAssetContainer(container): Loads an asset container asynchronously.
86
87
  * - #loadContainers(): Loads all asset containers and adds them to the scene.
88
+ * - #checkModelMetadata(oldMetadata, newMetadata): Processes metadata changes after loading.
89
+ * - #checkInnerFloorTranslation(oldMetadata, newMetadata): Applies inner floor Y translations from metadata.
90
+ * - #translateNodeY(name, deltaY): Adjusts the Y position of a scene node.
87
91
  * - #addDateToName(name): Appends the current date/time to a name string.
88
92
  * - #downloadZip(files, name, comment, addDateInName): Generates and downloads a ZIP file.
89
93
  * - #openDownloadDialog(): Opens the modal download dialog.
@@ -319,9 +323,9 @@ export default class BabylonJSController {
319
323
  * @returns {boolean}
320
324
  */
321
325
  #initializeEnvironmentTexture() {
322
- return false;
326
+ return false; // Environment texture disabled by the moment
323
327
  if (this.#scene.environmentTexture) {
324
- return;
328
+ return false;
325
329
  }
326
330
  const hdrTextureURI = "../src/environments/noon_grass.hdr";
327
331
  const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
@@ -329,6 +333,7 @@ export default class BabylonJSController {
329
333
  hdrTexture._noMipmap = false;
330
334
  hdrTexture.level = 2.0;
331
335
  this.#scene.environmentTexture = hdrTexture;
336
+ return true;
332
337
  }
333
338
 
334
339
  /**
@@ -340,7 +345,7 @@ export default class BabylonJSController {
340
345
  * @returns {void|false} Returns false if no environment texture is set; otherwise void.
341
346
  */
342
347
  #initializeIBLShadows() {
343
- if (!this.#scene.environmentTexture) {
348
+ if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
344
349
  return false;
345
350
  }
346
351
 
@@ -407,7 +412,7 @@ export default class BabylonJSController {
407
412
  * Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
408
413
  */
409
414
  #initializeShadows() {
410
- if (!this.#scene.environmentTexture) {
415
+ if (this.#scene.environmentTexture) {
411
416
  this.#initializeIBLShadows();
412
417
  return true;
413
418
  }
@@ -417,7 +422,7 @@ export default class BabylonJSController {
417
422
  return false;
418
423
  }
419
424
  mesh.receiveShadows = true;
420
- if (!mesh.name === "hdri") {
425
+ if (mesh.name !== "hdri") {
421
426
  this.#shadowGen.addShadowCaster(mesh, true);
422
427
  }
423
428
  });
@@ -848,6 +853,8 @@ export default class BabylonJSController {
848
853
  container.assetContainer = null;
849
854
  }
850
855
  this.#scene.getEngine().releaseEffects();
856
+ this.#scene.getEngine().releaseComputeEffects();
857
+
851
858
  container.assetContainer = newAssetContainer;
852
859
  return this.#addContainer(container, false);
853
860
  }
@@ -914,7 +921,7 @@ export default class BabylonJSController {
914
921
  return [container, false];
915
922
  }
916
923
 
917
- container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp);
924
+ container.state.setPendingCacheData(sourceData.size, sourceData.timeStamp, sourceData.metadata);
918
925
 
919
926
  // https://doc.babylonjs.com/typedoc/interfaces/BABYLON.LoadAssetContainerOptions
920
927
  let options = {
@@ -943,6 +950,9 @@ export default class BabylonJSController {
943
950
  async #loadContainers() {
944
951
  this.#stopRender();
945
952
 
953
+ let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
954
+ let newModelMetadata = {};
955
+
946
956
  const promiseArray = [];
947
957
  Object.values(this.#containers).forEach((container) => {
948
958
  promiseArray.push(this.#loadAssetContainer(container));
@@ -962,6 +972,7 @@ export default class BabylonJSController {
962
972
  if (result.status === "fulfilled" && assetContainer) {
963
973
  if (container.state.name === "model") {
964
974
  assetContainer.lights = [];
975
+ newModelMetadata = { ...(container.state.update.metadata ?? {}) };
965
976
  }
966
977
  this.#replaceContainer(container, assetContainer);
967
978
  container.state.setSuccess(true);
@@ -985,6 +996,7 @@ export default class BabylonJSController {
985
996
  detail.error = error;
986
997
  })
987
998
  .finally(async () => {
999
+ this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
988
1000
  this.#setMaxSimultaneousLights();
989
1001
  this.#initializeShadows();
990
1002
  this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
@@ -993,6 +1005,101 @@ export default class BabylonJSController {
993
1005
  return detail;
994
1006
  }
995
1007
 
1008
+ /**
1009
+ * Checks and applies model metadata changes after asset loading.
1010
+ * @private
1011
+ * @param {object} oldMetadata - The previous metadata object for the model.
1012
+ * @param {object} newMetadata - The new metadata object for the model.
1013
+ * @returns {void}
1014
+ * @description
1015
+ * Currently, it only checks for changes related to the inner floor translation by delegating to #checkInnerFloorTranslation.
1016
+ */
1017
+ #checkModelMetadata(oldMetadata, newMetadata) {
1018
+ // For the moment only check inner floor translation
1019
+ this.#checkInnerFloorTranslation(oldMetadata, newMetadata);
1020
+ }
1021
+
1022
+ /**
1023
+ * Checks and applies Y-axis translation for the inner floor node based on metadata changes.
1024
+ * @private
1025
+ * @param {object} oldMetadata - The previous metadata object for the model.
1026
+ * @param {object} newMetadata - The new metadata object for the model.
1027
+ * @returns {void}
1028
+ */
1029
+ #checkInnerFloorTranslation(oldMetadata, newMetadata) {
1030
+ let newDeltaY = newMetadata?.customProperties?.innerFloorValue ? parseFloat(newMetadata.customProperties.innerFloorValue) : 0;
1031
+ let oldDeltaY = oldMetadata?.customProperties?.innerFloorValue ? parseFloat(oldMetadata.customProperties.innerFloorValue) : 0;
1032
+
1033
+ if (newDeltaY === 0 && oldDeltaY === 0) {
1034
+ return;
1035
+ }
1036
+
1037
+ if (!this.#containers.environment.assetContainer) {
1038
+ return;
1039
+ }
1040
+
1041
+ const getNodeName = (data) => {
1042
+ if (data && typeof data === "string" && data.includes("/")) {
1043
+ data = data.slice(data.lastIndexOf("/") + 1);
1044
+ return data;
1045
+ }
1046
+ return false;
1047
+ };
1048
+
1049
+ let applyNewTranslation = false;
1050
+ let applyOldTranslation = false;
1051
+ let undoOldTranslation = false;
1052
+ const newTranslationNodeName = getNodeName(newMetadata?.customProperties?.innerFloorPath);
1053
+ const oldTranslationNodeName = getNodeName(oldMetadata?.customProperties?.innerFloorPath);
1054
+
1055
+ if (this.#containers.environment.state.isSuccess) {
1056
+ // If environment is being reloaded:
1057
+ if (this.#containers.model.state.isSuccess) {
1058
+ // If model is also being reloaded, apply only new translation
1059
+ applyNewTranslation = newDeltaY !== 0 && newTranslationNodeName;
1060
+ } else {
1061
+ // If model is not being reloaded, apply old translation
1062
+ applyOldTranslation = oldDeltaY !== 0 && oldTranslationNodeName;
1063
+ }
1064
+ } else {
1065
+ // If environment is not being reloaded:
1066
+ if (this.#containers.model.state.isSuccess) {
1067
+ // If environment is not being reload but model is being reloaded, undo old trasnslation and apply new translation
1068
+ undoOldTranslation = oldDeltaY !== 0 && oldTranslationNodeName;
1069
+ applyNewTranslation = newDeltaY !== 0 && newTranslationNodeName;
1070
+ }
1071
+ }
1072
+
1073
+ if (undoOldTranslation) {
1074
+ this.#translateNodeY(oldTranslationNodeName, -oldDeltaY);
1075
+ }
1076
+ if (applyOldTranslation) {
1077
+ this.#translateNodeY(oldTranslationNodeName, oldDeltaY);
1078
+ }
1079
+ if (applyNewTranslation) {
1080
+ this.#translateNodeY(newTranslationNodeName, newDeltaY);
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Translates a node along the scene's vertical (Y) axis by the provided value.
1086
+ * @private
1087
+ * @param {string} name - The node name to find in the scene.
1088
+ * @param {number} deltaY - The Y-axis translation amount (positive or negative).
1089
+ * @returns {boolean} True if the node was found and translated, false otherwise.
1090
+ */
1091
+ #translateNodeY(name, deltaY) {
1092
+ if (!this.#scene) {
1093
+ return false;
1094
+ }
1095
+ const node = this.#scene.getNodeByName(name);
1096
+ if (!node || !node.position) {
1097
+ return false;
1098
+ }
1099
+ node.position.y += deltaY;
1100
+ return true;
1101
+ }
1102
+
996
1103
  /**
997
1104
  * Appends the current date and time to the provided name string in the format: name_YYYY-MM-DD_HH.mm.ss
998
1105
  * @private
@@ -220,6 +220,7 @@ export default class GLTFResolver {
220
220
  let source = storage.url || null;
221
221
  let newSize, newTimeStamp;
222
222
  let pending = false;
223
+ let metadata = {};
223
224
 
224
225
  if (storage.db && storage.table && storage.id) {
225
226
  await this.#initializeStorage(storage.db, storage.table);
@@ -228,6 +229,7 @@ export default class GLTFResolver {
228
229
  return false;
229
230
  }
230
231
  source = object.data;
232
+ metadata = { ...(object.metadata ?? {}) };
231
233
  if (object.timeStamp === currentTimeStamp) {
232
234
  return false;
233
235
  } else {
@@ -283,6 +285,6 @@ export default class GLTFResolver {
283
285
  }
284
286
  }
285
287
  }
286
- return { source: file || source, size: newSize, timeStamp: newTimeStamp, extension: extension };
288
+ return { source: file || source, size: newSize, timeStamp: newTimeStamp, extension: extension, metadata: metadata, };
287
289
  }
288
290
  }
@@ -13,6 +13,7 @@
13
13
  */
14
14
  export class ContainerData {
15
15
  constructor(name = "") {
16
+ this.metadata = {};
16
17
  this.name = name;
17
18
  this.show = true;
18
19
  this.size = 0;
@@ -23,6 +24,7 @@ export class ContainerData {
23
24
  }
24
25
  reset() {
25
26
  this.update = {
27
+ metadata: {},
26
28
  pending: false,
27
29
  storage: null,
28
30
  success: false,
@@ -37,6 +39,7 @@ export class ContainerData {
37
39
  this.show = this.update.show !== null ? this.update.show : this.show;
38
40
  this.size = this.update.size;
39
41
  this.timeStamp = this.update.timeStamp;
42
+ this.metadata = { ...(this.update.metadata ?? {}) };
40
43
  } else {
41
44
  this.update.success = false;
42
45
  }
@@ -48,9 +51,10 @@ export class ContainerData {
48
51
  this.update.success = false;
49
52
  this.update.show = show !== undefined ? show : this.update.show !== null ? this.update.show : this.show;
50
53
  }
51
- setPendingCacheData(size = 0, timeStamp = null) {
54
+ setPendingCacheData(size = 0, timeStamp = null, metadata = {}) {
52
55
  this.update.size = size;
53
56
  this.update.timeStamp = timeStamp;
57
+ this.update.metadata = { ...(metadata ?? {}) };
54
58
  }
55
59
  get isPending() {
56
60
  return this.update.pending === true;
package/src/styles.js CHANGED
@@ -111,15 +111,62 @@ export const PrefViewer3DAnimationMenuStyles = `
111
111
  --slider-thumb-width: 20px;
112
112
  --slider-bar-height: 8px;
113
113
  --slider-bar-offset: 10px;
114
+ --selector-button-radius: 6px;
115
+ --selector-button-padding-horizontal: 8px;
116
+ --selector-button-padding-vertical: 4px;
117
+ --selector-button-font-size: 14px;
118
+ --selector-button-font-weight: 500;
119
+ --width: calc(var(--button-size) * 6 + var(--button-spacing) * 5 + var(--button-loop-margin-left));
114
120
 
115
121
  display: block;
116
122
  position: absolute;
117
123
  bottom: 10px;
118
124
 
119
- right: calc(50% - ((6 * var(--button-size) + 5 * var(--button-spacing) + var(--button-loop-margin-left)) / 2));
125
+ right: calc(50% - (var(--width) / 2));
120
126
  z-index: 1000;
121
127
  }
122
128
 
129
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector {
130
+ fore
131
+ padding: 0;
132
+ margin: 0;
133
+ display: flex;
134
+ flex-wrap: wrap;
135
+ gap: var(--button-spacing);
136
+ max-width: var(--width);
137
+ margin-bottom: var(--button-spacing);
138
+ }
139
+
140
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button {
141
+ display: block;
142
+ padding: var(--selector-button-padding-vertical) var(--selector-button-padding-horizontal);
143
+ margin: 0;
144
+ border: 1px solid var(--color-border);
145
+ border-radius: var(--selector-button-radius);
146
+ background: var(--color-enabled);
147
+ color: var(--color-icon);
148
+ font-size: var(--selector-button-font-size);
149
+ font-weight: var(--selector-button-font-weight);
150
+ line-height: 1;
151
+ flex: 1;
152
+ }
153
+
154
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button.active {
155
+ background: var(--color-active);
156
+ }
157
+
158
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button.active:hover {
159
+ background: var(--color-active-hover);
160
+ }
161
+
162
+ div.pref-viewer-3d.animation-menu>div.animation-menu-selector>button:not(.active) {
163
+ cursor: pointer;
164
+ }
165
+
166
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button:not(.active):hover {
167
+ background: var(--color-enabled-hover);
168
+ }
169
+
123
170
  div.pref-viewer-3d.animation-menu>div.animation-menu-buttons {
124
171
  padding: 0;
125
172
  margin: 0;