@preference-sl/pref-viewer 2.16.0-beta.1 → 2.16.0-beta.11

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,5 +1,5 @@
1
1
  import PrefViewerTask from "./pref-viewer-task.js";
2
- import { PrefViewerStyles } from "./styles.js";
2
+ import { PREF_VIEWER_STYLES_STORAGE_KEY, PrefViewerStyles, PrefViewerStyleVariables } from "./styles.js";
3
3
  import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./localization/i18n.js";
4
4
 
5
5
  /**
@@ -9,10 +9,10 @@ import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./l
9
9
  * - Hosts synchronized 2D (SVG) and 3D (Babylon.js) canvases plus a render-settings menu inside a single shadow DOM wrapper.
10
10
  * - Queues mutations (config/model/scene/materials/drawing/options/culture) to guarantee deterministic execution and avoid race conditions.
11
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.
12
+ * - Persists the active locale and supported style variables to localStorage, mirrors locale to PrefViewerMenu3D, and keeps the shared i18n module in sync with the chosen culture.
13
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.
14
+ * - Emits granular lifecycle custom events so host applications can react to loading progress, drawing zoom changes, selection changes, and menu interactions.
15
+ * - Exposes imperative methods for toggling visibility, updating options, switching modes, controlling animations, managing selection state, and requesting downloads without attribute juggling.
16
16
  * - Automatically reflects supported attributes back to the DOM so templates, frameworks, and manual scripting stay aligned with viewer state.
17
17
  *
18
18
  * Usage:
@@ -20,29 +20,49 @@ import { DEFAULT_LOCALE, resolveLocale, setLocale, SUPPORTED_LOCALES } from "./l
20
20
  * - Feed serialized JSON strings or plain objects to `loadConfig`, `loadModel`, `loadScene`, `loadMaterials`, `loadDrawing`, or `setOptions`.
21
21
  * - Drive localization by setting the `culture` attribute/property or invoking `setCulture("es-ES")`, which propagates to the menu and dialog copy.
22
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.
23
+ * - Listen for `pref-viewer-initialized`, `pref-viewer-loading`, `pref-viewer-loaded`, `pref-viewer-error`, `pref-viewer-3d-loaded`, `pref-viewer-2d-loaded`, and `pref-viewer-2d-viewport-changed` to integrate with surrounding UX.
24
24
  *
25
25
  * Reactive Attributes:
26
26
  * - config: Configuration descriptor that can bundle model/scene/materials/drawing/culture hints.
27
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.
28
+ * - options: JSON describing render toggles (AA, SSAO, IBL, shadows), animation menu, highlight, lighting, and misc viewer knobs.
29
29
  * - culture: Locale identifier (RFC-5646 style) that resolves against `SUPPORTED_LOCALES` before applying.
30
30
  * - mode: Switches between `2d` and `3d`, automatically hiding/showing the relevant subcomponents and menu states.
31
31
  * - show-model / show-scene: Boolean flags to toggle visibility without reloading the underlying resources.
32
+ * - show-render-menu: Boolean flag to show or hide the render-settings menu (default: true).
33
+ * - styles: JSON style overrides. Currently supports `colorPrimary`, persisted in localStorage and exposed as `--pref-viewer-color-primary`.
32
34
  *
33
35
  * Public Methods (selection):
34
36
  * - loadConfig / loadModel / loadScene / loadMaterials / loadDrawing: Queue asset ingestion jobs.
35
- * - setOptions / setMode / setCulture: Update runtime behavior, render mode, or locale.
37
+ * - setOptions / setIBL / setMode / setCulture / setStyles: Update runtime behavior, IBL lighting, render mode, locale, or supported visual variables.
36
38
  * - showModel / hideModel / showScene / hideScene: Toggle visibility using the internal task queue for consistent sequencing.
37
39
  * - 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.
40
+ * - downloadModelGLB / downloadModelGLTF / downloadModelUSDZ: Export the loaded model in the requested format.
41
+ * - downloadSceneGLB / downloadSceneGLTF / downloadSceneUSDZ: Export the loaded environment in the requested format.
42
+ * - downloadModelAndSceneGLB / downloadModelAndSceneGLTF / downloadModelAndSceneUSDZ: Export the combined scene in the requested format.
39
43
  * - openDialog / closeDialog: Manage PrefViewerDialog instances rendered inside the shadow tree.
44
+ * - setSelectableElements / setSelectedElements / applySelectionState: Drive the semantic selection contract.
45
+ * - clearSelectedElements / clearSelectableElements / clearSelectionState: Reset selection state fully or partially.
46
+ * - getSelectionState: Return a snapshot of the current selection.
47
+ * - notifyElementClick: Forward a host-side element click into the viewer event stream.
48
+ * - startTurnable / stopTurnable / toggleTurnable / getTurnableState: Control and inspect the auto-rotate state.
49
+ * - getAnimations / playAnimation / setAnimationProgress / setAnimationLoop: Inspect and control named glTF animations.
50
+ * - getDebugInfo: Return a diagnostic snapshot of the active 3D runtime.
40
51
  *
41
52
  * Public Properties:
42
- * - culture, isInitialized, isLoaded, isLoading, isMode2D, isMode3D expose runtime state for host inspection.
53
+ * - Getters: culture, isInitialized, isLoaded, isLoading, isMode2D, isMode3D expose runtime state for host inspection.
54
+ * - Setters: config, culture, drawing, materials, mode, model, options, scene, and styles mirror the corresponding load/set methods for reactive bindings.
43
55
  *
44
56
  * Events:
45
- * - `scene-loading` / `scene-loaded` announce Babylon work, `drawing-loading` / `drawing-loaded` cover SVG pipelines, `drawing-zoom-changed` proxies pan/zoom delta.
57
+ * - `pref-viewer-initialized`: emitted after the host and internal components are mounted and public methods/properties are available.
58
+ * - `pref-viewer-loading` / `pref-viewer-loaded` / `pref-viewer-error`: generic lifecycle events that fire for both 2D and 3D pipelines.
59
+ * - `pref-viewer-3d-loading` / `pref-viewer-3d-loaded` / `pref-viewer-3d-error`: announce Babylon.js work start, completion, and failure.
60
+ * - `pref-viewer-2d-loading` / `pref-viewer-2d-loaded` / `pref-viewer-2d-error`: announce SVG pipeline start, completion, and failure.
61
+ * - `pref-viewer-2d-viewport-changed`: proxies pan/zoom delta from the 2D controller.
62
+ * - `pref-viewer-selection-state-changed`: emitted whenever the selectable or selected element sets change.
63
+ * - `pref-viewer-3d-turnable-changed`: emitted after the auto-rotate state starts, stops, or is toggled.
64
+ * - `pref-viewer-element-click`: emitted when the host forwards an element click via `notifyElementClick`.
65
+ * - Legacy `scene-*` and `drawing-*` aliases are still emitted alongside their modern equivalents for backward compatibility.
46
66
  *
47
67
  * Notes:
48
68
  * - Internally wires PrefViewerMenu3D hover/apply events to PrefViewer3D render settings, ensuring persisted toggles stay in sync with BabylonJSController.
@@ -56,6 +76,16 @@ export default class PrefViewer extends HTMLElement {
56
76
  #isLoaded = false;
57
77
  #isLoading = false;
58
78
  #mode = "3d";
79
+ #selectionState = {
80
+ selectableIds: [],
81
+ selectedIds: [],
82
+ selectedItems: [],
83
+ };
84
+ #turnableState = {
85
+ running: false,
86
+ speed: 1,
87
+ direction: "clockwise",
88
+ };
59
89
 
60
90
  // Task queue for sequential processing
61
91
  #taskQueue = [];
@@ -66,7 +96,12 @@ export default class PrefViewer extends HTMLElement {
66
96
  #cultureList = SUPPORTED_LOCALES;
67
97
  #culture = "";
68
98
  #cultureSuppressAttribute = false;
69
- #suppressVisibilityAttributeCallback = false;
99
+
100
+ // Runtime style management
101
+ #STYLES_STORAGE_KEY = PREF_VIEWER_STYLES_STORAGE_KEY;
102
+ #styles = {
103
+ colorPrimary: PrefViewerStyleVariables.colorPrimary,
104
+ };
70
105
 
71
106
  // References to internal components
72
107
  #wrapper = null;
@@ -99,6 +134,8 @@ export default class PrefViewer extends HTMLElement {
99
134
  } else {
100
135
  this.#culture = setLocale(this.#cultureInitial);
101
136
  }
137
+
138
+ this.#applyStyles(this.#stylesReadPersisted(), { persist: false, applyCssVariables: false });
102
139
  }
103
140
 
104
141
  /**
@@ -107,13 +144,13 @@ export default class PrefViewer extends HTMLElement {
107
144
  * @returns {string[]} Array of attribute names to observe.
108
145
  */
