@preference-sl/pref-viewer 2.13.0-beta.2 → 2.13.0-beta.21

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.
@@ -1,7 +1,7 @@
1
1
  import { CameraData, ContainerData, MaterialData, IBLData } from "./pref-viewer-3d-data.js";
2
2
  import BabylonJSController from "./babylonjs-controller.js";
3
3
  import { PrefViewer3DStyles } from "./styles.js";
4
- import { FileStorage } from "./file-storage.js";
4
+ import FileStorage from "./file-storage.js";
5
5
 
6
6
  /**
7
7
  * PrefViewer3D - Custom Web Component for interactive 3D visualization and configuration.
@@ -75,6 +75,7 @@ export default class PrefViewer3D extends HTMLElement {
75
75
  #isInitialized = false;
76
76
  #isLoaded = false;
77
77
  #isVisible = false;
78
+ #lastLoadTimestamp = undefined;
78
79
 
79
80
  #wrapper = null;
80
81
  #canvas = null;
@@ -162,7 +163,7 @@ export default class PrefViewer3D extends HTMLElement {
162
163
  */
163
164
  disconnectedCallback() {
164
165
  if (this.#babylonJSController) {
165
- this.#babylonJSController.disable();
166
+ void this.#babylonJSController.disable();
166
167
  }
167
168
  }
168
169
 
@@ -230,6 +231,27 @@ export default class PrefViewer3D extends HTMLElement {
230
231
  this.#babylonJSController.enable();
231
232
  }
232
233
 
