@preference-sl/pref-viewer 2.16.0 → 2.16.2-beta.0

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,50 @@ 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
+ * - appendSvgNode / removeAppendedSvgNodes: Manage host-provided SVG nodes through the root facade.
49
+ * - startTurnable / stopTurnable / toggleTurnable / getTurnableState: Control and inspect the auto-rotate state.
50
+ * - getAnimations / playAnimation / setAnimationProgress / setAnimationLoop: Inspect and control named glTF animations.
51
+ * - getDebugInfo: Return a diagnostic snapshot of the active 3D runtime.
40
52
  *
41
53
  * Public Properties:
42
- * - culture, isInitialized, isLoaded, isLoading, isMode2D, isMode3D expose runtime state for host inspection.
54
+ * - Getters: culture, isInitialized, isLoaded, isLoading, isMode2D, isMode3D expose runtime state for host inspection.
55
+ * - Setters: config, culture, drawing, materials, mode, model, options, scene, and styles mirror the corresponding load/set methods for reactive bindings.
43
56
  *
44
57
  * Events:
45
- * - `scene-loading` / `scene-loaded` announce Babylon work, `drawing-loading` / `drawing-loaded` cover SVG pipelines, `drawing-zoom-changed` proxies pan/zoom delta.
58
+ * - `pref-viewer-initialized`: emitted after the host and internal components are mounted and public methods/properties are available.
59
+ * - `pref-viewer-loading` / `pref-viewer-loaded` / `pref-viewer-error`: generic lifecycle events that fire for both 2D and 3D pipelines.
60
+ * - `pref-viewer-3d-loading` / `pref-viewer-3d-loaded` / `pref-viewer-3d-error`: announce Babylon.js work start, completion, and failure.
61
+ * - `pref-viewer-2d-loading` / `pref-viewer-2d-loaded` / `pref-viewer-2d-error`: announce SVG pipeline start, completion, and failure.
62
+ * - `pref-viewer-2d-viewport-changed`: proxies pan/zoom delta from the 2D controller.
63
+ * - `pref-viewer-selection-state-changed`: emitted whenever the selectable or selected element sets change.
64
+ * - `pref-viewer-3d-turnable-changed`: emitted after the auto-rotate state starts, stops, or is toggled.
65
+ * - `pref-viewer-element-click`: emitted with `{ ids: string[] }` for 2D or host-forwarded semantic clicks.
66
+ * - Legacy `scene-*` and `drawing-*` aliases are still emitted alongside their modern equivalents for backward compatibility.
46
67
  *
47
68
  * Notes:
48
69
  * - Internally wires PrefViewerMenu3D hover/apply events to PrefViewer3D render settings, ensuring persisted toggles stay in sync with BabylonJSController.
@@ -56,6 +77,16 @@ export default class PrefViewer extends HTMLElement {
56
77
  #isLoaded = false;
57
78
  #isLoading = false;
58
79
  #mode = "3d";
80
+ #selectionState = {
81
+ selectableIds: [],
82
+ selectedIds: [],
83
+ selectedItems: [],
84
+ };
85
+ #turnableState = {
86
+ running: false,
87
+ speed: 1,
88
+ direction: "clockwise",
89
+ };
59
90
 
60
91
  // Task queue for sequential processing
61
92
  #taskQueue = [];
@@ -67,6 +98,12 @@ export default class PrefViewer extends HTMLElement {
67
98
  #culture = "";
68
99
  #cultureSuppressAttribute = false;
69
100
 
101
+ // Runtime style management
102
+ #STYLES_STORAGE_KEY = PREF_VIEWER_STYLES_STORAGE_KEY;
103
+ #styles = {
104
+ colorPrimary: PrefViewerStyleVariables.colorPrimary,
105
+ };
106
+
70
107
  // References to internal components
71
108
  #wrapper = null;
72
109
  #component2D = null;
@@ -77,6 +114,7 @@ export default class PrefViewer extends HTMLElement {
77
114
  // Event handlers
78
115
  #handlers = {
79
116
  on2DZoomChanged: null,
117
+ on2DElementClick: null,
80
118
  onMenuApply: null,
81
119
  onViewerHoverStart: null,
82
120
  onViewerHoverEnd: null,
@@ -98,6 +136,8 @@ export default class PrefViewer extends HTMLElement {
98
136
  } else {
99
137
  this.#culture = setLocale(this.#cultureInitial);
100
138
  }
139
+
140
+ this.#applyStyles(this.#stylesReadPersisted(), { persist: false, applyCssVariables: false });
101
141
  }
102
142
 
103
143
  /**
@@ -106,13 +146,13 @@ export default class PrefViewer extends HTMLElement {
106
146
  * @returns {string[]} Array of attribute names to observe.
107
147
  */
108
148
  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"];
149
+ return ["config", "culture", "drawing", "materials", "mode", "model", "scene", "options", "show-model", "show-scene", "show-render-menu", "styles"];
110
150
  }
111
151
 
112
152
  /**
113
153
  * Observes changes to specific attributes and triggers corresponding actions.
114
154
  * 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.
155
+ * 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
156
  * @public
117
157
  * @param {string} name - The name of the changed attribute.
118
158
  * @param {*} _old - The previous value of the attribute (unused).
@@ -174,16 +214,12 @@ export default class PrefViewer extends HTMLElement {
174
214
  const showScene = value.toLowerCase() === "true";
175
215
  showScene ? this.showScene() : this.hideScene();
176
216
  break;
177
- case "show-menu":
217
+ case "show-render-menu":
178
218
  // Recreate menu to apply show/hide setting
179
219
  this.#createMenu3D();
180
220
  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);
221
+ case "styles":
222
+ this.setStyles(value);
187
223
  break;
188
224
  }
189
225
  }
@@ -195,24 +231,24 @@ export default class PrefViewer extends HTMLElement {
195
231
  * @returns {void|boolean} Returns false if initialization fails; otherwise void.
196
232
  */
