@preference-sl/pref-viewer 2.16.0-beta.10 → 2.16.0-beta.12

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 = [];
@@ -67,6 +97,12 @@ export default class PrefViewer extends HTMLElement {
67
97
  #culture = "";
68
98
  #cultureSuppressAttribute = false;
69
99
 
100
+ // Runtime style management
101
+ #STYLES_STORAGE_KEY = PREF_VIEWER_STYLES_STORAGE_KEY;
102
+ #styles = {
103
+ colorPrimary: PrefViewerStyleVariables.colorPrimary,
104
+ };
105
+
70
106
  // References to internal components
71
107
  #wrapper = null;
72
108
  #component2D = null;
@@ -98,6 +134,8 @@ export default class PrefViewer extends HTMLElement {
98
134
  } else {
99
135
  this.#culture = setLocale(this.#cultureInitial);
100
136
  }
137
+
138
+ this.#applyStyles(this.#stylesReadPersisted(), { persist: false, applyCssVariables: false });
101
139
  }
102
140
 
103
141
  /**
@@ -106,13 +144,13 @@ export default class PrefViewer extends HTMLElement {
106
144
  * @returns {string[]} Array of attribute names to observe.
107
145
  */
108
146
  static get observedAttributes() {
109
- 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"];
110
148
  }
111
149
 
112
150
  /**
113
151
  * Observes changes to specific attributes and triggers corresponding actions.
114
152
  * Loads configuration, drawing, model, scene, materials, or options when their attributes change.
115
- * 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.
116
154
  * @public
117
155
  * @param {string} name - The name of the changed attribute.
118
156
  * @param {*} _old - The previous value of the attribute (unused).
@@ -174,16 +212,12 @@ export default class PrefViewer extends HTMLElement {
174
212
  const showScene = value.toLowerCase() === "true";
175
213
  showScene ? this.showScene() : this.hideScene();
176
214
  break;
177
- case "show-menu":
215
+ case "show-render-menu":
178
216
  // Recreate menu to apply show/hide setting
179
217
  this.#createMenu3D();
180
218
  break;
181
- case "accent-color":
182
- case "show-animation-menu":
183
- case "highlight-color":
184
- case "highlight-enabled":
185
- // Pass these attributes to the 3D component and menu
186
- this.#updateComponentAttributes(name, value);
219
+ case "styles":
220
+ this.setStyles(value);
187
221
  break;
188
222
  }
189
223
  }
@@ -195,24 +229,24 @@ export default class PrefViewer extends HTMLElement {
195
229
  * @returns {void|boolean} Returns false if initialization fails; otherwise void.
196
230
  */
197
231
  connectedCallback() {
198
- this.#wrapper = document.createElement("div");
199
- this.#wrapper.classList.add("pref-viewer-wrapper");
200
- this.shadowRoot.append(this.#wrapper);
201
-
202
- const style = document.createElement("style");
203
- style.textContent = PrefViewerStyles;
204
- this.shadowRoot.append(style);
205
-
232
+ this.#createShadowDOM();
233
+ this.#stylesApplyCssVariables();
206
234
  this.#createComponent3D();
207
235
  this.#createComponent2D();
208
236
  this.#createMenu3D();
209
237
 
210
- if (!this.hasAttribute("mode")) {
211
- this.setMode();
212
- }
238
+ this.setMode(this.getAttribute("mode") ?? this.#mode);
213
239
 
214
240
  this.#isInitialized = true;
215
- 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
+ });
216
250
  }
217
251
 