234
+ /**
235
+ * Determines whether the next 3D load should bypass cache checks.
236
+ * @private
237
+ * @param {object} config - Incoming config payload.
238
+ * @returns {boolean} True when force reload should be enabled.
239
+ */
240
+ #shouldForceReload(config) {
241
+ if (!config || typeof config !== "object") {
242
+ return false;
243
+ }
244
+ if (config.forceReload === true) {
245
+ return true;
246
+ }
247
+ if (config.timestamp === undefined || config.timestamp === null) {
248
+ return false;
249
+ }
250
+ const changed = config.timestamp !== this.#lastLoadTimestamp;
251
+ this.#lastLoadTimestamp = config.timestamp;
252
+ return changed;
253
+ }
254
+
233
255
  /**
234
256
  * Resets update flags for all containers and material/camera options after loading or setting options.
235
257
  * @private
@@ -239,7 +261,6 @@ export default class PrefViewer3D extends HTMLElement {
239
261
  Object.values(this.#data.containers).forEach((container) => container.reset());
240
262
  Object.values(this.#data.options.materials).forEach((material) => material.reset());
241
263
  this.#data.options.camera.reset();
242
- this.#data.options.ibl.reset();
243
264
  }
244
265
 
245
266
  /**
@@ -324,6 +345,10 @@ export default class PrefViewer3D extends HTMLElement {
324
345
  /**
325
346
  * Resolves incoming IBL settings (HDR URL, timestamp, intensity, shadows) and marks the option as pending when changed.
326
347
  * Fetches signed URLs/time stamps when storage keys are provided so the Babylon controller can reload the environment map.
348
+ * If `options.ibl` exists but `options.ibl.url` is undefined (missing), the current IBL URL is kept and not marked as pending.
349
+ * This allows updating other IBL properties (such as intensity or shadows) without requiring a new URL.
350
+ * If `options.ibl.url` is explicitly provided as `null` (or resolves to an unavailable URL), it is treated as a request to
351
+ * clear the current IBL environment, and the IBL state is reset so Babylon can remove the environment map.
327
352
  * @private
328
353
  * @param {object} options - Options payload that may contain an `ibl` block with url, intensity, or shadow flags.
329
354
  * @returns {Promise<boolean>} Resolves to true when any IBL property differs from the cached state, otherwise false.
@@ -335,18 +360,25 @@ export default class PrefViewer3D extends HTMLElement {
335
360
  const iblState = this.#data.options.ibl;
336
361
 
337
362
  let url = undefined;
363
+ let cachedUrl = undefined;
338
364
  let timeStamp = undefined;
339
365
  let shadows = undefined;
340
366
  let intensity = undefined;
341
367
 
342
- if (options.ibl.url) {
343
- url = options.ibl.url;
368
+ if (typeof options.ibl.url === "string" && options.ibl.url.length > 0) {
344
369
  const fileStorage = new FileStorage("PrefViewer", "Files");
345
370
  const newURL = await fileStorage.getURL(options.ibl.url);
346
371
  if (newURL) {
347
- url = newURL;
372
+ url = options.ibl.url;
373
+ cachedUrl = newURL;
348
374
  timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
375
+ } else {
376
+ url = null;
377
+ cachedUrl = null;
349
378
  }
379
+ } else if (options.ibl.url === null) {
380
+ url = null;
381
+ cachedUrl = null;
350
382
  }
351
383
  if (options.ibl.shadows !== undefined) {
352
384
  shadows = options.ibl.shadows;
@@ -358,10 +390,15 @@ export default class PrefViewer3D extends HTMLElement {
358
390
  const needUpdate = url !== undefined && url !== iblState.url ||
359
391
  timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
360
392
  shadows !== undefined && shadows !== iblState.shadows ||
361
- intensity !== undefined && intensity !== iblState.intensity;
393
+ shadows === undefined && iblState.shadows !== iblState.defaultShadows ||
394
+ intensity !== undefined && intensity !== iblState.intensity ||
395
+ intensity === undefined && iblState.intensity !== iblState.defaultIntensity;
362
396
  if (needUpdate) {
363
- iblState.setValues(url, intensity, shadows, timeStamp);
364
- iblState.setPending(true);
397
+ if (url === null || cachedUrl === null) {
398
+ iblState.reset();
399
+ } else {
400
+ iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
401
+ }
365
402
  }
366
403
 
367
404
  return needUpdate;
@@ -543,6 +580,7 @@ export default class PrefViewer3D extends HTMLElement {
543
580
  }
544
581
 
545
582
  this.#onLoading();
583
+ const forceReload = this.#shouldForceReload(config);
546
584
 
547
585
  // Containers
548
586
  this.#checkNeedToUpdateContainers(config);
@@ -551,10 +589,10 @@ export default class PrefViewer3D extends HTMLElement {
551
589
  if (config.options) {
552
590
  this.#checkNeedToUpdateCamera(config.options);
553
591
  this.#checkNeedToUpdateMaterials(config.options);
554
- this.#checkNeedToUpdateIBL(config.options);
592
+ await this.#checkNeedToUpdateIBL(config.options);
555
593
  }
556
594
 
557
- const loadDetail = await this.#babylonJSController.load();
595
+ const loadDetail = await this.#babylonJSController.load(forceReload);
558
596
 
559
597
  return { ...loadDetail, load: this.#onLoaded() };
560
598
  }
@@ -564,9 +602,9 @@ export default class PrefViewer3D extends HTMLElement {
564
602
  * Updates internal states, triggers option setting events, and returns the result.
565
603
  * @public
566
604
  * @param {object} options - Options object containing camera and material settings.
567
- * @returns {object} Object containing success status and details of set options.
605
+ * @returns {Promise<object>} Object containing success status and details of set options.
568
606
  */
