@preference-sl/pref-viewer 2.12.0 → 2.13.0-beta.1

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,94 +1,86 @@
1
1
  import PrefViewerTask from "./pref-viewer-task.js";
2
2
  import { PrefViewerStyles } from "./styles.js";
3
+ import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./localization/i18n.js";
3
4
 
4
5
  /**
5
- * PrefViewer - Custom Web Component for advanced 2D and 3D product visualization and configuration.
6
+ * PrefViewer - Custom Web Component for immersive 2D/3D product visualization with localization, persistence, and download tooling.
6
7
  *
7
8
  * Overview:
8
- * - Encapsulates both 2D (SVG) and 3D (Babylon.js) viewers, supporting glTF/GLB models, environments, and drawings.
9
- * - Loads assets from remote URLs, Base64 data URIs, and IndexedDB sources.
10
- * - Provides a unified API for loading models, scenes, drawings, materials, and configuration via attributes or methods.
11
- * - Manages an internal task queue for sequential processing of viewer operations.
12
- * - Emits custom events for loading, errors, and state changes to facilitate integration.
13
- * - Supports downloading models and scenes in GLB and USDZ formats.
14
- * - Automatically updates the viewer when reactive attributes change.
9
+ * - Hosts synchronized 2D (SVG) and 3D (Babylon.js) canvases plus a render-settings menu inside a single shadow DOM wrapper.
10
+ * - Queues mutations (config/model/scene/materials/drawing/options/culture) to guarantee deterministic execution and avoid race conditions.
11
+ * - Normalizes remote URLs, Base64 payloads, and IndexedDB lookups before forwarding jobs to the BabylonJSController stack.
12
+ * - Persists the active locale to localStorage, mirrors it to PrefViewerMenu3D, and keeps the shared i18n module in sync with the chosen culture.
13
+ * - Surfaces a dialog API that powers built-in download flows (GLB, glTF+ZIP, USDZ) for model-only, scene-only, or combined asset exports.
14
+ * - Emits granular lifecycle custom events so host applications can react to loading progress, drawing zoom changes, and menu interactions.
15
+ * - Exposes imperative methods for toggling visibility, updating options, switching modes, and requesting downloads without attribute juggling.
16
+ * - Automatically reflects supported attributes back to the DOM so templates, frameworks, and manual scripting stay aligned with viewer state.
15
17
  *
16
18
  * Usage:
17
- * - Use as a custom HTML element: <pref-viewer ...>
18
- * - Configure via attributes (config, model, scene, materials, drawing, options, mode).
19
- * - Control viewer mode, visibility, and downloads via public methods.
19
+ * - Drop the `<pref-viewer>` custom element into any document or framework wrapper and supply configuration via attributes or setters.
20
+ * - Feed serialized JSON strings or plain objects to `loadConfig`, `loadModel`, `loadScene`, `loadMaterials`, `loadDrawing`, or `setOptions`.
21
+ * - Drive localization by setting the `culture` attribute/property or invoking `setCulture("es-ES")`, which propagates to the menu and dialog copy.
22
+ * - Trigger downloads through the provided public methods or open the interactive dialog with `openDialog()` for user-driven selection.
23
+ * - Listen for `scene-loading`, `scene-loaded`, `drawing-loading`, `drawing-loaded`, and `drawing-zoom-changed` to integrate with surrounding UX.
20
24
  *
21
25
  * Reactive Attributes:
22
- * - config: URL or Base64 for configuration file.
23
- * - model: URL or Base64 for 3D model (glTF/GLB).
24
- * - scene: URL or Base64 for environment/scene (glTF/GLB).
25
- * - materials: URL or Base64 for materials definition.
26
- * - drawing: URL or Base64 for SVG drawing.
27
- * - options: JSON string for viewer options.
28
- * - mode: Viewer mode ("2d" or "3d").
26
+ * - config: Configuration descriptor that can bundle model/scene/materials/drawing/culture hints.
27
+ * - model / scene / materials / drawing: Independent asset entry points (URLs, Base64, indexed data) for each subsystem.
28
+ * - options: JSON describing render toggles (AA, SSAO, IBL, shadows) plus misc viewer knobs.
29
+ * - culture: Locale identifier (RFC-5646 style) that resolves against `SUPPORTED_LOCALES` before applying.
30
+ * - mode: Switches between `2d` and `3d`, automatically hiding/showing the relevant subcomponents and menu states.
31
+ * - show-model / show-scene: Boolean flags to toggle visibility without reloading the underlying resources.
29
32
  *
30
- * Public Methods:
31
- * - loadConfig(config): Loads a configuration object or JSON string.
32
- * - loadModel(model): Loads a model object or JSON string.
33
- * - loadScene(scene): Loads a scene/environment object or JSON string.
34
- * - loadMaterials(materials): Loads materials object or JSON string.
35
- * - loadDrawing(drawing): Loads a drawing object or JSON string.
36
- * - setOptions(options): Sets viewer options from an object or JSON string.
37
- * - setMode(mode): Sets the viewer mode to "2d" or "3d" and updates component visibility.
38
- * - showModel(): Shows the 3D model.
39
- * - hideModel(): Hides the 3D model.
40
- * - showScene(): Shows the 3D environment/scene.
41
- * - hideScene(): Hides the 3D environment/scene.
42
- * - zoomCenter(): Centers the 2D drawing view.
43
- * - zoomExtentsAll(): Zooms the 2D drawing to fit all content.
44
- * - zoomIn(): Zooms in on the 2D drawing.
45
- * - zoomOut(): Zooms out of the 2D drawing.
46
- * - downloadModelGLB(): Downloads the current 3D model as a GLB file.
47
- * - downloadModelGLTF(): Downloads the current 3D model as a glTF ZIP file.
48
- * - downloadModelUSDZ(): Downloads the current 3D model as a USDZ file.
49
- * - downloadModelAndSceneGLB(): Downloads both the model and scene as a GLB file.
50
- * - downloadModelAndSceneGLTF(): Downloads both the model and scene as a glTF ZIP file.
51
- * - downloadModelAndSceneUSDZ(): Downloads both the model and scene as a USDZ file.
52
- * - downloadSceneGLB(): Downloads the environment as a GLB file.
53
- * - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
54
- * - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
55
- * - openDialog(title, content, footer): Opens a modal dialog with the specified title, content, and footer.
56
- * - closeDialog(): Closes the currently open dialog, if any, and removes it from the DOM.
33
+ * Public Methods (selection):
34
+ * - loadConfig / loadModel / loadScene / loadMaterials / loadDrawing: Queue asset ingestion jobs.
35
+ * - setOptions / setMode / setCulture: Update runtime behavior, render mode, or locale.
36
+ * - showModel / hideModel / showScene / hideScene: Toggle visibility using the internal task queue for consistent sequencing.
37
+ * - zoomCenter / zoomExtentsAll / zoomIn / zoomOut: Forward zoom commands to the 2D controller when active.
38
+ * - downloadModel / downloadScene / downloadModelAndScene: Export assets in GLB, glTF ZIP, or USDZ flavors.
39
+ * - openDialog / closeDialog: Manage PrefViewerDialog instances rendered inside the shadow tree.
57
40
  *
58
41
  * Public Properties:
59
- * - isInitialized: Indicates if the viewer is initialized.
60
- * - isLoaded: Indicates if the viewer has finished loading.
61
- * - isLoading: Indicates if the viewer is currently loading.
62
- * - isMode2D: Indicates if the viewer is in 2D mode.
63
- * - isMode3D: Indicates if the viewer is in 3D mode.
42
+ * - culture, isInitialized, isLoaded, isLoading, isMode2D, isMode3D expose runtime state for host inspection.
64
43
  *
65
44
  * Events:
66
- * - "scene-loading": Dispatched when a 3D loading operation starts.
67
- * - "scene-loaded": Dispatched when a 3D loading operation completes.
68
- * - "drawing-loading": Dispatched when a 2D drawing loading operation starts.
69
- * - "drawing-loaded": Dispatched when a 2D drawing loading operation completes.
70
- * - "drawing-zoom-changed": Dispatched when the 2D drawing zoom/pan state changes.
45
+ * - `scene-loading` / `scene-loaded` announce Babylon work, `drawing-loading` / `drawing-loaded` cover SVG pipelines, `drawing-zoom-changed` proxies pan/zoom delta.
71
46
  *
72
47
  * Notes:
73
- * - Automatically creates and manages 2D and 3D viewer components in its shadow DOM.
74
- * - Processes tasks sequentially to ensure consistent state.
75
- * - Designed for extensibility and integration in product configurators and visualization tools.
48
+ * - Internally wires PrefViewerMenu3D hover/apply events to PrefViewer3D render settings, ensuring persisted toggles stay in sync with BabylonJSController.
49
+ * - Cleans up dialogs, listeners, and subcomponents on disconnect to prevent leaks when frameworks mount/unmount the custom element.
50
+ * - Uses localStorage-backed culture persistence and attribute reflection safeguards to keep templates, menu, and global i18n aligned.
51
+ * - Designed for embeddable configurators, product showcases, and sales tooling that require responsive 2D/3D switching plus offline-friendly exports.
76
52
  */