218
252
  /**
@@ -229,21 +263,41 @@ export default class PrefViewer extends HTMLElement {
229
263
  this.#dialog?.remove();
230
264
  }
231
265
  if (this.#component2D) {
232
- this.#component2D.removeEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
266
+ this.#component2D.removeEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
233
267
  this.#component2D.remove();
234
268
  }
235
269
  if (this.#component3D) {
236
- this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
237
- 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);
238
272
  this.#component3D.remove();
239
273
  }
240
274
  if (this.#menu3D) {
241
- 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);
242
276
  this.#menu3D.remove();
243
277
  this.#menu3D = null;
244
278
  }
245
279
  }
246
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
+
247
301
  /**
248
302
  * Creates and appends the 2D viewer component to the shadow DOM.
249
303
  * Sets the "visible" attribute to true by default.
@@ -254,7 +308,7 @@ export default class PrefViewer extends HTMLElement {
254
308
  this.#handlers.on2DZoomChanged = this.#on2DZoomChanged.bind(this);
255
309
  this.#component2D = document.createElement("pref-viewer-2d");
256
310
  this.#component2D.setAttribute("visible", "false");
257
- this.#component2D.addEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
311
+ this.#component2D.addEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
258
312
  this.#wrapper.appendChild(this.#component2D);
259
313
  }
260
314
 
@@ -267,15 +321,7 @@ export default class PrefViewer extends HTMLElement {
267
321
  #createComponent3D() {
268
322
  this.#component3D = document.createElement("pref-viewer-3d");
269
323
  this.#component3D.setAttribute("visible", "false");
270
-
271
- // Pass new configuration attributes to 3D component
272
- const newAttrs = ["show-animation-menu", "highlight-color", "highlight-enabled"];
273
- for (const attr of newAttrs) {
274
- if (this.hasAttribute(attr)) {
275
- this.#component3D.setAttribute(attr, this.getAttribute(attr));
276
- }
277
- }
278
-
324
+
279
325
  this.#handlers.on3DSceneLoaded = () => {
280
326
  this.#menu3DSyncSettings();
281
327
  this.#menu3D?.setApplying(false);
@@ -283,15 +329,15 @@ export default class PrefViewer extends HTMLElement {
283
329
  this.#handlers.on3DSceneError = () => {
284
330
  this.#menu3D?.setApplying(false, true);
285
331
  };
286
- this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
287
- 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);
288
334
  this.#wrapper.appendChild(this.#component3D);
289
335
  }
290
336
 
291
337
  /**
292
338
  * Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
293
339
  * When a menu already exists it is removed so the DOM stays clean.
294
- * 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.
295
341
  * @private
296
342
  * @returns {void}
297
343
  */
@@ -300,7 +346,7 @@ export default class PrefViewer extends HTMLElement {
300
346
  return;
301
347
  }
302
348
  if (this.#menu3D) {
303
- 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);
304
350
  this.#menu3D.remove();
305
351
  this.#menu3D = null;
306
352
  }
@@ -314,13 +360,13 @@ export default class PrefViewer extends HTMLElement {
314
360
  }
315
361
 
316
362
  // Check if menu should be shown (default: true if not specified)
317
- const showMenuAttr = this.getAttribute("show-menu");
318
- const showMenu = showMenuAttr === null || showMenuAttr === "true";
363
+ const show3DMenuAttr = this.getAttribute("show-render-menu");
364
+ const show3DMenu = show3DMenuAttr === null || show3DMenuAttr === "true";
319
365
 
320
- if (!showMenu) {
366
+ if (!show3DMenu) {
321
367
  return;
322
368
  }
323
-
369
+
324
370
  this.#menu3D = document.createElement("pref-viewer-menu-3d");
325
371
  if (typeof this.#menu3D.setCulture === "function") {
326
372
  this.#menu3D.setCulture(this.#culture);
@@ -328,57 +374,22 @@ export default class PrefViewer extends HTMLElement {
328
374
  this.#menu3D.setAttribute("culture", this.#culture);
329
375
  }
330
376
 
331
- // Pass illumination-related attributes to menu
332
- const illumAttrs = ["show-illumination", "illumination-color"];
333
- for (const attr of illumAttrs) {
334
- if (this.hasAttribute(attr)) {
335
- this.#menu3D.setAttribute(attr, this.getAttribute(attr));
336
- }
337
- }
338
-
339
377
  if (!this.#handlers.onMenuApply) {
340
378
  this.#handlers.onMenuApply = this.#onMenuApply.bind(this);
341
379
  }
342
380
  this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
343
381
  this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
344
382
 
345
- 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);
346
384
  this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
347
385
  this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
348
386
 
349
387
  this.#wrapper.appendChild(this.#menu3D);
350
388
  this.#menu3DSyncSettings();
351
- this.#menu3DUpdateSwitchAvailability();
389
+ this.#menu3DUpdateElementAvailability();
352
390
  this.#menu3DUpdateAvailability();
353
391
  }