109
146
  static get observedAttributes() {
110
- return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene", "show-menu", "accent-color", "show-animation-menu", "highlight-color", "highlight-enabled"];
147
+ return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene", "show-render-menu", "styles"];
111
148
  }
112
149
 
113
150
  /**
114
151
  * Observes changes to specific attributes and triggers corresponding actions.
115
152
  * Loads configuration, drawing, model, scene, materials, or options when their attributes change.
116
- * Toggles model or scene visibility when "show-model" or "show-scene" attributes change.
153
+ * Toggles model or scene visibility when "show-model" or "show-scene" attributes change, recreates the 3D menu when "show-render-menu" changes, and queues style overrides when "styles" changes.
117
154
  * @public
118
155
  * @param {string} name - The name of the changed attribute.
119
156
  * @param {*} _old - The previous value of the attribute (unused).
@@ -121,9 +158,6 @@ export default class PrefViewer extends HTMLElement {
121
158
  * @returns {void}
122
159
  */
123
160
  attributeChangedCallback(name, _old, value) {
124
- if (this.#suppressVisibilityAttributeCallback && (name === "show-model" || name === "show-scene")) {
125
- return;
126
- }
127
161
  switch (name) {
128
162
  case "config":
129
163
  this.loadConfig(value);
@@ -178,16 +212,12 @@ export default class PrefViewer extends HTMLElement {
178
212
  const showScene = value.toLowerCase() === "true";
179
213
  showScene ? this.showScene() : this.hideScene();
180
214
  break;
181
- case "show-menu":
215
+ case "show-render-menu":
182
216
  // Recreate menu to apply show/hide setting
183
217
  this.#createMenu3D();
184
218
  break;
185
- case "accent-color":
186
- case "show-animation-menu":
187
- case "highlight-color":
188
- case "highlight-enabled":
189
- // Pass these attributes to the 3D component and menu
190
- this.#updateComponentAttributes(name, value);
219
+ case "styles":
220
+ this.setStyles(value);
191
221
  break;
192
222
  }
193
223
  }
@@ -199,24 +229,24 @@ export default class PrefViewer extends HTMLElement {
199
229
  * @returns {void|boolean} Returns false if initialization fails; otherwise void.
200
230
  */
201
231
  connectedCallback() {
202
- this.#wrapper = document.createElement("div");
203
- this.#wrapper.classList.add("pref-viewer-wrapper");
204
- this.shadowRoot.append(this.#wrapper);
205
-
206
- const style = document.createElement("style");
207
- style.textContent = PrefViewerStyles;
208
- this.shadowRoot.append(style);
209
-
232
+ this.#createShadowDOM();
233
+ this.#stylesApplyCssVariables();
210
234
  this.#createComponent3D();
211
235
  this.#createComponent2D();
212
236
  this.#createMenu3D();
213
237
 
214
- if (!this.hasAttribute("mode")) {
215
- this.setMode();
216
- }
238
+ this.setMode(this.getAttribute("mode") ?? this.#mode);
217
239
 
218
240
  this.#isInitialized = true;
219
- this.#processNextTask();
241
+ Promise.resolve().then(() => {
242
+ if (!this.isConnected || !this.#isInitialized) {
243
+ return;
244
+ }
245
+ this.#dispatchPrefViewerEvent("pref-viewer-initialized", {
246
+ isInitialized: this.#isInitialized,
247
+ });
248
+ this.#processNextTask();
249
+ });
220
250
  }
221
251
 
222
252
  /**
@@ -233,21 +263,41 @@ export default class PrefViewer extends HTMLElement {
233
263
  this.#dialog?.remove();
234
264
  }
235
265
  if (this.#component2D) {
236
- this.#component2D.removeEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
266
+ this.#component2D.removeEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
237
267
  this.#component2D.remove();
238
268
  }
239
269
  if (this.#component3D) {
240
- this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
241
- this.#component3D.removeEventListener("scene-error", this.#handlers.on3DSceneError);
270
+ this.#component3D.removeEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
271
+ this.#component3D.removeEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
242
272
  this.#component3D.remove();
243
273
  }
244
274
  if (this.#menu3D) {
245
- this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
275
+ this.#menu3D.removeEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
246
276
  this.#menu3D.remove();
247
277
  this.#menu3D = null;
248
278
  }
249
279
  }
250
280
 
281
+ /**
282
+ * Creates the root shadow DOM structure and host styles.
283
+ * @private
284
+ * @returns {void}
285
+ */
286
+ #createShadowDOM() {
287
+ this.#wrapper = document.createElement("div");
288
+ this.#wrapper.classList.add("pref-viewer-wrapper");
289
+ this.shadowRoot.append(this.#wrapper);
290
+
291
+ const link = document.createElement("link");
292
+ link.setAttribute("rel", "stylesheet");
293
+ link.setAttribute("href", "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
294
+ this.shadowRoot.append(link);
295
+
296
+ const style = document.createElement("style");
297
+ style.textContent = PrefViewerStyles;
298
+ this.shadowRoot.append(style);
299
+ }
300
+
251
301
  /**
252
302
  * Creates and appends the 2D viewer component to the shadow DOM.
253
303
  * Sets the "visible" attribute to true by default.
@@ -258,7 +308,7 @@ export default class PrefViewer extends HTMLElement {
258
308
  this.#handlers.on2DZoomChanged = this.#on2DZoomChanged.bind(this);
259
309
  this.#component2D = document.createElement("pref-viewer-2d");
260
310
  this.#component2D.setAttribute("visible", "false");
261
- this.#component2D.addEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
311
+ this.#component2D.addEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
262
312
  this.#wrapper.appendChild(this.#component2D);
263
313
  }
264
314
 
@@ -271,15 +321,7 @@ export default class PrefViewer extends HTMLElement {
271
321
  #createComponent3D() {
272
322
  this.#component3D = document.createElement("pref-viewer-3d");
273
323
  this.#component3D.setAttribute("visible", "false");
274
-
275
- // Pass new configuration attributes to 3D component
276
- const newAttrs = ["show-animation-menu", "highlight-color", "highlight-enabled"];
277
- for (const attr of newAttrs) {
278
- if (this.hasAttribute(attr)) {
279
- this.#component3D.setAttribute(attr, this.getAttribute(attr));
280
- }
281
- }
282
-
324
+
283
325
  this.#handlers.on3DSceneLoaded = () => {
284
326
  this.#menu3DSyncSettings();
285
327
  this.#menu3D?.setApplying(false);
@@ -287,15 +329,15 @@ export default class PrefViewer extends HTMLElement {
287
329
  this.#handlers.on3DSceneError = () => {
288
330
  this.#menu3D?.setApplying(false, true);
289
331
  };
290
- this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
291
- this.#component3D.addEventListener("scene-error", this.#handlers.on3DSceneError);
332
+ this.#component3D.addEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
333
+ this.#component3D.addEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
292
334
  this.#wrapper.appendChild(this.#component3D);
293
335
  }
294
336
 
295
337
  /**
296
338
  * Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
297
339
  * When a menu already exists it is removed so the DOM stays clean.
298
- * Respects the "show-menu" attribute to determine if menu should be visible.
340
+ * Respects the "show-render-menu" attribute to determine if the menu should be visible.
299
341
  * @private
300
342
  * @returns {void}
301
343
  */
@@ -304,7 +346,7 @@ export default class PrefViewer extends HTMLElement {
304
346
  return;
305
347
  }
306
348
  if (this.#menu3D) {
307
- this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
349
+ this.#menu3D.removeEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
308
350
  this.#menu3D.remove();
309
351
  this.#menu3D = null;
310
352
  }
@@ -318,13 +360,13 @@ export default class PrefViewer extends HTMLElement {
318
360
  }
319
361
 
320
362
  // Check if menu should be shown (default: true if not specified)
321
- const showMenuAttr = this.getAttribute("show-menu");
322
- const showMenu = showMenuAttr === null || showMenuAttr === "true";
363
+ const show3DMenuAttr = this.getAttribute("show-render-menu");
364
+ const show3DMenu = show3DMenuAttr === null || show3DMenuAttr === "true";
323
365
 
324
- if (!showMenu) {
366
+ if (!show3DMenu) {
325
367
  return;
326
368
  }
327
-
369
+
328
370
  this.#menu3D = document.createElement("pref-viewer-menu-3d");
329
371
  if (typeof this.#menu3D.setCulture === "function") {
330
372
  this.#menu3D.setCulture(this.#culture);
@@ -332,57 +374,22 @@ export default class PrefViewer extends HTMLElement {
332
374
  this.#menu3D.setAttribute("culture", this.#culture);
333
375
  }
334
376
 
335
- // Pass illumination-related attributes to menu
336
- const illumAttrs = ["show-illumination", "illumination-color"];
337
- for (const attr of illumAttrs) {
338
- if (this.hasAttribute(attr)) {
339
- this.#menu3D.setAttribute(attr, this.getAttribute(attr));
340
- }
341
- }
342
-
343
377
  if (!this.#handlers.onMenuApply) {
344
378
  this.#handlers.onMenuApply = this.#onMenuApply.bind(this);
345
379
  }
346
380
  this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
347
381
  this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
348
382
 
349
- this.#menu3D.addEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
383
+ this.#menu3D.addEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
350
384
  this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
351
385
  this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
352
386
 
353
387
  this.#wrapper.appendChild(this.#menu3D);
354
388
  this.#menu3DSyncSettings();
355
- this.#menu3DUpdateSwitchAvailability();
389
+ this.#menu3DUpdateElementAvailability();
356
390
  this.#menu3DUpdateAvailability();
357
391
  }
358
392
 
359
- /**
360
- * Updates attributes on child components (3D viewer and menu) based on parent attributes.
361
- * @private
362
- * @param {string} name - Attribute name.
363
- * @param {*} value - Attribute value.
364
- * @returns {void}
365
- */
366
- #updateComponentAttributes(name, value) {
367
- // Pass to 3D component
368
- if (this.#component3D) {
369
- if (value === null) {
370
- this.#component3D.removeAttribute(name);
371
- } else {
372
- this.#component3D.setAttribute(name, value);
373
- }
374
- }
375
-
376
- // Pass to menu
377
- if (this.#menu3D) {
378
- if (value === null) {
379
- this.#menu3D.removeAttribute(name);
380
- } else {
381
- this.#menu3D.setAttribute(name, value);
382
- }
383
- }
384
- }
385
-
386
393
  /**
387
394
  * Reads the last persisted locale from localStorage so the viewer restores user preference.
388
395
  * @private
@@ -432,6 +439,84 @@ export default class PrefViewer extends HTMLElement {
432
439
  this.#cultureSuppressAttribute = false;
433
440
  }
434
441
 
442
+ /**
443
+ * Reads persisted style overrides from localStorage.
444
+ * @private
445
+ * @returns {object} Persisted style overrides, or an empty object when unavailable.
446
+ */
447
+ #stylesReadPersisted() {
448
+ if (typeof window === "undefined") {
449
+ return {};
450
+ }
451
+ try {
452
+ const serialized = window.localStorage?.getItem(this.#STYLES_STORAGE_KEY);
453
+ return serialized ? JSON.parse(serialized) : {};
454
+ } catch (_error) {
455
+ return {};
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Persists current style overrides in localStorage.
461
+ * @private
462
+ * @returns {void}
463
+ */
464
+ #stylesPersist() {
465
+ if (typeof window === "undefined") {
466
+ return;
467
+ }
468
+ try {
469
+ window.localStorage?.setItem(this.#STYLES_STORAGE_KEY, JSON.stringify(this.#styles));
470
+ } catch (_error) {}
471
+ }
472
+
473
+ /**
474
+ * Keeps the host CSS custom properties in sync with the style variables.
475
+ * @private
476
+ * @returns {void}
477
+ */
478
+ #stylesApplyCssVariables() {
479
+ this.style.setProperty("--pref-viewer-color-primary", this.#styles.colorPrimary);
480
+ }
481
+
482
+ /**
483
+ * Applies supported style overrides to PrefViewerStyleVariables and the host CSS variables.
484
+ * @private
485
+ * @param {object} styles - Style overrides.
486
+ * @param {object} [options] - Apply options.
487
+ * @param {boolean} [options.persist=true] - Whether to persist the resolved styles.
488
+ * @param {boolean} [options.applyCssVariables=true] - Whether to update host CSS custom properties immediately.
489
+ * @returns {boolean} True when at least one supported style was applied.
490
+ */
491
+ #applyStyles(styles, options = {}) {
492
+ const { persist = true, applyCssVariables = true } = options;
493
+ if (!styles || typeof styles !== "object") {
494
+ return false;
495
+ }
496
+
497
+ const nextStyles = {};
498
+ if (typeof styles.colorPrimary === "string" && styles.colorPrimary.trim().length > 0) {
499
+ nextStyles.colorPrimary = styles.colorPrimary.trim();
500
+ }
501
+
502
+ if (!Object.keys(nextStyles).length) {
503
+ return false;
504
+ }
505
+
506
+ this.#styles = {
507
+ ...this.#styles,
508
+ ...nextStyles,
509
+ };
510
+ PrefViewerStyleVariables.colorPrimary = this.#styles.colorPrimary;
511
+ if (applyCssVariables) {
512
+ this.#stylesApplyCssVariables();
513
+ }
514
+ if (persist) {
515
+ this.#stylesPersist();
516
+ }
517
+ return true;
518
+ }
519
+
435
520
  /**
436
521
  * Synchronizes the menu switches with the current render settings reported by PrefViewer3D.
437
522
  * Skips the update when either the menu or the getter API is unavailable.
@@ -471,37 +556,45 @@ export default class PrefViewer extends HTMLElement {
471
556
  * @private
472
557
  * @returns {void}
473
558
  */
474
- #menu3DUpdateSwitchAvailability() {
559
+ #menu3DUpdateElementAvailability() {
475
560
  if (!this.#menu3D) {
476
561
  return;
477
562
  }
478
563
  const iblAvailable = this.#component3D?.isIBLAvailable?.() === true;
479
- this.#menu3D.setSwitchEnabled?.("iblEnabled", iblAvailable);
564
+ const animationMenuEnabled = this.#component3D?.isAnimationMenuEnabled?.() === true;
565
+ const lightingTimeOfDayEnabled = this.#component3D?.isLightingTimeOfDayAvailable?.() === true;
566
+ this.#menu3D.setElementEnabled?.("iblEnabled", iblAvailable);
567
+ this.#menu3D.setElementVisible?.("showAnimationMenu", false);
568
+ this.#menu3D.setElementVisible?.("highlightEnabled", animationMenuEnabled);
569
+ this.#menu3D.setElementVisible?.("highlightColor", animationMenuEnabled);
570
+ this.#menu3D.setElementVisible?.("showLightingTimeOfDay", false);
571
+ this.#menu3D.setElementVisible?.("lightingTimeOfDay", lightingTimeOfDayEnabled);
480
572
  }
481
573
 
482
574
  /**
483
- * Applies render toggles via PrefViewer3D, showing progress in the menu and resyncing on completion.
484
- * When no changes occur, the menu is simply refreshed to match the controller snapshot.
575
+ * Applies render settings via PrefViewer3D.
576
+ * Reload-capable toggles lock the menu while they run; partial highlight/lighting updates are forwarded immediately.
485
577
  * @private
486
- * @param {object} settings - Partial render settings (AA, SSAO, IBL, shadows) requested by the menu.
578
+ * @param {object} settings - Partial render settings requested by the menu.
487
579
  * @returns {Promise<void>}
488
580
  */
489
581
  async #applyRenderSettings(settings) {
490
582
  if (!this.#component3D) {
491
583
  return;
492
584
  }
493
- const { highlightEnabled: _he, highlightColor: _hc, lightingTimeOfDay: _lt, lighting: _lighting, persist: _persist, ...reloadSettings } = settings ?? {};
585
+ const { showAnimationMenu: _sam, highlightEnabled: _he, highlightColor: _hc, showLightingTimeOfDay: _slt, lightingTimeOfDay: _lt, persist: _persist, ...reloadSettings } = settings ?? {};
494
586
  const hasReloadSettings = Object.keys(reloadSettings).length > 0;
495
587
  if (hasReloadSettings) {
496
588
  this.#menu3D?.setApplying(true);
497
589
  }
498
590
  try {
499
591
  const result = await this.#component3D.applyRenderSettings(settings);
592
+ const controllerSettings = this.#component3D.getRenderSettings();
593
+ if (controllerSettings) {
594
+ this.#menu3D?.setSettings(controllerSettings);
595
+ }
596
+ this.#menu3DUpdateElementAvailability();
500
597
  if (!result?.changed) {
501
- const controllerSettings = this.#component3D.getRenderSettings();
502
- if (controllerSettings) {
503
- this.#menu3D?.setSettings(controllerSettings);
504
- }
505
598
  this.#menu3D?.setApplying(false);
506
599
  }
507
600
  } catch (error) {
@@ -552,10 +645,7 @@ export default class PrefViewer extends HTMLElement {
552
645
  */
553
646
  #addTaskToQueue(value, type) {
554
647
  const task = new PrefViewerTask(value, type);
555
- if (
556
- task.type === PrefViewerTask.Types.Options ||
557
- task.type === PrefViewerTask.Types.Drawing
558
- ) {
648
+ if (task.type === PrefViewerTask.Types.Options || task.type === PrefViewerTask.Types.Drawing || task.type === PrefViewerTask.Types.Styles) {
559
649
  this.#taskQueue = this.#taskQueue.filter((queuedTask) => queuedTask.type !== task.type);
560
650
  }
561
651
  this.#taskQueue.push(task);
@@ -601,6 +691,9 @@ export default class PrefViewer extends HTMLElement {
601
691
  case PrefViewerTask.Types.Options:
602
692
  this.#processOptions(task.value);
603
693
  break;
694
+ case PrefViewerTask.Types.Styles:
695
+ this.#processStyles(task.value);
696
+ break;
604
697
  case PrefViewerTask.Types.Visibility:
605
698
  this.#processVisibility(task.value);
606
699
  break;
@@ -620,24 +713,16 @@ export default class PrefViewer extends HTMLElement {
620
713
  this.removeAttribute("loaded-3d");
621
714
  this.setAttribute("loading-3d", "");
622
715
 
623
- const customEventOptions = {
624
- bubbles: true,
625
- cancelable: false,
626
- composed: true,
627
- };
628
- this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
716
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-loading");
717
+ this.#dispatchPrefViewerEvent("scene-loading"); // Legacy event for backward compatibility
718
+ this.#dispatchPrefViewerEvent("pref-viewer-loading");
629
719
  }
630
720
 
631
721
  /**
632
722
  * Handles the completion of a 3D loading operation.
633
723
  * Updates loading state, sets attributes, dispatches a "scene-loaded" event, and processes the next task.
634
- * Dispatches granular container events ("model-loaded", "environment-loaded", "materials-loaded")
635
- * for each successfully loaded container before firing "scene-loaded".
636
- * The independent model path may additionally emit "model-first-painted" when the first painted
637
- * frame is acknowledged by the Babylon controller.
638
724
  * @private
639
725
  * @param {object} [detail] - Optional details to include in the event.
640
- * @param {string[]} [detail.loadedContainers] - Names of containers that loaded successfully.
641
726
  * @returns {void}
642
727
  */
643
728
  #on3DLoaded(detail) {
@@ -645,42 +730,16 @@ export default class PrefViewer extends HTMLElement {
645
730
  this.#isLoading = false;
646
731
 
647
732
  this.#menu3DSyncSettings();
648
- this.#menu3DUpdateSwitchAvailability();
733
+ this.#menu3DUpdateElementAvailability();
649
734
 
650
735
  this.removeAttribute("loading-3d");
651
736
  this.setAttribute("loaded-3d", "");
652
737
 
653
- // Dispatch granular container events before the aggregate "scene-loaded".
654
- // This lets consumers react as soon as individual assets are ready without
655
- // waiting for the full scene-loaded signal.
656
- const granularEventMap = {
657
- model: "model-loaded",
658
- environment: "environment-loaded",
659
- materials: "materials-loaded",
660
- };
661
- if (Array.isArray(detail?.loadedContainers)) {
662
- const granularEventOptions = {
663
- bubbles: true,
664
- cancelable: false,
665
- composed: true,
666
- };
667
- detail.loadedContainers.forEach((containerName) => {
668
- const eventName = granularEventMap[containerName];
669
- if (eventName) {
670
- this.dispatchEvent(new CustomEvent(eventName, granularEventOptions));
671
- }
672
- });
673
- }
674
-
675
- const customEventOptions = {
676
- bubbles: true,
677
- cancelable: true,
678
- composed: true,
679
- };
680
- if (detail) {
681
- customEventOptions.detail = detail;
682
- }
683
- this.dispatchEvent(new CustomEvent("scene-loaded", customEventOptions));
738
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-loaded", detail ?? {});
739
+ this.#dispatchPrefViewerEvent("scene-loaded", detail ?? {}); // Legacy event for backward compatibility
740
+ this.#dispatchPrefViewerEvent("pref-viewer-loaded", {
741
+ result: detail ?? null,
742
+ });
684
743
  }
685
744
 
686
745
  /**
@@ -698,15 +757,9 @@ export default class PrefViewer extends HTMLElement {
698
757
  this.removeAttribute("loading-3d");
699
758
  this.removeAttribute("loaded-3d");
700
759
 
701
- const customEventOptions = {
702
- bubbles: true,
703
- cancelable: true,
704
- composed: true,
705
- };
706
- if (detail) {
707
- customEventOptions.detail = detail;
708
- }
709
- this.dispatchEvent(new CustomEvent("scene-error", customEventOptions));
760
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-error", detail ?? {});
761
+ this.#dispatchPrefViewerEvent("scene-error", detail ?? {}); // Legacy event for backward compatibility
762
+ this.#dispatchPrefViewerEvent("pref-viewer-error", { error: detail?.error ?? detail ?? null });
710
763
  }
711
764
 
712
765
  /**
@@ -722,12 +775,9 @@ export default class PrefViewer extends HTMLElement {
722
775
  this.removeAttribute("loaded-2d");
723
776
  this.setAttribute("loading-2d", "");
724
777
 
725
- const customEventOptions = {
726
- bubbles: true,
727
- cancelable: true,
728
- composed: true,
729
- };
730
- this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
778
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-loading");
779
+ this.#dispatchPrefViewerEvent("drawing-loading"); // Legacy event for backward compatibility
780
+ this.#dispatchPrefViewerEvent("pref-viewer-loading");
731
781
  }
732
782
 
733
783
  /**
@@ -744,15 +794,9 @@ export default class PrefViewer extends HTMLElement {
744
794
  this.removeAttribute("loading-2d");
745
795
  this.setAttribute("loaded-2d", "");
746
796
 
747
- const customEventOptions = {
748
- bubbles: true,
749
- cancelable: true,
750
- composed: true,
751
- };
752
- if (detail) {
753
- customEventOptions.detail = detail;
754
- }
755
- this.dispatchEvent(new CustomEvent("drawing-loaded", customEventOptions));
797
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-loaded", detail ?? {});
798
+ this.#dispatchPrefViewerEvent("drawing-loaded", detail ?? {}); // Legacy event for backward compatibility
799
+ this.#dispatchPrefViewerEvent("pref-viewer-loaded", { result: detail ?? null });
756
800
  }
757
801
 
758
802
  /**
@@ -769,20 +813,14 @@ export default class PrefViewer extends HTMLElement {
769
813
  this.removeAttribute("loading-2d");
770
814
  this.removeAttribute("loaded-2d");
771
815
 
772
- const customEventOptions = {
773
- bubbles: true,
774
- cancelable: true,
775
- composed: true,
776
- };
777
- if (detail) {
778
- customEventOptions.detail = detail;
779
- }
780
- this.dispatchEvent(new CustomEvent("drawing-error", customEventOptions));
816
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-error", detail ?? {});
817
+ this.#dispatchPrefViewerEvent("drawing-error", detail ?? {}); // Legacy event for backward compatibility
818
+ this.#dispatchPrefViewerEvent("pref-viewer-error", { error: detail?.error ?? detail ?? null });
781
819
  }
782
820
 
783
821
  /**
784
- * Handles the "drawing-zoom-changed" event from the 2D viewer component.
785
- * Dispatches a custom "drawing-zoom-changed" event from the PrefViewer element, forwarding the event detail to external listeners.
822
+ * Handles the "pref-viewer-2d-viewport-changed" event from the 2D viewer component.
823
+ * Dispatches the public 2D viewport event, forwarding the event detail to external listeners.
786
824
  * @private
787
825
  * @param {CustomEvent} event - The original zoom change event from the 2D viewer.
788
826
  * @returns {void}
@@ -790,29 +828,24 @@ export default class PrefViewer extends HTMLElement {
790
828
  #on2DZoomChanged(event) {
791
829
  event.stopPropagation();
792
830
  event.preventDefault();
793
- const customEventOptions = {
794
- bubbles: true,
795
- cancelable: true,
796
- composed: true,
797
- detail: event.detail,
798
- };
799
- this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
831
+ const detail = { ...(event.detail ?? {}) };
832
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-viewport-changed", detail);
800
833
  }
801
834
 
802
835
  /**
803
- * Handles the custom "pref-viewer-menu-3d-apply" event emitted by the menu component.
836
+ * Handles the custom "pref-viewer-3d-render-settings-change-requested" event emitted by the menu component.
804
837
  * Forwards the requested settings to the render-application pipeline.
805
838
  * @private
806
839
  * @param {CustomEvent} event - Menu event containing the `settings` payload.
807
840
  * @returns {void}
808
841
  */
809
- #onMenuApply(event) {
842
+ async #onMenuApply(event) {
810
843
  event.stopPropagation();
811
844
  const settings = event.detail?.settings;
812
845
  if (!settings) {
813
846
  return;
814
847
  }
815
- this.#applyRenderSettings(settings);
848
+ await this.#applyRenderSettings(settings);
816
849
  }
817
850
 
818
851
  /**
@@ -944,6 +977,17 @@ export default class PrefViewer extends HTMLElement {
944
977
  }
945
978
  }
946
979
 
980
+ /**
981
+ * Processes queued style overrides and advances the task queue.
982
+ * @private
983
+ * @param {object} styles - Style overrides to apply.
984
+ * @returns {void}
985
+ */
986
+ #processStyles(styles) {
987
+ this.#applyStyles(styles);
988
+ this.#processNextTask();
989
+ }
990
+
947
991
  /**
948
992
  * Processes visibility configuration for the model and scene.
949
993
  * Shows or hides the model and/or scene based on the config, then processes the next task.
@@ -967,6 +1011,107 @@ export default class PrefViewer extends HTMLElement {
967
1011
  this.#processNextTask();
968
1012
  }
969
1013
 
1014
+ /**
1015
+ * Constructs and dispatches a CustomEvent from the host element.
1016
+ * Strips any incoming `mode` field from the detail to avoid leaking stale values,
1017
+ * then re-attaches `mode` only for generic (non-scoped) event types.
1018
+ * Always appends a `timestamp` (Unix ms) so consumers can correlate events.
1019
+ * @private
1020
+ * @param {string} type - The event type string, e.g. "pref-viewer-loaded".
1021
+ * @param {object} [detail={}] - Arbitrary key/value pairs to include in `event.detail`.
1022
+ * @returns {void}
1023
+ */
1024
+ #dispatchPrefViewerEvent(type, detail = {}) {
1025
+ const eventDetail = { ...(detail ?? {}) };
1026
+ delete eventDetail.mode;
1027
+
1028
+ const hasExplicitModeScope = type.startsWith("pref-viewer-2d-")
1029
+ || type.startsWith("pref-viewer-3d-")
1030
+ || type.startsWith("drawing-")
1031
+ || type.startsWith("scene-");
1032
+
1033
+ if (!hasExplicitModeScope) {
1034
+ eventDetail.mode = this.#mode;
1035
+ }
1036
+ eventDetail.timestamp = Date.now();
1037
+
1038
+ this.dispatchEvent(
1039
+ new CustomEvent(type, {
1040
+ bubbles: true,
1041
+ cancelable: false,
1042
+ composed: true,
1043
+ detail: eventDetail,
1044
+ }),
1045
+ );
1046
+ }
1047
+
1048
+ /**
1049
+ * Normalizes a mixed array of element descriptors into a plain string-id list.
1050
+ * Accepts raw strings, objects with an `id` property, or objects with an `Id` property.
1051
+ * Non-string and empty-string results are filtered out.
1052
+ * @private
1053
+ * @param {Array<string|{id?: string, Id?: string}>} value - Array of id-like items to normalize.
1054
+ * @returns {string[]} Deduplicated, non-empty string ids.
1055
+ */
1056
+ #normalizeIdList(value) {
1057
+ if (!Array.isArray(value)) {
1058
+ return [];
1059
+ }
1060
+ return value.map((item) => (typeof item === "string" ? item : (item?.id ?? item?.Id))).filter((id) => typeof id === "string" && id.length > 0);
1061
+ }
1062
+
1063
+ /**
1064
+ * Normalizes a mixed array of selection descriptors into a canonical list of selection items.
1065
+ * Each output item contains an `id` string and a `highlights` string array.
1066
+ * Accepts raw id strings (expanded to `{ id, highlights: [id] }`), or objects with
1067
+ * `id`/`Id` and optional `highlights`/`Highlights` fields.
1068
+ * Items that cannot be resolved to a non-empty string id are discarded.
1069
+ * @private
1070
+ * @param {Array<string|{id?: string, Id?: string, highlights?: string[], Highlights?: string[]}>} value - Raw selection descriptors.
1071
+ * @returns {Array<{id: string, highlights: string[]}>} Normalized, non-null selection items.
1072
+ */
1073
+ #normalizeSelectionItems(value) {
1074
+ if (!Array.isArray(value)) {
1075
+ return [];
1076
+ }
1077
+ return value
1078
+ .map((item) => {
1079
+ if (typeof item === "string") {
1080
+ return { id: item, highlights: [item] };
1081
+ }
1082
+ const id = item?.id ?? item?.Id;
1083
+ if (typeof id !== "string" || id.length === 0) {
1084
+ return null;
1085
+ }
1086
+ const highlights = item?.highlights ?? item?.Highlights ?? [id];
1087
+ return {
1088
+ id,
1089
+ highlights: this.#normalizeIdList(highlights),
1090
+ };
1091
+ })
1092
+ .filter(Boolean);
1093
+ }
1094
+
1095
+ /**
1096
+ * Dispatches a `pref-viewer-selection-state-changed` event carrying the current selection snapshot.
1097
+ * Called whenever `selectableIds`, `selectedIds`, or `selectedItems` change.
1098
+ * @private
1099
+ * @returns {void}
1100
+ */
1101
+ #emitSelectionStateChanged() {
1102
+ this.#dispatchPrefViewerEvent("pref-viewer-selection-state-changed", this.getSelectionState());
1103
+ }
1104
+
1105
+ /**
1106
+ * Dispatches a `pref-viewer-3d-turnable-changed` event carrying the current turnable snapshot.
1107
+ * Called after `startTurnable`, `stopTurnable`, or `toggleTurnable` mutates the internal state.
1108
+ * @private
1109
+ * @returns {void}
1110
+ */
1111
+ #emitTurnableChanged() {
1112
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-turnable-changed", { state: this.getTurnableState() });
1113
+ }
1114
+
970
1115
  /**
971
1116
  * ---------------------------
972
1117
  * Public methods
@@ -1101,13 +1246,25 @@ export default class PrefViewer extends HTMLElement {
1101
1246
  if (!config) {
1102
1247
  return false;
1103
1248
  }
1104
- this.#addTaskToQueue(config, "config");
1105
- if (config.drawing) {
1106
- this.#addTaskToQueue(config.drawing, "drawing");
1249
+ if (config.styles) {
1250
+ this.#addTaskToQueue(config.styles, "styles");
1107
1251
  }
1108
1252
  if (config.culture) {
1109
1253
  this.#addTaskToQueue(config.culture, "culture");
1110
1254
  }
1255
+ this.#addTaskToQueue(config, "config");
1256
+ if (config.drawing) {
1257
+ this.#addTaskToQueue(config.drawing, "drawing");
1258
+ }
1259
+ }
1260
+
1261
+ /**
1262
+ * Returns a diagnostic snapshot for the active 3D runtime.
1263
+ * @public
1264
+ * @returns {object|null} Debug information, or null when the 3D component is not initialized.
1265
+ */
1266
+ getDebugInfo() {
1267
+ return this.#component3D?.getDebugInfo?.() ?? null;
1111
1268
  }