569
- setOptions(options) {
607
+ async setOptions(options) {
570
608
  if (!this.#babylonJSController) {
571
609
  return;
572
610
  }
@@ -580,8 +618,9 @@ export default class PrefViewer3D extends HTMLElement {
580
618
  if (this.#checkNeedToUpdateMaterials(options)) {
581
619
  someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
582
620
  }
583
- if (this.#checkNeedToUpdateIBL(options)) {
584
- someSetted = someSetted || this.#babylonJSController.setIBLOptions();
621
+ const needUpdateIBL = await this.#checkNeedToUpdateIBL(options);
622
+ if (needUpdateIBL) {
623
+ someSetted = someSetted || (await this.#babylonJSController.setIBLOptions());
585
624
  }
586
625
  const detail = this.#onSetOptions();
587
626
  return { success: someSetted, detail: detail };
@@ -755,6 +794,23 @@ export default class PrefViewer3D extends HTMLElement {
755
794
  return this.#babylonJSController.getRenderSettings();
756
795
  }
757
796
 
797
+ /**
798
+ * Reports whether an IBL environment map is currently available.
799
+ * @public
800
+ * @returns {boolean} True when a validated IBL texture exists or a pending cached URL is available.
801
+ */
802
+ isIBLAvailable() {
803
+ const ibl = this.#data?.options?.ibl;
804
+ if (!ibl) {
805
+ return false;
806
+ }
807
+ if (ibl.valid === true) {
808
+ return true;
809
+ }
810
+ const cachedUrl = ibl.cachedUrl;
811
+ return typeof cachedUrl === "string" ? cachedUrl.length > 0 : Boolean(cachedUrl);
812
+ }
813
+
758
814
  /**
759
815
  * Applies render settings that require reloading the Babylon.js scene.
760
816
  * @public
@@ -9,7 +9,8 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
9
9
  * - Builds an accessible hover/focus-activated panel with switches for AA, SSAO, IBL, and dynamic shadows.
10
10
  * - Caches translated copy in `#texts` and listens for culture changes via the `culture` attribute or `setCulture()`.
11
11
  * - Tracks applied vs. draft render settings so pending diffs, button enablement, and status text stay in sync.
12
- * - Emits `pref-viewer-menu-apply` whenever the user confirms toggles, allowing PrefViewer/BabylonJSController to persist.
12
+ * - Supports per-switch availability (enabled/disabled), preventing interaction and pending diffs for unavailable toggles.
13
+ * - Emits `pref-viewer-menu-3d-apply` whenever the user confirms toggles, allowing PrefViewer/BabylonJSController to persist.
13
14
  * - Shows transient status/error messages and per-switch pending states while operations complete.
14
15
  *
15
16
  * Public API:
@@ -17,6 +18,7 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
17
18
  * - `setApplying(isApplying, hasError?)`: locks the UI and optionally displays errors while async updates run.
18
19
  * - `setViewerHover(isHovering)`: opens/closes the panel based on viewer hover state.
19
20
  * - `setEnabled(isEnabled)`: hides the menu in 2D mode and clears hover data.
21
+ * - `setSwitchEnabled(settingKey, isEnabled)`: enables/disables a concrete toggle (e.g. IBL) based on runtime availability.
20
22
  * - `setCulture(cultureId)`: forces a locale change and re-renders copy from the i18n layer.
21
23
  *
22
24
  * Lifecycle & Integration:
@@ -52,6 +54,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
52
54
  #MENU_SWITCHES = Object.keys(this.#DEFAULT_RENDER_SETTINGS);
53
55
  #appliedSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
54
56
  #draftSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
57
+ #switchAvailability = {};
55
58
  #statusTimeout = null;
56
59
  #isApplying = false;
57
60
  #isEnabled = true;
@@ -352,6 +355,9 @@ export default class PrefViewerMenu3D extends HTMLElement {
352
355
  * @returns {void}
353
356
  */
354
357
  #handleSwitchChange(event) {
358
+ if (event.currentTarget?.disabled) {
359
+ return;
360
+ }
355
361
  const key = event.currentTarget?.dataset?.setting;
356
362
  if (!key) {
357
363
  return;
@@ -369,8 +375,15 @@ export default class PrefViewerMenu3D extends HTMLElement {
369
375
  if (this.#isApplying || !this.#hasPendingChanges()) {
370
376
  return;
371
377
  }
378
+
372
379
  const detail = { settings: { ...this.#draftSettings } };
373
- this.dispatchEvent(new CustomEvent("pref-viewer-menu-apply", { detail, bubbles: true, composed: true }));
380
+ const customEventOptions = {
381
+ bubbles: true,
382
+ cancelable: true,
383
+ composed: true,
384
+ detail: detail,
385
+ };
386
+ this.dispatchEvent(new CustomEvent("pref-viewer-menu-3d-apply", customEventOptions));
374
387
  }
375
388
 
376
389
  /**
@@ -381,7 +394,10 @@ export default class PrefViewerMenu3D extends HTMLElement {
381
394
  #updateSwitches() {
382
395
  Object.entries(this.#elements.switches).forEach(([key, input]) => {
383
396
  if (input) {
397
+ const isEnabled = this.#switchAvailability[key] !== false;
384
398
  input.checked = Boolean(this.#draftSettings[key]);
399
+ input.disabled = !isEnabled;
400
+ this.#elements.switchWrappers[key]?.toggleAttribute("data-disabled", !isEnabled);
385
401
  }
386
402
  });
387
403
  }
@@ -409,7 +425,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
409
425
  * @returns {string[]} Keys currently pending application.
410
426
  */
411
427
  #getPendingKeys() {
412
- return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => this.#draftSettings[key] !== this.#appliedSettings[key]);
428
+ return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => {
429
+ if (this.#elements.switches[key]?.disabled) {
430
+ return false;
431
+ }
432
+ return this.#draftSettings[key] !== this.#appliedSettings[key];
433
+ });
413
434
  }
414
435
 
415
436
  /**
@@ -554,4 +575,24 @@ export default class PrefViewerMenu3D extends HTMLElement {
554
575
  this.#closeMenu();
555
576
  }
556
577
  }
578
+
579
+ /**
580
+ * Enables or disables a specific render-setting switch in the menu.
581
+ * @public
582
+ * @param {string} settingKey - Switch key (e.g. "iblEnabled").
583
+ * @param {boolean} [isEnabled=true] - True to allow interaction, false to disable it.
584
+ * @returns {void}
585
+ */
586
+ setSwitchEnabled(settingKey, isEnabled = true) {
587
+ if (!settingKey || !this.#MENU_SWITCHES.includes(settingKey)) {
588
+ return;
589
+ }
590
+ const enabled = Boolean(isEnabled);
591
+ this.#switchAvailability[settingKey] = enabled;
592
+ if (!enabled) {
593
+ this.#draftSettings[settingKey] = this.#appliedSettings[settingKey];
594
+ }
595
+ this.#updateSwitches();
596
+ this.#updatePendingState();
597
+ }
557
598
  }
@@ -81,6 +81,7 @@ export default class PrefViewer extends HTMLElement {
81
81
  onViewerHoverStart: null,
82
82
  onViewerHoverEnd: null,
83
83
  on3DSceneLoaded: null,
84
+ on3DSceneError: null,
84
85
  };
85
86
 
86
87
  /**
@@ -136,6 +137,9 @@ export default class PrefViewer extends HTMLElement {
136
137
  this.loadMaterials(value);
137
138
  break;
138
139
  case "mode":
140
+ if (typeof value !== "string") {
141
+ return;
142
+ }
139
143
  if (_old === value || value.toLowerCase() === this.#mode) {
140
144
  return;
141
145
  }
@@ -151,6 +155,9 @@ export default class PrefViewer extends HTMLElement {
151
155
  this.setOptions(value);
152
156
  break;
153
157
  case "show-model":
158
+ if (typeof value !== "string") {
159
+ return;
160
+ }
154
161
  if (_old === value) {
155
162
  return;
156
163
  }
@@ -158,6 +165,9 @@ export default class PrefViewer extends HTMLElement {
158
165
  showModel ? this.showModel() : this.hideModel();
159
166
  break;
160
167
  case "show-scene":
168
+ if (typeof value !== "string") {
169
+ return;
170
+ }
161
171
  if (_old === value) {
162
172
  return;
163
173
  }
@@ -213,10 +223,11 @@ export default class PrefViewer extends HTMLElement {
213
223
  }
214
224
  if (this.#component3D) {
215
225
  this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
226
+ this.#component3D.removeEventListener("scene-error", this.#handlers.on3DSceneError);
216
227
  this.#component3D.remove();
217
228
  }
218
229
  if (this.#menu3D) {
219
- this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
230
+ this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
220
231
  this.#menu3D.remove();
221
232
  this.#menu3D = null;
222
233
  }
@@ -249,7 +260,11 @@ export default class PrefViewer extends HTMLElement {
249
260
  this.#menu3DSyncSettings();
250
261
  this.#menu3D?.setApplying(false);
251
262
  };
263
+ this.#handlers.on3DSceneError = () => {
264
+ this.#menu3D?.setApplying(false, true);
265
+ };
252
266
  this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
267
+ this.#component3D.addEventListener("scene-error", this.#handlers.on3DSceneError);
253
268
  this.#wrapper.appendChild(this.#component3D);
254
269
  }
255
270
 
@@ -264,7 +279,7 @@ export default class PrefViewer extends HTMLElement {
264
279
  return;
265
280
  }
266
281
  if (this.#menu3D) {
267
- this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
282
+ this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
268
283
  this.#menu3D.remove();
269
284
  }
270
285
  this.#menu3D = document.createElement("pref-viewer-menu-3d");
@@ -280,12 +295,13 @@ export default class PrefViewer extends HTMLElement {
280
295
  this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
281
296
  this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
282
297
 
283
- this.#menu3D.addEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
298
+ this.#menu3D.addEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
284
299
  this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
285
300
  this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
286
301
 
287
302
  this.#wrapper.appendChild(this.#menu3D);
288
303
  this.#menu3DSyncSettings();
304
+ this.#menu3DUpdateSwitchAvailability();
289
305
  this.#menu3DUpdateAvailability();
290
306
  }
291
307
 
@@ -372,6 +388,19 @@ export default class PrefViewer extends HTMLElement {
372
388
  this.#menu3D.setEnabled?.(isMode3D);
373
389
  }
374
390
 
391
+ /**
392
+ * Enables/disables menu switches according to current 3D option availability.
393
+ * @private
394
+ * @returns {void}
395
+ */
396
+ #menu3DUpdateSwitchAvailability() {
397
+ if (!this.#menu3D) {
398
+ return;
399
+ }
400
+ const iblAvailable = this.#component3D?.isIBLAvailable?.() === true;
401
+ this.#menu3D.setSwitchEnabled?.("iblEnabled", iblAvailable);
402
+ }
403
+
375
404
  /**
376
405
  * Applies render toggles via PrefViewer3D, showing progress in the menu and resyncing on completion.
377
406
  * When no changes occur, the menu is simply refreshed to match the controller snapshot.
@@ -440,7 +469,14 @@ export default class PrefViewer extends HTMLElement {
440
469
  * @returns {void}
441
470
  */
442
471
  #addTaskToQueue(value, type) {
443
- this.#taskQueue.push(new PrefViewerTask(value, type));
472
+ const task = new PrefViewerTask(value, type);
473
+ if (
474
+ task.type === PrefViewerTask.Types.Options ||
475
+ task.type === PrefViewerTask.Types.Drawing
476
+ ) {
477
+ this.#taskQueue = this.#taskQueue.filter((queuedTask) => queuedTask.type !== task.type);
478
+ }
479
+ this.#taskQueue.push(task);
444
480
  if (this.#isInitialized && !this.#isLoading) {
445
481
  this.#processNextTask();
446
482
  }
@@ -520,6 +556,9 @@ export default class PrefViewer extends HTMLElement {
520
556
  #on3DLoaded(detail) {
521
557
  this.#isLoaded = true;
522
558
  this.#isLoading = false;
559
+
560
+ this.#menu3DSyncSettings();
561
+ this.#menu3DUpdateSwitchAvailability();
523
562
 
524
563
  this.removeAttribute("loading-3d");
525
564
  this.setAttribute("loaded-3d", "");
@@ -535,6 +574,32 @@ export default class PrefViewer extends HTMLElement {
535
574
  this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
536
575
  }
537
576
 
577
+ /**
578
+ * Handles 3D load errors.
579
+ * Clears loading state and dispatches a "scene-error" event.
580
+ * @private
581
+ * @param {object} [detail] - Optional details about the failure.
582
+ * @returns {void}
583
+ */
584
+ #on3DError(detail) {
585
+ this.#isLoaded = false;
586
+ this.#isLoading = false;
587
+ this.#menu3D?.setApplying(false, true);
588
+
589
+ this.removeAttribute("loading-3d");
590
+ this.removeAttribute("loaded-3d");
591
+
592
+ const customEventOptions = {
593
+ bubbles: true,
594
+ cancelable: true,
595
+ composed: true,
596
+ };
597
+ if (detail) {
598
+ customEventOptions.detail = detail;
599
+ }
600
+ this.dispatchEvent(new CustomEvent("scene-error", customEventOptions));
601
+ }
602
+
538
603
  /**
539
604
  * Handles the start of a 2D loading operation.
540
605
  * Updates loading state, sets attributes, and dispatches a "drawing-loading" event.
@@ -581,6 +646,31 @@ export default class PrefViewer extends HTMLElement {
581
646
  this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
582
647
  }
583
648
 
649
+ /**
650
+ * Handles 2D load errors.
651
+ * Clears loading state and dispatches a "drawing-error" event.
652
+ * @private
653
+ * @param {object} [detail] - Optional details about the failure.
654
+ * @returns {void}
655
+ */
656
+ #on2DError(detail) {
657
+ this.#isLoaded = false;
658
+ this.#isLoading = false;
659
+
660
+ this.removeAttribute("loading-2d");
661
+ this.removeAttribute("loaded-2d");
662
+
663
+ const customEventOptions = {
664
+ bubbles: true,
665
+ cancelable: true,
666
+ composed: true,
667
+ };
668
+ if (detail) {
669
+ customEventOptions.detail = detail;
670
+ }
671
+ this.dispatchEvent(new CustomEvent("drawing-error", customEventOptions));
672
+ }
673
+
584
674
  /**
585
675
  * Handles the "drawing-zoom-changed" event from the 2D viewer component.
586
676
  * Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
@@ -601,7 +691,7 @@ export default class PrefViewer extends HTMLElement {
601
691
  }
602
692
 
603
693
  /**
604
- * Handles the custom "pref-viewer-menu-apply" event emitted by the menu component.
694
+ * Handles the custom "pref-viewer-menu-3d-apply" event emitted by the menu component.
605
695
  * Forwards the requested settings to the render-application pipeline.
606
696
  * @private
607
697
  * @param {CustomEvent} event - Menu event containing the `settings` payload.
@@ -623,16 +713,27 @@ export default class PrefViewer extends HTMLElement {
623
713
  * @param {object} config - The configuration object to process.
624
714
  * @returns {void}
625
715
  */
626
- #processConfig(config) {
716
+ async #processConfig(config) {
627
717
  if (!this.#component3D) {
718
+ this.#processNextTask();
628
719
  return;
629
720
  }
630
721
 
631
722
  this.#on3DLoading();
632
- this.#component3D.load(config).then((detail) => {
633
- this.#on3DLoaded(detail);
723
+ try {
724
+ const detail = await this.#component3D.load(config);
725
+ if (detail?.success === false) {
726
+ this.#on3DError(detail);
727
+ } else {
728
+ this.#on3DLoaded(detail);
729
+ }
730
+ } catch (error) {
731
+ this.#on3DError({
732
+ error: error instanceof Error ? error : new Error(String(error)),
733
+ });
734
+ } finally {
634
735
  this.#processNextTask();
635
- });
736
+ }
636
737
  }