354
392
 
355
- /**
356
- * Updates attributes on child components (3D viewer and menu) based on parent attributes.
357
- * @private
358
- * @param {string} name - Attribute name.
359
- * @param {*} value - Attribute value.
360
- * @returns {void}
361
- */
362
- #updateComponentAttributes(name, value) {
363
- // Pass to 3D component
364
- if (this.#component3D) {
365
- if (value === null) {
366
- this.#component3D.removeAttribute(name);
367
- } else {
368
- this.#component3D.setAttribute(name, value);
369
- }
370
- }
371
-
372
- // Pass to menu
373
- if (this.#menu3D) {
374
- if (value === null) {
375
- this.#menu3D.removeAttribute(name);
376
- } else {
377
- this.#menu3D.setAttribute(name, value);
378
- }
379
- }
380
- }
381
-
382
393
  /**
383
394
  * Reads the last persisted locale from localStorage so the viewer restores user preference.
384
395
  * @private
@@ -428,6 +439,84 @@ export default class PrefViewer extends HTMLElement {
428
439
  this.#cultureSuppressAttribute = false;
429
440
  }
430
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
+
431
520
  /**
432
521
  * Synchronizes the menu switches with the current render settings reported by PrefViewer3D.
433
522
  * Skips the update when either the menu or the getter API is unavailable.
@@ -467,12 +556,19 @@ export default class PrefViewer extends HTMLElement {
467
556
  * @private
468
557
  * @returns {void}
469
558
  */
470
- #menu3DUpdateSwitchAvailability() {
559
+ #menu3DUpdateElementAvailability() {
471
560
  if (!this.#menu3D) {
472
561
  return;
473
562
  }
474
563
  const iblAvailable = this.#component3D?.isIBLAvailable?.() === true;
475
- 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);
476
572
  }
477
573
 
478
574
  /**
@@ -486,18 +582,19 @@ export default class PrefViewer extends HTMLElement {
486
582
  if (!this.#component3D) {
487
583
  return;
488
584
  }
489
- 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 ?? {};
490
586
  const hasReloadSettings = Object.keys(reloadSettings).length > 0;
491
587
  if (hasReloadSettings) {
492
588
  this.#menu3D?.setApplying(true);
493
589
  }
494
590
  try {
495
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();
496
597
  if (!result?.changed) {
497
- const controllerSettings = this.#component3D.getRenderSettings();
498
- if (controllerSettings) {
499
- this.#menu3D?.setSettings(controllerSettings);
500
- }
501
598
  this.#menu3D?.setApplying(false);
502
599
  }
503
600
  } catch (error) {
@@ -548,10 +645,7 @@ export default class PrefViewer extends HTMLElement {
548
645
  */
549
646
  #addTaskToQueue(value, type) {
550
647
  const task = new PrefViewerTask(value, type);