77
53
  export default class PrefViewer extends HTMLElement {
54
+ // Internal state flags
78
55
  #isInitialized = false;
79
56
  #isLoaded = false;
80
57
  #isLoading = false;
81
58
  #mode = "3d";
82
59
 
60
+ // Task queue for sequential processing
83
61
  #taskQueue = [];
84
62
 
63
+ // Localization/culture management
64
+ #CULTURE_STORAGE_KEY = "pref-viewer/culture";
65
+ #cultureInitial = DEFAULT_LOCALE;
66
+ #cultureList = SUPPORTED_LOCALES;
67
+ #culture = "";
68
+ #cultureSuppressAttribute = false;
69
+
70
+ // References to internal components
85
71
  #wrapper = null;
86
72
  #component2D = null;
87
73
  #component3D = null;
88
74
  #dialog = null;
75
+ #menu3D = null;
89
76
 
77
+ // Event handlers
90
78
  #handlers = {
91
79
  on2DZoomChanged: null,
80
+ onMenuApply: null,
81
+ onViewerHoverStart: null,
82
+ onViewerHoverEnd: null,
83
+ on3DSceneLoaded: null,
92
84
  };
93
85
 
94
86
  /**
@@ -99,6 +91,12 @@ export default class PrefViewer extends HTMLElement {
99
91
  constructor() {
100
92
  super();
101
93
  this.attachShadow({ mode: "open" });
94
+ const storedCulture = this.#cultureReadPersisted();
95
+ if (storedCulture) {
96
+ this.#culture = resolveLocale(storedCulture);
97
+ } else {
98
+ this.#culture = setLocale(this.#cultureInitial);
99
+ }
102
100
  }
103
101
 
104
102
  /**
@@ -107,7 +105,7 @@ export default class PrefViewer extends HTMLElement {
107
105
  * @returns {string[]} Array of attribute names to observe.
108
106
  */