637
738
 
638
739
  /**
@@ -653,16 +754,23 @@ export default class PrefViewer extends HTMLElement {
653
754
  * @param {object} drawing - The drawing object to process.
654
755
  * @returns {void}
655
756
  */
656
- #processDrawing(drawing) {
757
+ async #processDrawing(drawing) {
657
758
  if (!this.#component2D) {
759
+ this.#processNextTask();
658
760
  return;
659
761
  }
660
762
 
661
763
  this.#on2DLoading();
662
- this.#component2D.load(drawing).then((detail) => {
764
+ try {
765
+ const detail = await this.#component2D.load(drawing);
663
766
  this.#on2DLoaded(detail);
767
+ } catch (error) {
768
+ this.#on2DError({
769
+ error: error instanceof Error ? error : new Error(String(error)),
770
+ });
771
+ } finally {
664
772
  this.#processNextTask();
665
- });
773
+ }
666
774
  }
667
775
 
668
776
  /**
@@ -708,15 +816,23 @@ export default class PrefViewer extends HTMLElement {
708
816
  * @param {object} options - The options object to process.
709
817
  * @returns {void}
710
818
  */
711
- #processOptions(options) {
819
+ async #processOptions(options) {
712
820
  if (!this.#component3D) {
821
+ this.#processNextTask();
713
822
  return;
714
823
  }