197
233
  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
-
234
+ this.#createShadowDOM();
235
+ this.#stylesApplyCssVariables();
206
236
  this.#createComponent3D();
207
237
  this.#createComponent2D();
208
238
  this.#createMenu3D();
209
239
 
210
- if (!this.hasAttribute("mode")) {
211
- this.setMode();
212
- }
240
+ this.setMode(this.getAttribute("mode") ?? this.#mode);
213
241
 
214
242
  this.#isInitialized = true;
215
- this.#processNextTask();
243
+ Promise.resolve().then(() => {
244
+ if (!this.isConnected || !this.#isInitialized) {
245
+ return;
246
+ }
247
+ this.#dispatchPrefViewerEvent("pref-viewer-initialized", {
248
+ isInitialized: this.#isInitialized,
249
+ });
250
+ this.#processNextTask();
251
+ });
216
252
  }
217
253
 
218
254
  /**
@@ -229,21 +265,42 @@ export default class PrefViewer extends HTMLElement {
229
265
  this.#dialog?.remove();
230
266
  }
231
267
  if (this.#component2D) {
232
- this.#component2D.removeEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
268
+ this.#component2D.removeEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
269
+ this.#component2D.removeEventListener("pref-viewer-2d-element-click", this.#handlers.on2DElementClick);
233
270
  this.#component2D.remove();
234
271
  }
235
272
  if (this.#component3D) {
236
- this.#component3D.removeEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
237
- this.#component3D.removeEventListener("scene-error", this.#handlers.on3DSceneError);
273
+ this.#component3D.removeEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
274
+ this.#component3D.removeEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
238
275
  this.#component3D.remove();
239
276
  }
240
277
  if (this.#menu3D) {
241
- this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
278
+ this.#menu3D.removeEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
242
279
  this.#menu3D.remove();
243
280
  this.#menu3D = null;
244
281
  }
245
282
  }
246
283
 
284
+ /**
285
+ * Creates the root shadow DOM structure and host styles.
286
+ * @private
287
+ * @returns {void}
288
+ */
289
+ #createShadowDOM() {
290
+ this.#wrapper = document.createElement("div");
291
+ this.#wrapper.classList.add("pref-viewer-wrapper");
292
+ this.shadowRoot.append(this.#wrapper);
293
+
294
+ const link = document.createElement("link");
295
+ link.setAttribute("rel", "stylesheet");
296
+ link.setAttribute("href", "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
297
+ this.shadowRoot.append(link);
298
+
299
+ const style = document.createElement("style");
300
+ style.textContent = PrefViewerStyles;
301
+ this.shadowRoot.append(style);
302
+ }
303
+
247
304
  /**
248
305
  * Creates and appends the 2D viewer component to the shadow DOM.
249
306
  * Sets the "visible" attribute to true by default.
@@ -252,9 +309,11 @@ export default class PrefViewer extends HTMLElement {
252
309
  */
253
310
  #createComponent2D() {
254
311
  this.#handlers.on2DZoomChanged = this.#on2DZoomChanged.bind(this);
312
+ this.#handlers.on2DElementClick = this.#on2DElementClick.bind(this);
255
313
  this.#component2D = document.createElement("pref-viewer-2d");
256
314
  this.#component2D.setAttribute("visible", "false");
257
- this.#component2D.addEventListener("drawing-zoom-changed", this.#handlers.on2DZoomChanged);
315
+ this.#component2D.addEventListener("pref-viewer-2d-viewport-changed", this.#handlers.on2DZoomChanged);
316
+ this.#component2D.addEventListener("pref-viewer-2d-element-click", this.#handlers.on2DElementClick);
258
317
  this.#wrapper.appendChild(this.#component2D);
259
318
  }
260
319
 
@@ -267,15 +326,7 @@ export default class PrefViewer extends HTMLElement {
267
326
  #createComponent3D() {
268
327
  this.#component3D = document.createElement("pref-viewer-3d");
269
328
  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
-
329
+
279
330
  this.#handlers.on3DSceneLoaded = () => {
280
331
  this.#menu3DSyncSettings();
281
332
  this.#menu3D?.setApplying(false);
@@ -283,15 +334,15 @@ export default class PrefViewer extends HTMLElement {
283
334
  this.#handlers.on3DSceneError = () => {
284
335
  this.#menu3D?.setApplying(false, true);
285
336
  };
286
- this.#component3D.addEventListener("scene-loaded", this.#handlers.on3DSceneLoaded);
287
- this.#component3D.addEventListener("scene-error", this.#handlers.on3DSceneError);
337
+ this.#component3D.addEventListener("pref-viewer-3d-loaded", this.#handlers.on3DSceneLoaded);
338
+ this.#component3D.addEventListener("pref-viewer-3d-error", this.#handlers.on3DSceneError);
288
339
  this.#wrapper.appendChild(this.#component3D);
289
340
  }
290
341
 
291
342
  /**
292
343
  * Creates (or recreates) the PrefViewerMenu3D element, wires hover/apply handlers, and syncs it with the 3D component.
293
344
  * 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.
345
+ * Respects the "show-render-menu" attribute to determine if the menu should be visible.
295
346
  * @private
296
347
  * @returns {void}
297
348
  */
@@ -300,7 +351,7 @@ export default class PrefViewer extends HTMLElement {
300
351
  return;
301
352
  }