551
- if (
552
- task.type === PrefViewerTask.Types.Options ||
553
- task.type === PrefViewerTask.Types.Drawing
554
- ) {
648
+ if (task.type === PrefViewerTask.Types.Options || task.type === PrefViewerTask.Types.Drawing || task.type === PrefViewerTask.Types.Styles) {
555
649
  this.#taskQueue = this.#taskQueue.filter((queuedTask) => queuedTask.type !== task.type);
556
650
  }
557
651
  this.#taskQueue.push(task);
@@ -597,6 +691,9 @@ export default class PrefViewer extends HTMLElement {
597
691
  case PrefViewerTask.Types.Options:
598
692
  this.#processOptions(task.value);
599
693
  break;
694
+ case PrefViewerTask.Types.Styles:
695
+ this.#processStyles(task.value);
696
+ break;
600
697
  case PrefViewerTask.Types.Visibility:
601
698
  this.#processVisibility(task.value);
602
699
  break;
@@ -616,12 +713,9 @@ export default class PrefViewer extends HTMLElement {
616
713
  this.removeAttribute("loaded-3d");
617
714
  this.setAttribute("loading-3d", "");
618
715
 
619
- const customEventOptions = {
620
- bubbles: true,
621
- cancelable: false,
622
- composed: true,
623
- };
624
- 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");
625
719
  }
626
720
 
627
721
  /**
@@ -634,22 +728,18 @@ export default class PrefViewer extends HTMLElement {
634
728
  #on3DLoaded(detail) {
635
729
  this.#isLoaded = true;
636
730
  this.#isLoading = false;
637
-
731
+
638
732
  this.#menu3DSyncSettings();
639
- this.#menu3DUpdateSwitchAvailability();
733
+ this.#menu3DUpdateElementAvailability();
640
734
 
641
735
  this.removeAttribute("loading-3d");
642
736
  this.setAttribute("loaded-3d", "");
643
737
 
644
- const customEventOptions = {
645
- bubbles: true,
646
- cancelable: true,
647
- composed: true,
648
- };
649
- if (detail) {
650
- customEventOptions.detail = detail;
651
- }
652
- 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
+ });
653
743
  }
654
744
 
655
745
  /**
@@ -667,15 +757,9 @@ export default class PrefViewer extends HTMLElement {
667
757
  this.removeAttribute("loading-3d");
668
758
  this.removeAttribute("loaded-3d");
669
759
 
670
- const customEventOptions = {
671
- bubbles: true,
672
- cancelable: true,
673
- composed: true,
674
- };
675
- if (detail) {
676
- customEventOptions.detail = detail;
677
- }
678
- 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 });
679
763
  }
680
764
 
681
765
  /**
@@ -691,12 +775,9 @@ export default class PrefViewer extends HTMLElement {
691
775
  this.removeAttribute("loaded-2d");
692
776
  this.setAttribute("loading-2d", "");
693
777
 
694
- const customEventOptions = {
695
- bubbles: true,
696
- cancelable: true,
697
- composed: true,
698
- };
699
- 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");
700
781
  }
701
782
 
702
783
  /**
@@ -713,15 +794,9 @@ export default class PrefViewer extends HTMLElement {
713
794
  this.removeAttribute("loading-2d");
714
795
  this.setAttribute("loaded-2d", "");
715
796
 
716
- const customEventOptions = {
717
- bubbles: true,
718
- cancelable: true,
719
- composed: true,
720
- };
721
- if (detail) {
722
- customEventOptions.detail = detail;
723
- }
724
- 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 });
725
800
  }
726
801
 
727
802
  /**
@@ -738,20 +813,14 @@ export default class PrefViewer extends HTMLElement {
738
813
  this.removeAttribute("loading-2d");
739
814
  this.removeAttribute("loaded-2d");
740
815
 
741
- const customEventOptions = {
742
- bubbles: true,
743
- cancelable: true,
744
- composed: true,
745
- };
746
- if (detail) {
747
- customEventOptions.detail = detail;
748
- }
749
- 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 });
750
819
  }
751
820
 
752
821
  /**
753
- * Handles the "drawing-zoom-changed" event from the 2D viewer component.
754
- * 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.
755
824
  * @private
756
825
  * @param {CustomEvent} event - The original zoom change event from the 2D viewer.
757
826
  * @returns {void}
@@ -759,29 +828,24 @@ export default class PrefViewer extends HTMLElement {
759
828
  #on2DZoomChanged(event) {
760
829
  event.stopPropagation();
761
830
  event.preventDefault();
762
- const customEventOptions = {
763
- bubbles: true,
764
- cancelable: true,
765
- composed: true,
766
- detail: event.detail,
767
- };
768
- this.dispatchEvent(new CustomEvent("drawing-zoom-changed", customEventOptions));
831
+ const detail = { ...(event.detail ?? {}) };
832
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-viewport-changed", detail);
769
833
  }
770
834
 
771
835
  /**
772
- * 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.
773
837
  * Forwards the requested settings to the render-application pipeline.
774
838
  * @private
775
839
  * @param {CustomEvent} event - Menu event containing the `settings` payload.
776
840
  * @returns {void}
777
841
  */
778
- #onMenuApply(event) {
842
+ async #onMenuApply(event) {
779
843
  event.stopPropagation();
780
844
  const settings = event.detail?.settings;
781
845
  if (!settings) {
782
846
  return;
783
847
  }
784
- this.#applyRenderSettings(settings);
848
+ await this.#applyRenderSettings(settings);
785
849
  }
786
850
 
787
851
  /**
@@ -913,6 +977,17 @@ export default class PrefViewer extends HTMLElement {
913
977
  }
914
978
  }
915
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
+
916
991
  /**
917
992
  * Processes visibility configuration for the model and scene.
918
993
  * Shows or hides the model and/or scene based on the config, then processes the next task.
@@ -936,6 +1011,107 @@ export default class PrefViewer extends HTMLElement {
936
1011
  this.#processNextTask();
937
1012
  }
938
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
+
939
1115
  /**
940
1116
  * ---------------------------
941
1117
  * Public methods
@@ -1070,13 +1246,25 @@ export default class PrefViewer extends HTMLElement {
1070
1246
  if (!config) {
1071
1247
  return false;
1072
1248
  }
1073
- this.#addTaskToQueue(config, "config");
1074
- if (config.drawing) {
1075
- this.#addTaskToQueue(config.drawing, "drawing");
1249
+ if (config.styles) {
1250
+ this.#addTaskToQueue(config.styles, "styles");
1076
1251
  }
1077
1252
  if (config.culture) {
1078
1253
  this.#addTaskToQueue(config.culture, "culture");
1079
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;
1080
1268
  }
1081
1269
 
1082
1270
  /**
@@ -1188,6 +1376,191 @@ export default class PrefViewer extends HTMLElement {
1188
1376
  this.#addTaskToQueue(options, "options");
1189
1377
  }
1190
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
+
1191
1564
  /**
1192
1565
  * Opens a modal dialog with the specified title, content, and footer.
1193
1566
  * @public
@@ -1433,31 +1806,20 @@ export default class PrefViewer extends HTMLElement {
1433
1806
  }
1434
1807
 
1435
1808
  /**
1436
- * Redirects scene assignments to `loadScene()` for normalized handling.
1437
- * @public
1438
- * @param {object|string} value - Scene payload or serialized JSON.
1439
- */
1440
- set scene(value) {
1441
- this.loadScene(value);
1442
- }
1443
-
1444
- /**
1445
- * Enables or disables the hover highlight effect at runtime.
1446
- * Accepts boolean or string "true"/"false".
1809
+ * Sends style updates to `setStyles()` enabling reactive bindings.
1447
1810
  * @public
1448
- * @param {boolean|string} value
1811
+ * @param {object|string} value - Style overrides object or JSON string.
1449
1812
  */
1450
- set highlightEnabled(value) {
1451
- this.setAttribute("highlight-enabled", String(value));
1813
+ set styles(value) {
1814
+ this.setStyles(value);
1452
1815
  }
1453
-
1454
1816
  /**
1455
- * Sets the hover highlight color at runtime.
1817
+ * Redirects scene assignments to `loadScene()` for normalized handling.
1456
1818
  * @public
1457
- * @param {string} value - Hex color string, e.g. "#ff6700".
1819
+ * @param {object|string} value - Scene payload or serialized JSON.
1458
1820
  */
1459
- set highlightColor(value) {
1460
- this.setAttribute("highlight-color", value);
1821
+ set scene(value) {
1822
+ this.loadScene(value);
1461
1823
  }
1462
1824
 
1463
1825
  /**