715
824
 
716
825
  this.#on3DLoading();
717
- const detail = this.#component3D.setOptions(options);
718
- this.#on3DLoaded(detail);
719
- this.#processNextTask();
826
+ try {
827
+ const detail = await this.#component3D.setOptions(options);
828
+ this.#on3DLoaded(detail);
829
+ } catch (error) {
830
+ this.#on3DError({
831
+ error: error instanceof Error ? error : new Error(String(error)),
832
+ });
833
+ } finally {
834
+ this.#processNextTask();
835
+ }
720
836
  }
721
837
 
722
838
  /**
@@ -728,6 +844,7 @@ export default class PrefViewer extends HTMLElement {
728
844
  */
729
845
  #processVisibility(config) {
730
846
  if (!this.#component3D) {
847
+ this.#processNextTask();
731
848
  return;
732
849
  }
733
850
  const showModel = config.model?.visible;
@@ -957,7 +1074,7 @@ export default class PrefViewer extends HTMLElement {
957
1074
  * @returns {void}
958
1075
  */
959
1076
  setMode(mode = this.#mode) {
960
- mode = mode.toLowerCase();
1077
+ mode = typeof mode === "string" ? mode.toLowerCase() : this.#mode;
961
1078
  if (mode !== "2d" && mode !== "3d") {
962
1079
  console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
963
1080
  mode = this.#mode;
package/src/styles.js CHANGED
@@ -234,7 +234,7 @@ export const PrefViewerMenu3DStyles = `
234
234
  border-bottom: none;
235
235
  }
236
236
 
237
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-copy {
237
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-copy {
238
238
  display: flex;
239
239
  flex-direction: column;
240
240
  gap: calc(var(--panel-spacing) / 4);
@@ -262,14 +262,14 @@ export const PrefViewerMenu3DStyles = `
262
262
  font-size: var(--font-size-small);
263
263
  }
264
264
 
265
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control {
265
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control {
266
266
  position: relative;
267
267
  width: var(--switch-control-width);
268
268
  min-width: var(--switch-control-width);
269
269
  height: var(--switch-control-height);
270
270
  }
271
271
 
272
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input {
272
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input {
273
273
  position: absolute;
274
274
  inset: 0;
275
275
  margin: 0;
@@ -277,6 +277,18 @@ export const PrefViewerMenu3DStyles = `
277
277
  cursor: pointer;
278
278
  }
279
279
 
280
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch[data-disabled]>.menu-switch-copy {
281
+ opacity: 0.6;
282
+ }
283
+
284
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch[data-disabled]>.menu-switch-control {
285
+ opacity: 0.4;
286
+ }
287
+
288
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:disabled {
289
+ cursor: not-allowed;
290
+ }
291
+
280
292
  pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-visual {
281
293
  position: absolute;
282
294
  inset: 0;
@@ -286,6 +298,10 @@ export const PrefViewerMenu3DStyles = `
286
298
  cursor: pointer;
287
299
  }
288
300
 
301
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:disabled + .menu-switch-visual {
302
+ cursor: not-allowed;
303
+ }
304
+
289
305
  pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-visual::after {
290
306
  content: "";
291
307
  position: absolute;
@@ -299,11 +315,11 @@ export const PrefViewerMenu3DStyles = `
299
315
  transition: transform 0.2s ease;
300
316
  }
301
317
 
302
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input:checked + .menu-switch-visual {
318
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:checked + .menu-switch-visual {
303
319
  background: var(--switch-control-bar-checked-color);
304
320
  }
305
321
 
306
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input:checked + .menu-switch-visual::after {
322
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:checked + .menu-switch-visual::after {
307
323
  transform: translateX(var(--switch-control-thumb-size));
308
324
  }
309
325