302
353
  if (this.#menu3D) {
303
- this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
354
+ this.#menu3D.removeEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
304
355
  this.#menu3D.remove();
305
356
  this.#menu3D = null;
306
357
  }
@@ -314,13 +365,13 @@ export default class PrefViewer extends HTMLElement {
314
365
  }
315
366
 
316
367
  // 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";
368
+ const show3DMenuAttr = this.getAttribute("show-render-menu");
369
+ const show3DMenu = show3DMenuAttr === null || show3DMenuAttr === "true";
319
370
 
320
- if (!showMenu) {
371
+ if (!show3DMenu) {
321
372
  return;
322
373
  }
323
-
374
+
324
375
  this.#menu3D = document.createElement("pref-viewer-menu-3d");
325
376
  if (typeof this.#menu3D.setCulture === "function") {
326
377
  this.#menu3D.setCulture(this.#culture);
@@ -328,57 +379,22 @@ export default class PrefViewer extends HTMLElement {
328
379
  this.#menu3D.setAttribute("culture", this.#culture);
329
380
  }
330
381
 
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
382
  if (!this.#handlers.onMenuApply) {
340
383
  this.#handlers.onMenuApply = this.#onMenuApply.bind(this);
341
384
  }
342
385
  this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
343
386
  this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
344
387
 
345
- this.#menu3D.addEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
388
+ this.#menu3D.addEventListener("pref-viewer-3d-render-settings-change-requested", this.#handlers.onMenuApply);
346
389
  this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
347
390
  this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
348
391
 
349
392
  this.#wrapper.appendChild(this.#menu3D);
350
393
  this.#menu3DSyncSettings();
351
- this.#menu3DUpdateSwitchAvailability();
394
+ this.#menu3DUpdateElementAvailability();
352
395
  this.#menu3DUpdateAvailability();
353
396
  }
354
397
 
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
398
  /**
383
399
  * Reads the last persisted locale from localStorage so the viewer restores user preference.
384
400
  * @private
@@ -428,6 +444,84 @@ export default class PrefViewer extends HTMLElement {
428
444
  this.#cultureSuppressAttribute = false;
429
445
  }
430
446
 
447
+ /**
448
+ * Reads persisted style overrides from localStorage.
449
+ * @private
450
+ * @returns {object} Persisted style overrides, or an empty object when unavailable.
451
+ */
452
+ #stylesReadPersisted() {
453
+ if (typeof window === "undefined") {
454
+ return {};
455
+ }
456
+ try {
457
+ const serialized = window.localStorage?.getItem(this.#STYLES_STORAGE_KEY);
458
+ return serialized ? JSON.parse(serialized) : {};
459
+ } catch (_error) {
460
+ return {};
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Persists current style overrides in localStorage.
466
+ * @private
467
+ * @returns {void}
468
+ */
469
+ #stylesPersist() {
470
+ if (typeof window === "undefined") {
471
+ return;
472
+ }
473
+ try {
474
+ window.localStorage?.setItem(this.#STYLES_STORAGE_KEY, JSON.stringify(this.#styles));
475
+ } catch (_error) {}
476
+ }
477
+
478
+ /**
479
+ * Keeps the host CSS custom properties in sync with the style variables.
480
+ * @private
481
+ * @returns {void}
482
+ */
483
+ #stylesApplyCssVariables() {
484
+ this.style.setProperty("--pref-viewer-color-primary", this.#styles.colorPrimary);
485
+ }
486
+
487
+ /**
488
+ * Applies supported style overrides to PrefViewerStyleVariables and the host CSS variables.
489
+ * @private
490
+ * @param {object} styles - Style overrides.
491
+ * @param {object} [options] - Apply options.
492
+ * @param {boolean} [options.persist=true] - Whether to persist the resolved styles.
493
+ * @param {boolean} [options.applyCssVariables=true] - Whether to update host CSS custom properties immediately.
494
+ * @returns {boolean} True when at least one supported style was applied.
495
+ */
496
+ #applyStyles(styles, options = {}) {
497
+ const { persist = true, applyCssVariables = true } = options;
498
+ if (!styles || typeof styles !== "object") {
499
+ return false;
500
+ }
501
+
502
+ const nextStyles = {};
503
+ if (typeof styles.colorPrimary === "string" && styles.colorPrimary.trim().length > 0) {
504
+ nextStyles.colorPrimary = styles.colorPrimary.trim();
505
+ }
506
+
507
+ if (!Object.keys(nextStyles).length) {
508
+ return false;
509
+ }
510
+
511
+ this.#styles = {
512
+ ...this.#styles,
513
+ ...nextStyles,
514
+ };
515
+ PrefViewerStyleVariables.colorPrimary = this.#styles.colorPrimary;
516
+ if (applyCssVariables) {
517
+ this.#stylesApplyCssVariables();
518
+ }
519
+ if (persist) {
520
+ this.#stylesPersist();
521
+ }
522
+ return true;
523
+ }
524
+
431
525
  /**
432
526
  * Synchronizes the menu switches with the current render settings reported by PrefViewer3D.
433
527
  * Skips the update when either the menu or the getter API is unavailable.
@@ -467,12 +561,19 @@ export default class PrefViewer extends HTMLElement {
467
561
  * @private
468
562
  * @returns {void}
469
563
  */
470
- #menu3DUpdateSwitchAvailability() {
564
+ #menu3DUpdateElementAvailability() {
471
565
  if (!this.#menu3D) {
472
566
  return;
473
567
  }
474
568
  const iblAvailable = this.#component3D?.isIBLAvailable?.() === true;
475
- this.#menu3D.setSwitchEnabled?.("iblEnabled", iblAvailable);
569
+ const animationMenuEnabled = this.#component3D?.isAnimationMenuEnabled?.() === true;
570
+ const lightingTimeOfDayEnabled = this.#component3D?.isLightingTimeOfDayAvailable?.() === true;
571
+ this.#menu3D.setElementEnabled?.("iblEnabled", iblAvailable);
572
+ this.#menu3D.setElementVisible?.("showAnimationMenu", false);
573
+ this.#menu3D.setElementVisible?.("highlightEnabled", animationMenuEnabled);
574
+ this.#menu3D.setElementVisible?.("highlightColor", animationMenuEnabled);
575
+ this.#menu3D.setElementVisible?.("showLightingTimeOfDay", false);
576
+ this.#menu3D.setElementVisible?.("lightingTimeOfDay", lightingTimeOfDayEnabled);
476
577
  }