109
107
  static get observedAttributes() {
110
- return ["config", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
108
+ return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene"];
111
109
  }
112
110
 
113
111
  /**
@@ -125,6 +123,12 @@ export default class PrefViewer extends HTMLElement {
125
123
  case "config":
126
124
  this.loadConfig(value);
127
125
  break;
126
+ case "culture":
127
+ if (this.#cultureSuppressAttribute) {
128
+ return;
129
+ }
130
+ this.setCulture(value);
131
+ break;
128
132
  case "drawing":
129
133
  this.loadDrawing(value);
130
134
  break;
@@ -180,6 +184,7 @@ export default class PrefViewer extends HTMLElement {
180
184
 
181
185
  this.#createComponent3D();
182
186
  this.#createComponent2D();
187
+ this.#createMenu3D();
183
188
 
184
189
  if (!this.hasAttribute("mode")) {
185
190
  this.setMode();
@@ -195,6 +200,10 @@ export default class PrefViewer extends HTMLElement {
195
200
  * @returns {void}
196
201
  */
197
202
  disconnectedCallback() {
203
+ if (this.#wrapper) {
204
+ this.#wrapper.removeEventListener("mouseenter", this.#handlers.onViewerHoverStart);
205
+ this.#wrapper.removeEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
206
+ }
198
207
  if (this.#dialog) {
199
208
  this.#dialog?.remove();
200
209
  }
@@ -203,8 +212,14 @@ export default class PrefViewer extends HTMLElement {
203
212
  this.#component2D.remove();
204
213
  }
205
214
  if (this.#component3D) {
215
+ this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
206
216
  this.#component3D.remove();
207
217
  }
218
+ if (this.#menu3D) {
219
+ this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
220
+ this.#menu3D.remove();
221
+ this.#menu3D = null;
222
+ }
208
223
  }
209
224
 
210
225
  /**
@@ -230,9 +245,192 @@ export default class PrefViewer extends HTMLElement {
230
245
  #createComponent3D() {
231
246
  this.#component3D = document.createElement("pref-viewer-3d");
232
247
  this.#component3D.setAttribute("visible", "false");
248
+ this.#handlers.on3DSceneLoaded = () => {
249
+ this.#menu3DSyncSettings();
250
+ this.#menu3D?.setApplying(false);
251
+ };
252
+ this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
233
253
  this.#wrapper.appendChild(this.#component3D);
234
254
  }
235
255
 
256
+ /**
257
+ * Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
258
+ * When a menu already exists it is removed so the DOM stays clean.
259
+ * @private
260
+ * @returns {void}
261
+ */
262
+ #createMenu3D() {
263
+ if (!this.#wrapper) {
264
+ return;
265
+ }
266
+ if (this.#menu3D) {
267
+ this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
268
+ this.#menu3D.remove();
269
+ }
270
+ this.#menu3D = document.createElement("pref-viewer-menu-3d");
271
+ if (typeof this.#menu3D.setCulture === "function") {
272
+ this.#menu3D.setCulture(this.#culture);
273
+ } else {
274
+ this.#menu3D.setAttribute("culture", this.#culture);
275
+ }
276
+
277
+ if (!this.#handlers.onMenuApply) {
278
+ this.#handlers.onMenuApply = this.#onMenuApply.bind(this);
279
+ }
280
+ this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
281
+ this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
282
+
283
+ this.#menu3D.addEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
284
+ this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
285
+ this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
286
+
287
+ this.#wrapper.appendChild(this.#menu3D);
288
+ this.#menu3DSyncSettings();
289
+ this.#menu3DUpdateAvailability();
290
+ }
291
+
292
+ /**
293
+ * Reads the last persisted locale from localStorage so the viewer restores user preference.
294
+ * @private
295
+ * @returns {string|null} Resolved locale or null when unavailable.
296
+ */
297
+ #cultureReadPersisted() {
298
+ if (typeof window === "undefined") {
299
+ return null;
300
+ }
301
+ try {
302
+ return window.localStorage?.getItem(this.#CULTURE_STORAGE_KEY);
303
+ } catch (_error) {
304
+ return null;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Persists the active locale in localStorage so it survives reloads and sessions.
310
+ * @private
311
+ * @param {string} culture - Locale identifier to store.
312
+ * @returns {void}
313
+ */
314
+ #culturePersist(culture) {
315
+ if (typeof window === "undefined") {
316
+ return;
317
+ }
318
+ try {
319
+ window.localStorage?.setItem(this.#CULTURE_STORAGE_KEY, culture);
320
+ } catch (_error) {}
321
+ }
322
+
323
+ /**
324
+ * Mirrors the internal `#culture` value back to the `culture` attribute without feedback loops.
325
+ * @private
326
+ * @param {string} value - Locale identifier that should be reflected.
327
+ * @returns {void}
328
+ */
329
+ #cultureReflectAttribute(value) {
330
+ if (this.#cultureSuppressAttribute) {
331
+ return;
332
+ }
333
+ if (this.getAttribute("culture") === value) {
334
+ return;
335
+ }
336
+ this.#cultureSuppressAttribute = true;
337
+ this.setAttribute("culture", value);
338
+ this.#cultureSuppressAttribute = false;
339
+ }
340
+
341
+ /**
342
+ * Synchronizes the menu switches with the current render settings reported by PrefViewer3D.
343
+ * Skips the update when either the menu or the getter API is unavailable.
344
+ * @private
345
+ * @returns {void}
346
+ */
347
+ #menu3DSyncSettings() {
348
+ if (!this.#menu3D || !this.#component3D?.getRenderSettings) {
349
+ return;
350
+ }
351
+ const settings = this.#component3D.getRenderSettings();
352
+ if (settings) {
353
+ this.#menu3D.setSettings(settings);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Enables or disables the render menu depending on the active viewer mode.
359
+ * Clears hover/open states when the viewer switches to 2D so stale UI does not linger.
360
+ * @private
361
+ * @returns {void}
362
+ */
363
+ #menu3DUpdateAvailability() {
364
+ if (!this.#menu3D) {
365
+ return;
366
+ }
367
+ const isMode3D = this.#mode === "3d";
368
+ if (!isMode3D) {
369
+ this.#menu3D.removeAttribute("data-viewer-hover");
370
+ this.#menu3D.removeAttribute("data-open");
371
+ }
372
+ this.#menu3D.setEnabled?.(isMode3D);
373
+ }
374
+
375
+ /**
376
+ * Applies render toggles via PrefViewer3D, showing progress in the menu and resyncing on completion.
377
+ * When no changes occur, the menu is simply refreshed to match the controller snapshot.
378
+ * @private
379
+ * @param {object} settings - Partial render settings (AA, SSAO, IBL, shadows) requested by the menu.
380
+ * @returns {Promise<void>}
381
+ */
382
+ async #applyRenderSettings(settings) {
383
+ if (!this.#component3D) {
384
+ return;
385
+ }
386
+ this.#menu3D?.setApplying(true);
387
+ try {
388
+ const result = await this.#component3D.applyRenderSettings(settings);
389
+ if (!result?.changed) {
390
+ const controllerSettings = this.#component3D.getRenderSettings();
391
+ if (controllerSettings) {
392
+ this.#menu3D?.setSettings(controllerSettings);
393
+ }
394
+ this.#menu3D?.setApplying(false);
395
+ }
396
+ } catch (error) {
397
+ console.error("PrefViewer: failed to apply render settings", error);
398
+ this.#menu3D?.setApplying(false, true);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Resolves the requested culture, updates internal/global state, and persists the selection.
404
+ * @private
405
+ * @param {string} [cultureId] - Desired locale to activate.
406
+ * @param {object} [options] - Extra flags for the update.
407
+ * @param {boolean} [options.reflectAttribute=false] - Whether to mirror the attribute.
408
+ * @returns {boolean} True when the culture actually changes.
409
+ */
410
+ #applyCulture(cultureId, options = {}) {
411
+ let changed = false;
412
+ if (!cultureId || typeof cultureId !== "string") {
413
+ return changed;
414
+ }
415
+ const cultureInList = this.#cultureList.find((c) => c.toLowerCase() === cultureId.toLowerCase());
416
+ if (!cultureInList) {
417
+ return changed;
418
+ }
419
+ const { reflectAttribute = false } = options;
420
+ const resolved = resolveLocale(cultureId);
421
+ changed = resolved !== this.#culture;
422
+ if (reflectAttribute) {
423
+ this.#cultureReflectAttribute(resolved);
424
+ }
425
+ if (changed) {
426
+ this.#culture = resolved;
427
+ setLocale(resolved);
428
+ this.#menu3D?.setCulture?.(resolved);
429
+ this.#culturePersist(resolved);
430
+ }
431
+ return changed;
432
+ }
433
+
236
434
  /**
237
435
  * Adds a new task to the internal queue for processing.
238
436
  * If the viewer is initialized and not currently loading, immediately processes the next task.
@@ -260,10 +458,16 @@ export default class PrefViewer extends HTMLElement {
260
458
  }
261
459
  const task = this.#taskQueue[0];
262
460
  this.#taskQueue.shift();
461
+ if (this.#dialog) {
462
+ this.closeDialog();
463
+ }
263
464
  switch (task.type) {
264
465
  case PrefViewerTask.Types.Config:
265
466
  this.#processConfig(task.value);
266
467
  break;
468
+ case PrefViewerTask.Types.Culture:
469
+ this.#processCulture(task.value);
470
+ break;
267
471
  case PrefViewerTask.Types.Drawing:
268
472
  this.#processDrawing(task.value);
269
473
  break;
@@ -396,6 +600,22 @@ export default class PrefViewer extends HTMLElement {
396
600
  this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
397
601
  }
398
602
 
603
+ /**
604
+ * Handles the custom "pref-viewer-menu-apply" event emitted by the menu component.
605
+ * Forwards the requested settings to the render-application pipeline.
606
+ * @private
607
+ * @param {CustomEvent} event - Menu event containing the `settings` payload.
608
+ * @returns {void}
609
+ */
610
+ #onMenuApply(event) {
611
+ event.stopPropagation();
612
+ const settings = event.detail?.settings;
613
+ if (!settings) {
614
+ return;
615
+ }
616
+ this.#applyRenderSettings(settings);
617
+ }
618
+
399
619
  /**
400
620
  * Processes a configuration object by loading it into the 3D component.
401
621
  * Dispatches loading events and processes the next task when finished.
@@ -415,6 +635,17 @@ export default class PrefViewer extends HTMLElement {
415
635
  });
416
636
  }
417
637
 
638
+ /**
639
+ * Handles queued culture changes by applying the locale and advancing the task queue.
640
+ * @private
641
+ * @param {string} culture - Locale identifier coming from config, attributes, or API calls.
642
+ * @returns {void}
643
+ */
644
+ #processCulture(culture) {
645
+ this.#applyCulture(culture, { reflectAttribute: true });
646
+ this.#processNextTask();
647
+ }
648
+
418
649
  /**
419
650
  * Processes a drawing object by loading it into the 2D component.
420
651
  * Processes the next task when finished.
@@ -517,31 +748,119 @@ export default class PrefViewer extends HTMLElement {
517
748
  */