1112
1269
 
1113
1270
  /**
@@ -1152,78 +1309,6 @@ export default class PrefViewer extends HTMLElement {
1152
1309
  this.#addTaskToQueue(model, "model");
1153
1310
  }
1154
1311
 
1155
- /**
1156
- * Loads the model container in parallel with any ongoing scene load, bypassing the task queue.
1157
- * Dispatches "model-loading", "model-first-painted", and "model-loaded" events directly.
1158
- * @public
1159
- * @param {object|string} model - Model payload with storage pointer.
1160
- * @returns {Promise<void>}
1161
- */
1162
- async loadModelParallel(model) {
1163
- model = typeof model === "string" ? JSON.parse(model) : model;
1164
- if (!model || !this.#component3D) return;
1165
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1166
- this.dispatchEvent(new CustomEvent("model-loading", eventOptions));
1167
- try {
1168
- const detail = await this.#component3D.loadModelIndependent(model);
1169
- if (detail?.success) {
1170
- this.dispatchEvent(new CustomEvent("model-loaded", { ...eventOptions, detail }));
1171
- } else {
1172
- this.dispatchEvent(new CustomEvent("model-error", { ...eventOptions, detail }));
1173
- }
1174
- } catch (error) {
1175
- this.dispatchEvent(new CustomEvent("model-error", { ...eventOptions, detail: { error } }));
1176
- }
1177
- }
1178
-
1179
- /**
1180
- * Loads the environment/scene container in parallel with any ongoing model load, bypassing the
1181
- * task queue. Dispatches "scene-loading" and "scene-loaded" (or "scene-error") events directly.
1182
- * @public
1183
- * @param {object|string} scene - Scene payload: { storage, ibl?, visible? }
1184
- * @returns {Promise<void>}
1185
- */
1186
- async loadSceneParallel(scene) {
1187
- scene = typeof scene === "string" ? JSON.parse(scene) : scene;
1188
- if (!scene || !this.#component3D) return;
1189
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1190
- this.dispatchEvent(new CustomEvent("scene-loading", eventOptions));
1191
- try {
1192
- const detail = await this.#component3D.loadSceneIndependent(scene);
1193
- if (detail?.success) {
1194
- this.dispatchEvent(new CustomEvent("scene-loaded", { ...eventOptions, detail }));
1195
- } else {
1196
- this.dispatchEvent(new CustomEvent("scene-error", { ...eventOptions, detail }));
1197
- }
1198
- } catch (error) {
1199
- this.dispatchEvent(new CustomEvent("scene-error", { ...eventOptions, detail: { error } }));
1200
- }
1201
- }
1202
-
1203
- /**
1204
- * Loads the materials container in parallel with any ongoing model or environment load,
1205
- * bypassing the task queue. Dispatches "materials-loading" and "materials-loaded" events.
1206
- * @public
1207
- * @param {object|string} materials - Materials payload: { storage }
1208
- * @returns {Promise<void>}
1209
- */
1210
- async loadMaterialsParallel(materials) {
1211
- materials = typeof materials === "string" ? JSON.parse(materials) : materials;
1212
- if (!materials || !this.#component3D) return;
1213
- const eventOptions = { bubbles: true, cancelable: false, composed: true };
1214
- this.dispatchEvent(new CustomEvent("materials-loading", eventOptions));
1215
- try {
1216
- const detail = await this.#component3D.loadMaterialsIndependent(materials);
1217
- if (detail?.success) {
1218
- this.dispatchEvent(new CustomEvent("materials-loaded", { ...eventOptions, detail }));
1219
- } else {
1220
- this.dispatchEvent(new CustomEvent("materials-error", { ...eventOptions, detail }));
1221
- }
1222
- } catch (error) {
1223
- this.dispatchEvent(new CustomEvent("materials-error", { ...eventOptions, detail: { error } }));
1224
- }
1225
- }
1226
-
1227
1312
  /**
1228
1313
  * Loads a scene/environment object or JSON string and adds it to the task queue.
1229
1314
  * @public
@@ -1291,6 +1376,191 @@ export default class PrefViewer extends HTMLElement {
1291
1376
  this.#addTaskToQueue(options, "options");
1292
1377
  }
1293
1378
 
1379
+ /**
1380
+ * Sets supported PrefViewer style variables from an object or JSON string and adds them to the task queue.
1381
+ * Currently supports `colorPrimary`, which updates `PrefViewerStyleVariables.colorPrimary`, applies the host CSS custom property, and persists the value in localStorage.
1382
+ * @public
1383
+ * @param {object|string} styles - Style overrides object or JSON string.
1384
+ * @returns {boolean|void} Returns false if styles are invalid; otherwise void.
1385
+ */
1386
+ setStyles(styles) {
1387
+ styles = typeof styles === "string" ? JSON.parse(styles) : styles;
1388
+ if (!styles || typeof styles !== "object") {
1389
+ return false;
1390
+ }
1391
+ this.#addTaskToQueue(styles, "styles");
1392
+ }
1393
+
1394
+ /**
1395
+ * Sets image-based lighting options using the stable contract entry point.
1396
+ * @public
1397
+ * @param {object|string} ibl - IBL options payload.
1398
+ * @returns {boolean|void} Returns false if options are invalid; otherwise void.
1399
+ */
1400
+ setIBL(ibl) {
1401
+ ibl = typeof ibl === "string" ? JSON.parse(ibl) : ibl;
1402
+ if (!ibl) {
1403
+ return false;
1404
+ }
1405
+ return this.setOptions({ ibl });
1406
+ }
1407
+
1408
+ /**
1409
+ * Defines the elements that the host application considers selectable.
1410
+ * This method stores the contract state; visual selection rendering is handled by host-specific follow-up work.
1411
+ * @public
1412
+ * @param {Array<string|{id?: string, Id?: string}>} elements - Element ids or objects containing id/Id.
1413
+ * @returns {object} Current selection state.
1414
+ */
1415
+ setSelectableElements(elements = []) {
1416
+ this.#selectionState.selectableIds = this.#normalizeIdList(elements);
1417
+ this.#emitSelectionStateChanged();
1418
+ return this.getSelectionState();
1419
+ }
1420
+
1421
+ /**
1422
+ * Defines the currently selected elements using the semantic PrefViewer contract.
1423
+ * @public
1424
+ * @param {Array<string|{id?: string, Id?: string, highlights?: string[], Highlights?: string[]}>} elements - Element ids or objects containing id/Id and optional highlights/Highlights.
1425
+ * @returns {object} Current selection state.
1426
+ */
1427
+ setSelectedElements(elements = []) {
1428
+ const selectedItems = this.#normalizeSelectionItems(elements);
1429
+ this.#selectionState.selectedItems = selectedItems;
1430
+ this.#selectionState.selectedIds = selectedItems.map((item) => item.id);
1431
+ this.#emitSelectionStateChanged();
1432
+ return this.getSelectionState();
1433
+ }
1434
+
1435
+ /**
1436
+ * Applies a complete host selection state in one call.
1437
+ * @public
1438
+ * @param {{selectableIds?: string[], selectables?: string[], selectedItems?: object[], selectedIds?: string[], selected?: string[]}} state - Selection state with selectableIds/selectables and selectedItems/selectedIds/selected.
1439
+ * @returns {object} Current selection state.
1440
+ */
1441
+ applySelectionState(state = {}) {
1442
+ this.#selectionState.selectableIds = this.#normalizeIdList(state.selectableIds ?? state.selectables ?? []);
1443
+ this.setSelectedElements(state.selectedItems ?? state.selectedIds ?? state.selected ?? []);
1444
+ return this.getSelectionState();
1445
+ }
1446
+
1447
+ /**
1448
+ * Clears selected elements while keeping the selectable set.
1449
+ * @public
1450
+ * @returns {object} Current selection state.
1451
+ */
1452
+ clearSelectedElements() {
1453
+ this.#selectionState.selectedIds = [];
1454
+ this.#selectionState.selectedItems = [];
1455
+ this.#emitSelectionStateChanged();
1456
+ return this.getSelectionState();
1457
+ }
1458
+
1459
+ /**
1460
+ * Clears selectable elements while keeping the current selected state.
1461
+ * @public
1462
+ * @returns {object} Current selection state.
1463
+ */
1464
+ clearSelectableElements() {
1465
+ this.#selectionState.selectableIds = [];
1466
+ this.#emitSelectionStateChanged();
1467
+ return this.getSelectionState();
1468
+ }
1469
+
1470
+ /**
1471
+ * Clears all semantic selection state.
1472
+ * @public
1473
+ * @returns {object} Current selection state.
1474
+ */
1475
+ clearSelectionState() {
1476
+ this.#selectionState = {
1477
+ selectableIds: [],
1478
+ selectedIds: [],
1479
+ selectedItems: [],
1480
+ };
1481
+ this.#emitSelectionStateChanged();
1482
+ return this.getSelectionState();
1483
+ }
1484
+
1485
+ /**
1486
+ * Returns the current semantic selection state.
1487
+ * @public
1488
+ * @returns {{selectableIds: string[], selectedIds: string[], selectedItems: object[]}}
1489
+ */
1490
+ getSelectionState() {
1491
+ return {
1492
+ selectableIds: [...this.#selectionState.selectableIds],
1493
+ selectedIds: [...this.#selectionState.selectedIds],
1494
+ selectedItems: this.#selectionState.selectedItems.map((item) => ({
1495
+ id: item.id,
1496
+ highlights: [...item.highlights],
1497
+ })),
1498
+ };
1499
+ }
1500
+
1501
+ /**
1502
+ * Contract hook for host-driven element click forwarding.
1503
+ * Internal 2D/3D hit-testing can call this once it exposes stable element ids.
1504
+ * @public
1505
+ * @param {object|string} element - Element id or detail object.
1506
+ * @returns {void}
1507
+ */
1508
+ notifyElementClick(element) {
1509
+ const detail = typeof element === "string" ? { id: element } : { ...(element ?? {}) };
1510
+ this.#dispatchPrefViewerEvent("pref-viewer-element-click", {
1511
+ element: detail,
1512
+ });
1513
+ }
1514
+
1515
+ /**
1516
+ * Starts the semantic turnable state.
1517
+ * @public
1518
+ * @param {{speed?: number, direction?: string}} [options] - Optional speed/direction values.
1519
+ * @returns {object} Current turnable state.
1520
+ */
1521
+ startTurnable(options = {}) {
1522
+ this.#turnableState = {
1523
+ ...this.#turnableState,
1524
+ ...options,
1525
+ running: true,
1526
+ };
1527
+ this.#emitTurnableChanged();
1528
+ return this.getTurnableState();
1529
+ }
1530
+
1531
+ /**
1532
+ * Stops the semantic turnable state.
1533
+ * @public
1534
+ * @returns {object} Current turnable state.
1535
+ */
1536
+ stopTurnable() {
1537
+ this.#turnableState = {
1538
+ ...this.#turnableState,
1539
+ running: false,
1540
+ };
1541
+ this.#emitTurnableChanged();
1542
+ return this.getTurnableState();
1543
+ }
1544
+
1545
+ /**
1546
+ * Toggles the semantic turnable state.
1547
+ * @public
1548
+ * @param {object} [options] - Optional speed/direction values used when starting.
1549
+ * @returns {object} Current turnable state.
1550
+ */
1551
+ toggleTurnable(options = {}) {
1552
+ return this.#turnableState.running ? this.stopTurnable() : this.startTurnable(options);
1553
+ }
1554
+
1555
+ /**
1556
+ * Returns the current semantic turnable state.
1557
+ * @public
1558
+ * @returns {{running: boolean, speed: number, direction: string}}
1559
+ */
1560
+ getTurnableState() {
1561
+ return { ...this.#turnableState };
1562
+ }
1563
+
1294
1564
  /**
1295
1565
  * Opens a modal dialog with the specified title, content, and footer.
1296
1566
  * @public
@@ -1368,31 +1638,6 @@ export default class PrefViewer extends HTMLElement {
1368
1638
  this.#addTaskToQueue(config, "visibility");
1369
1639
  }
1370
1640
 
1371
- /**
1372
- * Reflects the current visibility of a container without feeding the task queue.
1373
- * Used by the Babylon controller to keep host attributes in sync with renderer state.
1374
- * @public
1375
- * @param {string} name - Container name ("model" or "environment").
1376
- * @param {boolean} isVisible - Whether the container should be reflected as visible.
1377
- * @returns {void}
1378
- */
1379
- syncVisibility(name, isVisible) {
1380
- const attrName = name === "model" ? "show-model" : name === "environment" ? "show-scene" : null;
1381
- if (!attrName) {
1382
- return;
1383
- }
1384
- const nextValue = isVisible ? "true" : "false";
1385
- if (this.getAttribute(attrName) === nextValue) {
1386
- return;
1387
- }
1388
- this.#suppressVisibilityAttributeCallback = true;
1389
- try {
1390
- this.setAttribute(attrName, nextValue);
1391
- } finally {
1392
- this.#suppressVisibilityAttributeCallback = false;
1393
- }
1394
- }
1395
-
1396
1641
  /**
1397
1642
  * Centers the 2D drawing view in the viewer.
1398
1643
  * @public
@@ -1561,31 +1806,20 @@ export default class PrefViewer extends HTMLElement {
1561
1806
  }
1562
1807
 
1563
1808
  /**
1564
- * Redirects scene assignments to `loadScene()` for normalized handling.
1809
+ * Sends style updates to `setStyles()` enabling reactive bindings.
1565
1810
  * @public
1566
- * @param {object|string} value - Scene payload or serialized JSON.
1811
+ * @param {object|string} value - Style overrides object or JSON string.
1567
1812
  */
1568
- set scene(value) {
1569
- this.loadScene(value);
1813
+ set styles(value) {
1814
+ this.setStyles(value);
1570
1815
  }
1571
-
1572
- /**
1573
- * Enables or disables the hover highlight effect at runtime.
1574
- * Accepts boolean or string "true"/"false".
1575
- * @public
1576
- * @param {boolean|string} value
1577
- */
1578
- set highlightEnabled(value) {
1579
- this.setAttribute("highlight-enabled", String(value));
1580
- }
1581
-
1582
1816
  /**
1583
- * Sets the hover highlight color at runtime.
1817
+ * Redirects scene assignments to `loadScene()` for normalized handling.
1584
1818
  * @public
1585
- * @param {string} value - Hex color string, e.g. "#ff6700".
1819
+ * @param {object|string} value - Scene payload or serialized JSON.
1586
1820
  */
1587
- set highlightColor(value) {
1588
- this.setAttribute("highlight-color", value);
1821
+ set scene(value) {
1822
+ this.loadScene(value);
1589
1823
  }
1590
1824
 
1591
1825
  /**
@@ -1647,22 +1881,4 @@ export default class PrefViewer extends HTMLElement {
1647
1881
  get isMode3D() {
1648
1882
  return this.#mode === "3d";
1649
1883
  }
1650
-
1651
- /**
1652
- * Forces the 3D camera to frame the current model, when supported.
1653
- * @public
1654
- * @returns {boolean}
1655
- */
1656
- focusModel() {
1657
- return this.#component3D?.focusModel?.() ?? false;
1658
- }
1659
-
1660
- /**
1661
- * Exposes a debug snapshot of the 3D runtime when available.
1662
- * @public
1663
- * @returns {object|null}
1664
- */
1665
- getDebugState() {
1666
- return this.#component3D?.getDebugState?.() ?? null;
1667
- }
1668
1884
  }