477
578
 
478
579
  /**
@@ -486,18 +587,19 @@ export default class PrefViewer extends HTMLElement {
486
587
  if (!this.#component3D) {
487
588
  return;
488
589
  }
489
- const { highlightEnabled: _he, highlightColor: _hc, lightingTimeOfDay: _lt, lighting: _lighting, persist: _persist, ...reloadSettings } = settings ?? {};
590
+ const { showAnimationMenu: _sam, highlightEnabled: _he, highlightColor: _hc, showLightingTimeOfDay: _slt, lightingTimeOfDay: _lt, persist: _persist, ...reloadSettings } = settings ?? {};
490
591
  const hasReloadSettings = Object.keys(reloadSettings).length > 0;
491
592
  if (hasReloadSettings) {
492
593
  this.#menu3D?.setApplying(true);
493
594
  }
494
595
  try {
495
596
  const result = await this.#component3D.applyRenderSettings(settings);
597
+ const controllerSettings = this.#component3D.getRenderSettings();
598
+ if (controllerSettings) {
599
+ this.#menu3D?.setSettings(controllerSettings);
600
+ }
601
+ this.#menu3DUpdateElementAvailability();
496
602
  if (!result?.changed) {
497
- const controllerSettings = this.#component3D.getRenderSettings();
498
- if (controllerSettings) {
499
- this.#menu3D?.setSettings(controllerSettings);
500
- }
501
603
  this.#menu3D?.setApplying(false);
502
604
  }
503
605
  } catch (error) {
@@ -548,10 +650,7 @@ export default class PrefViewer extends HTMLElement {
548
650
  */
549
651
  #addTaskToQueue(value, type) {
550
652
  const task = new PrefViewerTask(value, type);