518
749
 
519
750
  /**
520
- * Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
751
+ * Initiates download of the current complete scene (3D model and environment) in GLB format.
521
752
  * @public
522
- * @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
523
753
  * @returns {void}
524
754
  */
525
- setMode(mode = this.#mode) {
526
- mode = mode.toLowerCase();
527
- if (mode !== "2d" && mode !== "3d") {
528
- console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
529
- mode = this.#mode;
755
+ downloadModelAndSceneGLB() {
756
+ if (!this.#component3D) {
757
+ return;
530
758
  }
531
- this.#mode = mode;
532
- if (mode === "2d") {
533
- this.#component3D?.hide();
534
- this.#component2D?.show();
535
- } else {
536
- this.#component2D?.hide();
537
- this.#component3D?.show();
759
+
760
+ this.#component3D.downloadModelAndSceneGLB();
761
+ }
762
+
763
+ /**
764
+ * Initiates download of the current complete scene (3D model and environment) in GLTF format.
765
+ * @public
766
+ * @returns {void}
767
+ */
768
+ downloadModelAndSceneGLTF() {
769
+ if (!this.#component3D) {
770
+ return;
538
771
  }
539
- if (this.getAttribute("mode") !== mode) {
540
- this.setAttribute("mode", mode);
541
- if (this.#dialog) {
542
- this.closeDialog();
543
- }
772
+
773
+ this.#component3D.downloadModelAndSceneGLTF();
774
+ }
775
+
776
+ /**
777
+ * Initiates download of the current complete scene (3D model and environment) in USDZ format.
778
+ * @public
779
+ * @returns {void}
780
+ */
781
+ downloadModelAndSceneUSDZ() {
782
+ if (!this.#component3D) {
783
+ return;
544
784
  }
785
+
786
+ this.#component3D.downloadModelAndSceneUSDZ();
787
+ }
788
+
789
+ /**
790
+ * Initiates download of the current 3D model in GLB format.
791
+ * @public
792
+ * @returns {void}
793
+ */
794
+ downloadModelGLB() {
795
+ if (!this.#component3D) {
796
+ return;
797
+ }
798
+
799
+ this.#component3D.downloadModelGLB();
800
+ }
801
+
802
+ /**
803
+ * Initiates download of the current 3D model in GLTF format.
804
+ * @public
805
+ * @returns {void}
806
+ */
807
+ downloadModelGLTF() {
808
+ if (!this.#component3D) {
809
+ return;
810
+ }
811
+
812
+ this.#component3D.downloadModelGLTF();
813
+ }
814
+
815
+ /**
816
+ * Initiates download of the current 3D model in USDZ format.
817
+ * @public
818
+ * @returns {void}
819
+ */
820
+ downloadModelUSDZ() {
821
+ if (!this.#component3D) {
822
+ return;
823
+ }
824
+
825
+ this.#component3D.downloadModelUSDZ();
826
+ }
827
+
828
+ /**
829
+ * Initiates download of the current 3D environment in GLB format.
830
+ * @public
831
+ * @returns {void}
832
+ */
833
+ downloadSceneGLB() {
834
+ if (!this.#component3D) {
835
+ return;
836
+ }
837
+
838
+ this.#component3D.downloadSceneGLB();
839
+ }
840
+
841
+ /**
842
+ * Initiates download of the current 3D environment in GLTF format.
843
+ * @public
844
+ * @returns {void}
845
+ */
846
+ downloadSceneGLTF() {
847
+ if (!this.#component3D) {
848
+ return;
849
+ }
850
+
851
+ this.#component3D.downloadSceneGLTF();
852
+ }
853
+
854
+ /**
855
+ * Initiates download of the current 3D environment in USDZ format.
856
+ * @public
857
+ * @returns {void}
858
+ */
859
+ downloadSceneUSDZ() {
860
+ if (!this.#component3D) {
861
+ return;
862
+ }
863
+ this.#component3D.downloadSceneUSDZ();
545
864
  }
546
865
 
547
866
  /**
@@ -556,10 +875,41 @@ export default class PrefViewer extends HTMLElement {
556
875
  if (!config) {
557
876
  return false;
558
877
  }
878
+ this.#addTaskToQueue(config, "config");
559
879
  if (config.drawing) {
560
880
  this.#addTaskToQueue(config.drawing, "drawing");
561
881
  }
562
- this.#addTaskToQueue(config, "config");
882
+ if (config.culture) {
883
+ this.#addTaskToQueue(config.culture, "culture");
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Loads a drawing object or JSON string and adds it to the task queue.
889
+ * @public
890
+ * @param {object|string} drawing - Drawing object or JSON string.
891
+ * @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
892
+ */
893
+ loadDrawing(drawing) {
894
+ drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
895
+ if (!drawing) {
896
+ return false;
897
+ }
898
+ this.#addTaskToQueue(drawing, "drawing");
899
+ }
900
+
901
+ /**
902
+ * Loads materials object or JSON string and adds it to the task queue.
903
+ * @public
904
+ * @param {object|string} materials - Materials object or JSON string.
905
+ * @returns {boolean|void} Returns false if materials are invalid; otherwise void.
906
+ */
907
+ loadMaterials(materials) {
908
+ materials = typeof materials === "string" ? JSON.parse(materials) : materials;
909
+ if (!materials) {
910
+ return false;
911
+ }
912
+ this.#addTaskToQueue(materials, "materials");
563
913
  }
564
914
 
565
915
  /**
@@ -591,31 +941,42 @@ export default class PrefViewer extends HTMLElement {
591
941
  }
592
942
 
593
943
  /**
594
- * Loads materials object or JSON string and adds it to the task queue.
944
+ * Sets the active culture for UI translations and propagates it to child components.
595
945
  * @public
596
- * @param {object|string} materials - Materials object or JSON string.
597
- * @returns {boolean|void} Returns false if materials are invalid; otherwise void.
946
+ * @param {string} [cultureId] - Locale identifier, e.g., "en-EN".
947
+ * @returns {void}
598
948
  */
599
- loadMaterials(materials) {
600
- materials = typeof materials === "string" ? JSON.parse(materials) : materials;
601
- if (!materials) {
602
- return false;
603
- }
604
- this.#addTaskToQueue(materials, "materials");
949
+ setCulture(cultureId) {
950
+ this.#addTaskToQueue(cultureId, "culture");
605
951
  }
606
952
 
607
953
  /**
608
- * Loads a drawing object or JSON string and adds it to the task queue.
954
+ * Sets the viewer mode to "2d" or "3d" and updates component visibility accordingly.
609
955
  * @public
610
- * @param {object|string} drawing - Drawing object or JSON string.
611
- * @returns {boolean|void} Returns false if drawing is invalid; otherwise void.
956
+ * @param {string} [mode=this.#mode] - The mode to set ("2d" or "3d").
957
+ * @returns {void}
612
958
  */
613
- loadDrawing(drawing) {
614
- drawing = typeof drawing === "string" ? JSON.parse(drawing) : drawing;
615
- if (!drawing) {
616
- return false;
959
+ setMode(mode = this.#mode) {
960
+ mode = mode.toLowerCase();
961
+ if (mode !== "2d" && mode !== "3d") {
962
+ console.warn(`PrefViewer: invalid mode "${mode}". Allowed modes are "2d" and "3d".`);
963
+ mode = this.#mode;
964
+ }
965
+ this.#mode = mode;
966
+ if (mode === "2d") {
967
+ this.#component3D?.hide();
968
+ this.#component2D?.show();
969
+ } else {
970
+ this.#component2D?.hide();
971
+ this.#component3D?.show();
972
+ }
973
+ this.#menu3DUpdateAvailability();
974
+ if (this.getAttribute("mode") !== mode) {
975
+ this.setAttribute("mode", mode);
976
+ if (this.#dialog) {
977
+ this.closeDialog();
978
+ }
617
979
  }
618
- this.#addTaskToQueue(drawing, "drawing");
619
980
  }
620
981
 
621
982
  /**
@@ -632,6 +993,39 @@ export default class PrefViewer extends HTMLElement {
632
993
  this.#addTaskToQueue(options, "options");
633
994
  }
634
995
 
996
+ /**
997
+ * Opens a modal dialog with the specified title, content, and footer.
998
+ * @public
999
+ * @param {string} title - The dialog title to display in the header.
1000
+ * @param {string} content - The HTML content to display in the dialog body.
1001
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
1002
+ * @returns {HTMLElement} The created dialog element.
1003
+ * @description
1004
+ * If a dialog is already open, it is closed before opening the new one.
1005
+ * The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
1006
+ */
1007
+ openDialog(title, content, footer) {
1008
+ if (this.#dialog && this.#dialog.hasAttribute("open")) {
1009
+ this.#dialog.close();
1010
+ }
1011
+ this.#dialog = document.createElement("pref-viewer-dialog");
1012
+ this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
1013
+ const opened = this.#dialog.open(title, content, footer);
1014
+ return opened ? this.#dialog : null;
1015
+ }
1016
+
1017
+ /**
1018
+ * Closes the currently open dialog, if any, and removes it from the DOM.
1019
+ * @public
1020
+ * @returns {void}
1021
+ */
1022
+ closeDialog() {
1023
+ if (this.#dialog) {
1024
+ this.#dialog.close();
1025
+ this.#dialog = null;
1026
+ }
1027
+ }
1028
+
635
1029
  /**
636
1030
  * Shows the 3D model by setting its visibility to true.
637
1031
  * Adds a visibility task to the queue for processing.
@@ -733,160 +1127,98 @@ export default class PrefViewer extends HTMLElement {
733
1127
  }
734
1128
 
735
1129
  /**
736
- * Initiates download of the current 3D model in GLB format.
737
- * @public
738
- * @returns {void}
1130
+ * ---------------------------
1131
+ * Public properties (setters)
1132
+ * ---------------------------
739
1133
  */
740
- downloadModelGLB() {
741
- if (!this.#component3D) {
742
- return;
743
- }
744
-
745
- this.#component3D.downloadModelGLB();
746
- }
747
1134
 
748
1135
  /**
749
- * Initiates download of the current 3D model in GLTF format.
1136
+ * Accepts a config object or JSON string and forwards it to `loadConfig()`.
750
1137
  * @public
751
- * @returns {void}
1138
+ * @param {object|string} value - Configuration payload or serialized JSON.
752
1139
  */
753
- downloadModelGLTF() {
754
- if (!this.#component3D) {
755
- return;
756
- }
757
-
758
- this.#component3D.downloadModelGLTF();
1140
+ set config(value) {
1141
+ this.loadConfig(value);
759
1142
  }
760
1143
 
761
1144
  /**
762
- * Initiates download of the current 3D model in USDZ format.
1145
+ * Mirrors `setCulture()` so templates can change the locale via attributes or props.
763
1146
  * @public
764
- * @returns {void}
1147
+ * @param {string} value - Locale identifier such as "es-ES".
765
1148
  */
766
- downloadModelUSDZ() {
767
- if (!this.#component3D) {
768
- return;
769
- }
770
-
771
- this.#component3D.downloadModelUSDZ();
1149
+ set culture(value) {
1150
+ this.setCulture(value);
772
1151
  }
773
1152
 
774
1153
  /**
775
- * Initiates download of the current complete scene (3D model and environment) in GLB format.
1154
+ * Routes incoming drawing data to `loadDrawing()` for queued processing.
776
1155
  * @public
777
- * @returns {void}
1156
+ * @param {object|string} value - Drawing payload or serialized JSON.
778
1157
  */
779
- downloadModelAndSceneGLB() {
780
- if (!this.#component3D) {
781
- return;
782
- }
783
-
784
- this.#component3D.downloadModelAndSceneGLB();
1158
+ set drawing(value) {
1159
+ this.loadDrawing(value);
785
1160
  }
786
1161
 
787
1162
  /**
788
- * Initiates download of the current complete scene (3D model and environment) in GLTF format.
1163
+ * Convenience bridge to `loadMaterials()` for template bindings.
789
1164
  * @public
790
- * @returns {void}
1165
+ * @param {object|string} value - Materials definition or JSON string.
791
1166
  */
792
- downloadModelAndSceneGLTF() {
793
- if (!this.#component3D) {
794
- return;
795
- }
796
-
797
- this.#component3D.downloadModelAndSceneGLTF();
1167
+ set materials(value) {
1168
+ this.loadMaterials(value);
798
1169
  }
799
1170
 
800
1171
  /**
801
- * Initiates download of the current complete scene (3D model and environment) in USDZ format.
1172
+ * Delegates to `setMode()` so consumers can switch between 2D/3D through a setter.
802
1173
  * @public
803
- * @returns {void}
1174
+ * @param {string} value - Desired viewer mode, e.g., "2d" or "3d".
804
1175
  */
805
- downloadModelAndSceneUSDZ() {
806
- if (!this.#component3D) {
807
- return;
808
- }
809
-
810
- this.#component3D.downloadModelAndSceneUSDZ();
1176
+ set mode(value) {
1177
+ this.setMode(value);
811
1178
  }
812
1179
 
813
1180
  /**
814
- * Initiates download of the current 3D environment in GLB format.
1181
+ * Forwards models supplied via property assignment to `loadModel()`.
815
1182
  * @public
816
- * @returns {void}
1183
+ * @param {object|string} value - Model payload or serialized JSON.
817
1184
  */
818
- downloadSceneGLB() {
819
- if (!this.#component3D) {
820
- return;
821
- }
822
-
823
- this.#component3D.downloadSceneGLB();
1185
+ set model(value) {
1186
+ this.loadModel(value);
824
1187
  }
825
1188
 
826
1189
  /**
827
- * Initiates download of the current 3D environment in GLTF format.
1190
+ * Sends option updates to `setOptions()` enabling reactive bindings.
828
1191
  * @public
829
- * @returns {void}
1192
+ * @param {object|string} value - Options object or JSON string.
830
1193
  */
831
- downloadSceneGLTF() {
832
- if (!this.#component3D) {
833
- return;
834
- }
835
-
836
- this.#component3D.downloadSceneGLTF();
1194
+ set options(value) {
1195
+ this.setOptions(value);
837
1196
  }
838
1197
 
839
1198
  /**
840
- * Initiates download of the current 3D environment in USDZ format.
1199
+ * Redirects scene assignments to `loadScene()` for normalized handling.
841
1200
  * @public
842
- * @returns {void}
1201
+ * @param {object|string} value - Scene payload or serialized JSON.
843
1202
  */
844
- downloadSceneUSDZ() {
845
- if (!this.#component3D) {
846
- return;
847
- }
848
- this.#component3D.downloadSceneUSDZ();
1203
+ set scene(value) {
1204
+ this.loadScene(value);
849
1205
  }
850
1206
 
851
1207
  /**
852
- * Opens a modal dialog with the specified title, content, and footer.
853
- * @public
854
- * @param {string} title - The dialog title to display in the header.
855
- * @param {string} content - The HTML content to display in the dialog body.
856
- * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
857
- * @returns {HTMLElement} The created dialog element.
858
- * @description
859
- * If a dialog is already open, it is closed before opening the new one.
860
- * The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
1208
+ * ---------------------------
1209
+ * Public properties (getters)
1210
+ * ---------------------------
861
1211
  */
862
- openDialog(title, content, footer) {
863
- if (this.#dialog && this.#dialog.hasAttribute("open")) {
864
- this.#dialog.close();
865
- }
866
- this.#dialog = document.createElement("pref-viewer-dialog");
867
- this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
868
- const opened = this.#dialog.open(title, content, footer);
869
- return opened ? this.#dialog : null;
870
- }
871
1212
 
872
1213
  /**
873
- * Closes the currently open dialog, if any, and removes it from the DOM.
1214
+ * Exposes the currently active locale so hosts can inspect which translations are applied.
874
1215
  * @public
875
- * @returns {void}
1216
+ * @returns {string} Locale identifier, e.g., "en-EN".
876
1217
  */
877
- closeDialog() {
878
- if (this.#dialog) {
879
- this.#dialog.close();
880
- this.#dialog = null;
881
- }
1218
+ get culture() {
1219
+ return this.#culture;
882
1220
  }
883
1221
 
884
- /**
885
- * ---------------------------
886
- * Public properties
887
- * ---------------------------
888
- */
889
-
890
1222
  /**
891
1223
  * Indicates whether the viewer has been initialized.
892
1224
  * @public