551
- if (
552
- task.type === PrefViewerTask.Types.Options ||
553
- task.type === PrefViewerTask.Types.Drawing
554
- ) {
653
+ if (task.type === PrefViewerTask.Types.Options || task.type === PrefViewerTask.Types.Drawing || task.type === PrefViewerTask.Types.Styles) {
555
654
  this.#taskQueue = this.#taskQueue.filter((queuedTask) => queuedTask.type !== task.type);
556
655
  }
557
656
  this.#taskQueue.push(task);
@@ -597,6 +696,9 @@ export default class PrefViewer extends HTMLElement {
597
696
  case PrefViewerTask.Types.Options:
598
697
  this.#processOptions(task.value);
599
698
  break;
699
+ case PrefViewerTask.Types.Styles:
700
+ this.#processStyles(task.value);
701
+ break;
600
702
  case PrefViewerTask.Types.Visibility:
601
703
  this.#processVisibility(task.value);
602
704
  break;
@@ -616,12 +718,9 @@ export default class PrefViewer extends HTMLElement {
616
718
  this.removeAttribute("loaded-3d");
617
719
  this.setAttribute("loading-3d", "");
618
720
 
619
- const customEventOptions = {
620
- bubbles: true,
621
- cancelable: false,
622
- composed: true,
623
- };
624
- this.dispatchEvent(new CustomEvent("scene-loading", customEventOptions));
721
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-loading");
722
+ this.#dispatchPrefViewerEvent("scene-loading"); // Legacy event for backward compatibility
723
+ this.#dispatchPrefViewerEvent("pref-viewer-loading");
625
724
  }
626
725
 
627
726
  /**
@@ -634,22 +733,18 @@ export default class PrefViewer extends HTMLElement {
634
733
  #on3DLoaded(detail) {
635
734
  this.#isLoaded = true;
636
735
  this.#isLoading = false;
637
-
736
+
638
737
  this.#menu3DSyncSettings();
639
- this.#menu3DUpdateSwitchAvailability();
738
+ this.#menu3DUpdateElementAvailability();
640
739
 
641
740
  this.removeAttribute("loading-3d");
642
741
  this.setAttribute("loaded-3d", "");
643
742
 
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));
743
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-loaded", detail ?? {});
744
+ this.#dispatchPrefViewerEvent("scene-loaded", detail ?? {}); // Legacy event for backward compatibility
745
+ this.#dispatchPrefViewerEvent("pref-viewer-loaded", {
746
+ result: detail ?? null,
747
+ });
653
748
  }
654
749
 
655
750
  /**
@@ -667,15 +762,9 @@ export default class PrefViewer extends HTMLElement {
667
762
  this.removeAttribute("loading-3d");
668
763
  this.removeAttribute("loaded-3d");
669
764
 
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));
765
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-error", detail ?? {});
766
+ this.#dispatchPrefViewerEvent("scene-error", detail ?? {}); // Legacy event for backward compatibility
767
+ this.#dispatchPrefViewerEvent("pref-viewer-error", { error: detail?.error ?? detail ?? null });
679
768
  }
680
769
 
681
770
  /**
@@ -691,12 +780,9 @@ export default class PrefViewer extends HTMLElement {
691
780
  this.removeAttribute("loaded-2d");
692
781
  this.setAttribute("loading-2d", "");
693
782
 
694
- const customEventOptions = {
695
- bubbles: true,
696
- cancelable: true,
697
- composed: true,
698
- };
699
- this.dispatchEvent(new CustomEvent("drawing-loading", customEventOptions));
783
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-loading");
784
+ this.#dispatchPrefViewerEvent("drawing-loading"); // Legacy event for backward compatibility
785
+ this.#dispatchPrefViewerEvent("pref-viewer-loading");
700
786
  }
701
787
 
702
788
  /**
@@ -713,15 +799,11 @@ export default class PrefViewer extends HTMLElement {
713
799
  this.removeAttribute("loading-2d");
714
800
  this.setAttribute("loaded-2d", "");
715
801
 
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));
802
+ this.#applySelectionStateTo2D();
803
+
804
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-loaded", detail ?? {});
805
+ this.#dispatchPrefViewerEvent("drawing-loaded", detail ?? {}); // Legacy event for backward compatibility
806
+ this.#dispatchPrefViewerEvent("pref-viewer-loaded", { result: detail ?? null });
725
807
  }
726
808
 
727
809
  /**
@@ -738,20 +820,14 @@ export default class PrefViewer extends HTMLElement {
738
820
  this.removeAttribute("loading-2d");
739
821
  this.removeAttribute("loaded-2d");
740
822
 
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));
823
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-error", detail ?? {});
824
+ this.#dispatchPrefViewerEvent("drawing-error", detail ?? {}); // Legacy event for backward compatibility
825
+ this.#dispatchPrefViewerEvent("pref-viewer-error", { error: detail?.error ?? detail ?? null });
750
826
  }
751
827
 
752
828
  /**
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.
829
+ * Handles the "pref-viewer-2d-viewport-changed" event from the 2D viewer component.
830
+ * Dispatches the public 2D viewport event, forwarding the event detail to external listeners.
755
831
  * @private
756
832
  * @param {CustomEvent} event - The original zoom change event from the 2D viewer.
757
833
  * @returns {void}
@@ -759,29 +835,35 @@ export default class PrefViewer extends HTMLElement {
759
835
  #on2DZoomChanged(event) {
760
836
  event.stopPropagation();
761
837
  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));
838
+ const detail = { ...(event.detail ?? {}) };
839
+ this.#dispatchPrefViewerEvent("pref-viewer-2d-viewport-changed", detail);
840
+ }
841
+
842
+ /**
843
+ * Proxy semantic click ids from the encapsulated 2D component.
844
+ * @private
845
+ * @param {CustomEvent} event - Internal component event.
846
+ * @returns {void}
847
+ */
848
+ #on2DElementClick(event) {
849
+ event.stopPropagation();
850
+ this.notifyElementClick(event.detail?.ids ?? []);
769
851
  }
770
852
 
771
853
  /**
772
- * Handles the custom "pref-viewer-menu-3d-apply" event emitted by the menu component.
854
+ * Handles the custom "pref-viewer-3d-render-settings-change-requested" event emitted by the menu component.
773
855
  * Forwards the requested settings to the render-application pipeline.
774
856
  * @private
775
857
  * @param {CustomEvent} event - Menu event containing the `settings` payload.
776
858
  * @returns {void}
777
859
  */
778
- #onMenuApply(event) {
860
+ async #onMenuApply(event) {
779
861
  event.stopPropagation();
780
862
  const settings = event.detail?.settings;
781
863
  if (!settings) {
782
864
  return;
783
865
  }
784
- this.#applyRenderSettings(settings);
866
+ await this.#applyRenderSettings(settings);
785
867
  }
786
868
 
787
869
  /**
@@ -913,6 +995,17 @@ export default class PrefViewer extends HTMLElement {
913
995
  }
914
996
  }
915
997
 
998
+ /**
999
+ * Processes queued style overrides and advances the task queue.
1000
+ * @private
1001
+ * @param {object} styles - Style overrides to apply.
1002
+ * @returns {void}
1003
+ */
1004
+ #processStyles(styles) {
1005
+ this.#applyStyles(styles);
1006
+ this.#processNextTask();
1007
+ }
1008
+
916
1009
  /**
917
1010
  * Processes visibility configuration for the model and scene.
918
1011
  * Shows or hides the model and/or scene based on the config, then processes the next task.
@@ -936,6 +1029,112 @@ export default class PrefViewer extends HTMLElement {
936
1029
  this.#processNextTask();
937
1030
  }
938
1031
 
1032
+ /**
1033
+ * Constructs and dispatches a CustomEvent from the host element.
1034
+ * Strips any incoming `mode` field from the detail to avoid leaking stale values,
1035
+ * then re-attaches `mode` only for generic (non-scoped) event types.
1036
+ * Always appends a `timestamp` (Unix ms) so consumers can correlate events.
1037
+ * @private
1038
+ * @param {string} type - The event type string, e.g. "pref-viewer-loaded".
1039
+ * @param {object} [detail={}] - Arbitrary key/value pairs to include in `event.detail`.
1040
+ * @returns {void}
1041
+ */
1042
+ #dispatchPrefViewerEvent(type, detail = {}) {
1043
+ const eventDetail = { ...(detail ?? {}) };
1044
+ delete eventDetail.mode;
1045
+
1046
+ const hasExplicitModeScope = type.startsWith("pref-viewer-2d-")
1047
+ || type.startsWith("pref-viewer-3d-")
1048
+ || type.startsWith("drawing-")
1049
+ || type.startsWith("scene-");
1050
+
1051
+ if (!hasExplicitModeScope) {
1052
+ eventDetail.mode = this.#mode;
1053
+ }
1054
+ eventDetail.timestamp = Date.now();
1055
+
1056
+ this.dispatchEvent(
1057
+ new CustomEvent(type, {
1058
+ bubbles: true,
1059
+ cancelable: false,
1060
+ composed: true,
1061
+ detail: eventDetail,
1062
+ }),
1063
+ );
1064
+ }
1065
+
1066
+ /**
1067
+ * Normalizes a mixed array of element descriptors into a plain string-id list.
1068
+ * Accepts raw strings, objects with an `id` property, or objects with an `Id` property.
1069
+ * Non-string and empty-string results are filtered out.
1070
+ * @private
1071
+ * @param {Array<string|{id?: string, Id?: string}>} value - Array of id-like items to normalize.
1072
+ * @returns {string[]} Deduplicated, non-empty string ids.
1073
+ */
1074
+ #normalizeIdList(value) {
1075
+ if (!Array.isArray(value)) {
1076
+ return [];
1077
+ }
1078
+ return value.map((item) => (typeof item === "string" ? item : (item?.id ?? item?.Id))).filter((id) => typeof id === "string" && id.length > 0);
1079
+ }
1080
+
1081
+ /**
1082
+ * Normalizes a mixed array of selection descriptors into a canonical list of selection items.
1083
+ * Each output item contains an `id` string and a `highlights` string array.
1084
+ * Accepts raw id strings (expanded to `{ id, highlights: [id] }`), or objects with
1085
+ * `id`/`Id` and optional `highlights`/`Highlights` fields.
1086
+ * Items that cannot be resolved to a non-empty string id are discarded.
1087
+ * @private
1088
+ * @param {Array<string|{id?: string, Id?: string, highlights?: string[], Highlights?: string[]}>} value - Raw selection descriptors.
1089
+ * @returns {Array<{id: string, highlights: string[]}>} Normalized, non-null selection items.
1090
+ */
1091
+ #normalizeSelectionItems(value) {
1092
+ if (!Array.isArray(value)) {
1093
+ return [];
1094
+ }
1095
+ return value
1096
+ .map((item) => {
1097
+ if (typeof item === "string") {
1098
+ return { id: item, highlights: [item] };
1099
+ }
1100
+ const id = item?.id ?? item?.Id;
1101
+ if (typeof id !== "string" || id.length === 0) {
1102
+ return null;
1103
+ }
1104
+ const highlights = item?.highlights ?? item?.Highlights ?? [id];
1105
+ return {
1106
+ id,
1107
+ highlights: this.#normalizeIdList(highlights),
1108
+ };
1109
+ })
1110
+ .filter(Boolean);
1111
+ }
1112
+
1113
+ /**
1114
+ * Dispatches a `pref-viewer-selection-state-changed` event carrying the current selection snapshot.
1115
+ * Called whenever `selectableIds`, `selectedIds`, or `selectedItems` change.
1116
+ * @private
1117
+ * @returns {void}
1118
+ */
1119
+ #emitSelectionStateChanged() {
1120
+ this.#dispatchPrefViewerEvent("pref-viewer-selection-state-changed", this.getSelectionState());
1121
+ }
1122
+
1123
+ /** Apply the current semantic snapshot to the 2D renderer when available. */
1124
+ #applySelectionStateTo2D() {
1125
+ this.#component2D?.applySelectionState?.(this.getSelectionState());
1126
+ }
1127
+
1128
+ /**
1129
+ * Dispatches a `pref-viewer-3d-turnable-changed` event carrying the current turnable snapshot.
1130
+ * Called after `startTurnable`, `stopTurnable`, or `toggleTurnable` mutates the internal state.
1131
+ * @private
1132
+ * @returns {void}
1133
+ */
1134
+ #emitTurnableChanged() {
1135
+ this.#dispatchPrefViewerEvent("pref-viewer-3d-turnable-changed", { state: this.getTurnableState() });
1136
+ }
1137
+
939
1138
  /**
940
1139
  * ---------------------------
941
1140
  * Public methods
@@ -1070,13 +1269,25 @@ export default class PrefViewer extends HTMLElement {
1070
1269
  if (!config) {
1071
1270
  return false;
1072
1271
  }
1073
- this.#addTaskToQueue(config, "config");
1074
- if (config.drawing) {
1075
- this.#addTaskToQueue(config.drawing, "drawing");
1272
+ if (config.styles) {
1273
+ this.#addTaskToQueue(config.styles, "styles");
1076
1274
  }
1077
1275
  if (config.culture) {
1078
1276
  this.#addTaskToQueue(config.culture, "culture");
1079
1277
  }
1278
+ this.#addTaskToQueue(config, "config");
1279
+ if (config.drawing) {
1280
+ this.#addTaskToQueue(config.drawing, "drawing");
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Returns a diagnostic snapshot for the active 3D runtime.
1286
+ * @public
1287
+ * @returns {object|null} Debug information, or null when the 3D component is not initialized.
1288
+ */
1289
+ getDebugInfo() {
1290
+ return this.#component3D?.getDebugInfo?.() ?? null;
1080
1291
  }
1081
1292
 
1082
1293
  /**
@@ -1188,6 +1399,236 @@ export default class PrefViewer extends HTMLElement {
1188
1399
  this.#addTaskToQueue(options, "options");
1189
1400
  }
1190
1401
 
1402
+ /**
1403
+ * Sets supported PrefViewer style variables from an object or JSON string and adds them to the task queue.
1404
+ * Currently supports `colorPrimary`, which updates `PrefViewerStyleVariables.colorPrimary`, applies the host CSS custom property, and persists the value in localStorage.
1405
+ * @public
1406
+ * @param {object|string} styles - Style overrides object or JSON string.
1407
+ * @returns {boolean|void} Returns false if styles are invalid; otherwise void.
1408
+ */
1409
+ setStyles(styles) {
1410
+ styles = typeof styles === "string" ? JSON.parse(styles) : styles;
1411
+ if (!styles || typeof styles !== "object") {
1412
+ return false;
1413
+ }
1414
+ this.#addTaskToQueue(styles, "styles");
1415
+ }
1416
+
1417
+ /**
1418
+ * Sets image-based lighting options using the stable contract entry point.
1419
+ * @public
1420
+ * @param {object|string} ibl - IBL options payload.
1421
+ * @returns {boolean|void} Returns false if options are invalid; otherwise void.
1422
+ */
1423
+ setIBL(ibl) {
1424
+ ibl = typeof ibl === "string" ? JSON.parse(ibl) : ibl;
1425
+ if (!ibl) {
1426
+ return false;
1427
+ }
1428
+ return this.setOptions({ ibl });
1429
+ }
1430
+
1431
+ /**
1432
+ * Defines the elements that the host application considers selectable.
1433
+ * The normalized state is rendered by the 2D component as non-destructive yellow SVG clones.
1434
+ * @public
1435
+ * @param {Array<string|{id?: string, Id?: string}>} elements - Element ids or objects containing id/Id.
1436
+ * @returns {object} Current selection state.
1437
+ */
1438
+ setSelectableElements(elements = []) {
1439
+ this.#selectionState.selectableIds = this.#normalizeIdList(elements);
1440
+ this.#applySelectionStateTo2D();
1441
+ this.#emitSelectionStateChanged();
1442
+ return this.getSelectionState();
1443
+ }
1444
+
1445
+ /**
1446
+ * Defines the currently selected elements using the semantic PrefViewer contract.
1447
+ * @public
1448
+ * @param {Array<string|{id?: string, Id?: string, highlights?: string[], Highlights?: string[]}>} elements - Element ids or objects containing id/Id and optional highlights/Highlights.
1449
+ * @returns {object} Current selection state.
1450
+ */
1451
+ setSelectedElements(elements = []) {
1452
+ const selectedItems = this.#normalizeSelectionItems(elements);
1453
+ this.#selectionState.selectedItems = selectedItems;
1454
+ this.#selectionState.selectedIds = selectedItems.map((item) => item.id);
1455
+ this.#applySelectionStateTo2D();
1456
+ this.#emitSelectionStateChanged();
1457
+ return this.getSelectionState();
1458
+ }
1459
+
1460
+ /**
1461
+ * Applies a complete host selection state in one call.
1462
+ * @public
1463
+ * @param {{selectableIds?: Array<string|object>, selectables?: Array<string|object>, selectedItems?: Array<string|object>, selectedIds?: Array<string|object>, selected?: Array<string|object>}} state - Selection state with selectableIds/selectables and selectedItems/selectedIds/selected.
1464
+ * @returns {object} Current selection state.
1465
+ */
1466
+ applySelectionState(state = {}) {
1467
+ this.#selectionState.selectableIds = this.#normalizeIdList(state.selectableIds ?? state.selectables ?? []);
1468
+ const selectedItems = this.#normalizeSelectionItems(state.selectedItems ?? state.selectedIds ?? state.selected ?? []);
1469
+ this.#selectionState.selectedItems = selectedItems;
1470
+ this.#selectionState.selectedIds = selectedItems.map((item) => item.id);
1471
+ this.#applySelectionStateTo2D();
1472
+ this.#emitSelectionStateChanged();
1473
+ return this.getSelectionState();
1474
+ }
1475
+
1476
+ /**
1477
+ * Clears selected elements while keeping the selectable set.
1478
+ * @public
1479
+ * @returns {object} Current selection state.
1480
+ */
1481
+ clearSelectedElements() {
1482
+ this.#selectionState.selectedIds = [];
1483
+ this.#selectionState.selectedItems = [];
1484
+ this.#applySelectionStateTo2D();
1485
+ this.#emitSelectionStateChanged();
1486
+ return this.getSelectionState();
1487
+ }
1488
+
1489
+ /**
1490
+ * Clears selectable elements while keeping the current selected state.
1491
+ * @public
1492
+ * @returns {object} Current selection state.
1493
+ */
1494
+ clearSelectableElements() {
1495
+ this.#selectionState.selectableIds = [];
1496
+ this.#applySelectionStateTo2D();
1497
+ this.#emitSelectionStateChanged();
1498
+ return this.getSelectionState();
1499
+ }
1500
+
1501
+ /**
1502
+ * Clears all semantic selection state.
1503
+ * @public
1504
+ * @returns {object} Current selection state.
1505
+ */
1506
+ clearSelectionState() {
1507
+ this.#selectionState = {
1508
+ selectableIds: [],
1509
+ selectedIds: [],
1510
+ selectedItems: [],
1511
+ };
1512
+ this.#applySelectionStateTo2D();
1513
+ this.#emitSelectionStateChanged();
1514
+ return this.getSelectionState();
1515
+ }
1516
+
1517
+ /**
1518
+ * Returns the current semantic selection state.
1519
+ * @public
1520
+ * @returns {{selectableIds: string[], selectedIds: string[], selectedItems: object[]}}
1521
+ */
1522
+ getSelectionState() {
1523
+ return {
1524
+ selectableIds: [...this.#selectionState.selectableIds],
1525
+ selectedIds: [...this.#selectionState.selectedIds],
1526
+ selectedItems: this.#selectionState.selectedItems.map((item) => ({
1527
+ id: item.id,
1528
+ highlights: [...item.highlights],
1529
+ })),
1530
+ };
1531
+ }
1532
+
1533
+ /**
1534
+ * Publishes host-forwarded semantic ids through the same exact event contract as 2D hit-testing.
1535
+ * @public
1536
+ * @param {object|string|Array<object|string>} element - Element id, descriptor, collection, or `{ ids }` detail.
1537
+ * @returns {void}
1538
+ */
1539
+ notifyElementClick(element) {
1540
+ let candidates;
1541
+ if (Array.isArray(element)) {
1542
+ candidates = element;
1543
+ } else if (Array.isArray(element?.ids)) {
1544
+ candidates = element.ids;
1545
+ } else {
1546
+ candidates = [element?.element ?? element];
1547
+ }
1548
+ const ids = candidates
1549
+ .map((candidate) => typeof candidate === "string" ? candidate : (candidate?.id ?? candidate?.Id))
1550
+ .filter((id) => typeof id === "string" && id.length > 0);
1551
+ if (ids.length === 0) {
1552
+ return;
1553
+ }
1554
+ this.dispatchEvent(new CustomEvent("pref-viewer-element-click", {
1555
+ bubbles: true,
1556
+ cancelable: true,
1557
+ composed: true,
1558
+ detail: { ids },
1559
+ }));
1560
+ }
1561
+
1562
+ /**
1563
+ * Add a host-owned SVG node through the public root facade.
1564
+ * @public
1565
+ * @param {string} id - Owner id.
1566
+ * @param {string} svgId - Stable selector/id.
1567
+ * @param {SVGElement} svg - SVG node.
1568
+ * @returns {boolean} True when appended.
1569
+ */
1570
+ appendSvgNode(id, svgId, svg) {
1571
+ return this.#component2D?.appendSvgNode?.(id, svgId, svg) ?? false;
1572
+ }
1573
+
1574
+ /**
1575
+ * Remove every SVG node previously added through appendSvgNode.
1576
+ * @public
1577
+ * @returns {void}
1578
+ * */
1579
+ removeAppendedSvgNodes() {
1580
+ this.#component2D?.removeAppendedSvgNodes?.();
1581
+ }
1582
+
1583
+ /**
1584
+ * Starts the semantic turnable state.
1585
+ * @public
1586
+ * @param {{speed?: number, direction?: string}} [options] - Optional speed/direction values.
1587
+ * @returns {object} Current turnable state.
1588
+ */
1589
+ startTurnable(options = {}) {
1590
+ this.#turnableState = {
1591
+ ...this.#turnableState,
1592
+ ...options,
1593
+ running: true,
1594
+ };
1595
+ this.#emitTurnableChanged();
1596
+ return this.getTurnableState();
1597
+ }
1598
+
1599
+ /**
1600
+ * Stops the semantic turnable state.
1601
+ * @public
1602
+ * @returns {object} Current turnable state.
1603
+ */
1604
+ stopTurnable() {
1605
+ this.#turnableState = {
1606
+ ...this.#turnableState,
1607
+ running: false,
1608
+ };
1609
+ this.#emitTurnableChanged();
1610
+ return this.getTurnableState();
1611
+ }
1612
+
1613
+ /**
1614
+ * Toggles the semantic turnable state.
1615
+ * @public
1616
+ * @param {object} [options] - Optional speed/direction values used when starting.
1617
+ * @returns {object} Current turnable state.
1618
+ */
1619
+ toggleTurnable(options = {}) {
1620
+ return this.#turnableState.running ? this.stopTurnable() : this.startTurnable(options);
1621
+ }
1622
+
1623
+ /**
1624
+ * Returns the current semantic turnable state.
1625
+ * @public
1626
+ * @returns {{running: boolean, speed: number, direction: string}}
1627
+ */
1628
+ getTurnableState() {
1629
+ return { ...this.#turnableState };
1630
+ }
1631
+
1191
1632
  /**
1192
1633
  * Opens a modal dialog with the specified title, content, and footer.
1193
1634
  * @public
@@ -1433,31 +1874,20 @@ export default class PrefViewer extends HTMLElement {
1433
1874
  }
1434
1875
 
1435
1876
  /**
1436
- * Redirects scene assignments to `loadScene()` for normalized handling.
1877
+ * Sends style updates to `setStyles()` enabling reactive bindings.
1437
1878
  * @public
1438
- * @param {object|string} value - Scene payload or serialized JSON.
1879
+ * @param {object|string} value - Style overrides object or JSON string.
1439
1880
  */
1440
- set scene(value) {
1441
- this.loadScene(value);
1881
+ set styles(value) {
1882
+ this.setStyles(value);
1442
1883
  }
1443
-
1444
1884
  /**
1445
- * Enables or disables the hover highlight effect at runtime.
1446
- * Accepts boolean or string "true"/"false".
1447
- * @public
1448
- * @param {boolean|string} value
1449
- */
1450
- set highlightEnabled(value) {
1451
- this.setAttribute("highlight-enabled", String(value));
1452
- }
1453
-
1454
- /**
1455
- * Sets the hover highlight color at runtime.
1885
+ * Redirects scene assignments to `loadScene()` for normalized handling.
1456
1886
  * @public
1457
- * @param {string} value - Hex color string, e.g. "#ff6700".
1887
+ * @param {object|string} value - Scene payload or serialized JSON.
1458
1888
  */
1459
- set highlightColor(value) {
1460
- this.setAttribute("highlight-color", value);
1889
+ set scene(value) {
1890
+ this.loadScene(value);
1461
1891
  }
1462
1892
 
1463
1893
  /**