@preference-sl/pref-viewer 2.13.0-beta.3 → 2.13.0-beta.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.13.0-beta.3",
3
+ "version": "2.13.0-beta.5",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -35,11 +35,11 @@
35
35
  "index.d.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@babylonjs/core": "^8.47.0",
39
- "@babylonjs/loaders": "^8.47.0",
40
- "@babylonjs/serializers": "^8.47.0",
38
+ "@babylonjs/core": "^8.50.5",
39
+ "@babylonjs/loaders": "^8.50.5",
40
+ "@babylonjs/serializers": "^8.50.5",
41
41
  "@panzoom/panzoom": "^4.6.0",
42
- "babylonjs-gltf2interface": "^8.47.0",
42
+ "babylonjs-gltf2interface": "^8.50.5",
43
43
  "buffer": "^6.0.3",
44
44
  "idb": "^8.0.3",
45
45
  "is-svg": "^6.1.0",
@@ -388,7 +388,7 @@ export default class BabylonJSController {
388
388
 
389
389
  let lightsChanged = false;
390
390
 
391
- const iblEnabled = this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.cachedUrl !== null;
391
+ const iblEnabled = this.#settings.iblEnabled && this.#options.ibl?.cachedUrl !== null;
392
392
 
393
393
  if (iblEnabled) {
394
394
  if (hemiLight) {
@@ -935,7 +935,7 @@ export default class BabylonJSController {
935
935
 
936
936
  this.#ensureMeshesReceiveShadows();
937
937
 
938
- const iblEnabled = this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.cachedUrl !== null;
938
+ const iblEnabled = this.#settings.iblEnabled && this.#options.ibl?.cachedUrl !== null;
939
939
  const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
940
940
 
941
941
  if (iblShadowsEnabled) {
@@ -22,7 +22,7 @@ export const translations = {
22
22
  helper: "Reduction of ambient light in corners and areas close to surfaces.",
23
23
  },
24
24
  iblEnabled: {
25
- label: "IBL",
25
+ label: "Image Based Lighting (IBL)",
26
26
  helper: "Uses the HDR environment to light the scene.",
27
27
  },
28
28
  shadowsEnabled: {
@@ -68,11 +68,11 @@ export const translations = {
68
68
  helper: "Suaviza aristas y líneas.",
69
69
  },
70
70
  ambientOcclusionEnabled: {
71
- label: "Ambient Occlusion",
71
+ label: "Oclusión ambiental",
72
72
  helper: "Atenuación de la luz ambiental en rincones y zonas cercanas entre superficies.",
73
73
  },
74
74
  iblEnabled: {
75
- label: "IBL",
75
+ label: "Iluminación basada en imagen (IBL)",
76
76
  helper: "Usa la imagen HDR de entorno para iluminar la escena.",
77
77
  },
78
78
  shadowsEnabled: {
@@ -1,15 +1,18 @@
1
1
  /**
2
- * ContainerData - Represents the state and metadata of a asset container in the 3D viewer (e.g., model, environment, materials).
2
+ * ContainerData - Represents state, cache metadata, and update lifecycle for a 3D asset container
3
+ * (for example: model, environment, or materials).
3
4
  *
4
5
  * Responsibilities:
5
- * - Tracks container name, show (if must be visible), visibility (if currently visible), size, and timestamp.
6
- * - Manages update state for asynchronous operations (pending, success, etc.).
7
- * - Provides methods to reset, set pending, and set success states.
6
+ * - Tracks persistent container fields (`storage`, `show`, `size`, `timeStamp`, `metadata`, `visible`).
7
+ * - Tracks staged update data in `update` (`pending`, `success`, staged storage/show/cache info).
8
+ * - Provides helpers to stage updates (`setPending`, `setPendingCacheData`, `setPendingWithCurrentStorage`)
9
+ * and commit them (`setSuccess(true)`).
8
10
  *
9
11
  * Usage:
10
- * - Instantiate with a name: new ContainerData("model")
11
- * - Use setPending(), setSuccess(), and reset() to manage update state.
12
- * - Access status via isPending, isSuccess, isVisible getters.
12
+ * - Instantiate with a name: `new ContainerData("model")`.
13
+ * - Stage a reload with `setPending(...)`, optionally seed cache metadata, then finalize with `setSuccess(true)`.
14
+ * - Use `setPendingWithCurrentStorage()` to request a reload from the currently stored source.
15
+ * - Inspect flags via `isPending`, `isSuccess`, `isVisible`, and `mustBeShown`.
13
16
  */
14
17
  export class ContainerData {
15
18
  constructor(name = "") {
@@ -88,17 +91,18 @@ export class ContainerData {
88
91
  }
89
92
 
90
93
  /**
91
- * MaterialData - Represents the state and metadata of a material in the 3D viewer.
94
+ * MaterialData - Represents a material option plus its update lifecycle in the 3D viewer.
92
95
  *
93
96
  * Responsibilities:
94
- * - Tracks material name, value, node names, and node prefixes.
95
- * - Manages update state for asynchronous operations (pending, success, etc.).
96
- * - Provides methods to reset, set pending, and set success states.
97
+ * - Stores material identity and targeting info (`name`, `nodeNames`, `nodePrefixes`).
98
+ * - Stores current applied material value (`value`).
99
+ * - Tracks staged material changes in `update` (`pending`, `success`, `value`).
97
100
  *
98
101
  * Usage:
99
- * - Instantiate with a name and optional value: new MaterialData("innerWall", value, nodeNames, nodePrefixes)
100
- * - Use setPending(), setSuccess(), and reset() to manage update state.
101
- * - Access status via isPending, isSuccess getters.
102
+ * - Instantiate with initial values: `new MaterialData("innerWall", value, nodeNames, nodePrefixes)`.
103
+ * - Stage a change with `setPending(...)`, then commit with `setSuccess(true)`.
104
+ * - Use `setPendingWithCurrent()` to reapply the currently stored material value.
105
+ * - Inspect flags via `isPending` and `isSuccess`.
102
106
  */
103
107
  export class MaterialData {
104
108
  constructor(name = "", value = null, nodeNames = [], nodePrefixes = []) {
@@ -146,17 +150,18 @@ export class MaterialData {
146
150
  }
147
151
 
148
152
  /**
149
- * CameraData - Represents the state and metadata of a camera in the 3D viewer.
153
+ * CameraData - Represents camera selection state and camera-lock behavior for the 3D viewer.
150
154
  *
151
155
  * Responsibilities:
152
- * - Tracks camera name, value, and locked status.
153
- * - Manages update state for asynchronous operations (pending, success, etc.).
154
- * - Provides methods to reset, set pending, and set success states.
156
+ * - Stores current camera selection (`value`) and lock state (`locked`).
157
+ * - Tracks staged camera updates in `update` (`pending`, `success`, `value`, `locked`).
158
+ * - Provides helpers to stage (`setPending`, `setPendingWithCurrent`) and commit (`setSuccess(true)`) updates.
155
159
  *
156
160
  * Usage:
157
- * - Instantiate with a name and optional value: new CameraData("camera", value, locked)
158
- * - Use setPending(), setSuccess(), and reset() to manage update state.
159
- * - Access status via isPending, isSuccess getters.
161
+ * - Instantiate with a name and optional value: `new CameraData("camera", value, locked)`.
162
+ * - Stage a camera update with `setPending(value, locked)`.
163
+ * - Pass `null` as value to request an unlocked/default camera behavior.
164
+ * - Inspect flags via `isPending` and `isSuccess`.
160
165
  */
161
166
  export class CameraData {
162
167
  defaultLocked = true;
@@ -210,33 +215,40 @@ export class CameraData {
210
215
  }
211
216
 
212
217
  /**
213
- * IBLData - Tracks configurable settings for image-based lighting assets (IBL/HDR environments).
218
+ * IBLData - Stores image-based lighting configuration for HDR environment rendering.
214
219
  *
215
220
  * Responsibilities:
216
- * - Stores the HDR url, intensity scalar, whether environment-provided shadows should render, and cache timestamp.
217
- * - Exposes a lightweight pending/success state machine so UI flows know when a new IBL selection is being fetched.
218
- * - Provides helpers to update values atomically via `setValues`, toggle pending state, and mark completion.
221
+ * - Stores source and cache references (`url`, `cachedUrl`) for the HDR environment map.
222
+ * - Stores runtime tuning values (`intensity`, `shadows`) and cache identity (`timeStamp`).
223
+ * - Provides helpers to fully reset IBL state (`reset`) or partially update known fields (`setValues`).
219
224
  *
220
225
  * Usage:
221
- * - Instantiate with defaults: `const ibl = new IBLData();`
222
- * - Call `setPending()` before kicking off an async download, `setValues()` as metadata streams in, and `setSuccess(true)` once loading finishes.
223
- * - Inspect `isPending`/`isSuccess` to drive UI or re-render logic.
226
+ * - Instantiate with defaults: `const ibl = new IBLData();`.
227
+ * - Call `setValues(...)` with only the fields you want to update (undefined preserves current values).
228
+ * - Call `reset()` to clear the current IBL environment and restore default intensity/shadows.
224
229
  */
225
230
  export class IBLData {
226
231
  defaultIntensity = 1.0;
227
232
  defaultShadows = false;
228
- constructor(url = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null) {
229
- this.url = url;
230
- this.cachedUrl = null;
231
- this.intensity = intensity;
232
- this.shadows = shadows;
233
- this.timeStamp = timeStamp;
234
- }
235
- setValues(url, cachedUrl = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null) {
233
+ constructor(url = null, cachedUrl = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null) {
236
234
  this.url = url;
237
235
  this.cachedUrl = cachedUrl;
238
236
  this.intensity = intensity;
239
237
  this.shadows = shadows;
240
238
  this.timeStamp = timeStamp;
241
239
  }
240
+ reset() {
241
+ this.url = null
242
+ this.cachedUrl = null;
243
+ this.intensity = this.defaultIntensity;
244
+ this.shadows = this.defaultShadows;
245
+ this.timeStamp = null;
246
+ }
247
+ setValues(url, cachedUrl, intensity, shadows, timeStamp) {
248
+ this.url = url !== undefined ? url : this.url;
249
+ this.cachedUrl = cachedUrl !== undefined ? cachedUrl : this.cachedUrl;
250
+ this.intensity = intensity !== undefined ? intensity : this.intensity;
251
+ this.shadows = shadows !== undefined ? shadows : this.shadows;
252
+ this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
253
+ }
242
254
  }
@@ -323,6 +323,10 @@ export default class PrefViewer3D extends HTMLElement {
323
323
  /**
324
324
  * Resolves incoming IBL settings (HDR URL, timestamp, intensity, shadows) and marks the option as pending when changed.
325
325
  * Fetches signed URLs/time stamps when storage keys are provided so the Babylon controller can reload the environment map.
326
+ * If `options.ibl` exists but `options.ibl.url` is undefined (missing), the current IBL URL is kept and not marked as pending.
327
+ * This allows updating other IBL properties (such as intensity or shadows) without requiring a new URL.
328
+ * If `options.ibl.url` is explicitly provided as `null` (or resolves to an unavailable URL), it is treated as a request to
329
+ * clear the current IBL environment, and the IBL state is reset so Babylon can remove the environment map.
326
330
  * @private
327
331
  * @param {object} options - Options payload that may contain an `ibl` block with url, intensity, or shadow flags.
328
332
  * @returns {Promise<boolean>} Resolves to true when any IBL property differs from the cached state, otherwise false.
@@ -339,14 +343,20 @@ export default class PrefViewer3D extends HTMLElement {
339
343
  let shadows = undefined;
340
344
  let intensity = undefined;
341
345
 
342
- if (options.ibl.url) {
343
- url = options.ibl.url;
346
+ if (typeof options.ibl.url === "string" && options.ibl.url.length > 0) {
344
347
  const fileStorage = new FileStorage("PrefViewer", "Files");
345
348
  const newURL = await fileStorage.getURL(options.ibl.url);
346
349
  if (newURL) {
350
+ url = options.ibl.url;
347
351
  cachedUrl = newURL;
348
352
  timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
353
+ } else {
354
+ url = null;
355
+ cachedUrl = null;
349
356
  }
357
+ } else if (options.ibl.url === null) {
358
+ url = null;
359
+ cachedUrl = null;
350
360
  }
351
361
  if (options.ibl.shadows !== undefined) {
352
362
  shadows = options.ibl.shadows;
@@ -362,7 +372,11 @@ export default class PrefViewer3D extends HTMLElement {
362
372
  intensity !== undefined && intensity !== iblState.intensity ||
363
373
  intensity === undefined && iblState.intensity !== iblState.defaultIntensity;
364
374
  if (needUpdate) {
365
- iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
375
+ if (url === null || cachedUrl === null) {
376
+ iblState.reset();
377
+ } else {
378
+ iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
379
+ }
366
380
  }
367
381
 
368
382
  return needUpdate;
@@ -756,6 +770,16 @@ export default class PrefViewer3D extends HTMLElement {
756
770
  return this.#babylonJSController.getRenderSettings();
757
771
  }
758
772
 
773
+ /**
774
+ * Reports whether an IBL environment map is currently available (cached URL present).
775
+ * @public
776
+ * @returns {boolean} True when `options.ibl.cachedUrl` exists and is non-empty.
777
+ */
778
+ isIBLAvailable() {
779
+ const cachedUrl = this.#data?.options?.ibl?.cachedUrl;
780
+ return typeof cachedUrl === "string" ? cachedUrl.length > 0 : Boolean(cachedUrl);
781
+ }
782
+
759
783
  /**
760
784
  * Applies render settings that require reloading the Babylon.js scene.
761
785
  * @public
@@ -9,6 +9,7 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
9
9
  * - Builds an accessible hover/focus-activated panel with switches for AA, SSAO, IBL, and dynamic shadows.
10
10
  * - Caches translated copy in `#texts` and listens for culture changes via the `culture` attribute or `setCulture()`.
11
11
  * - Tracks applied vs. draft render settings so pending diffs, button enablement, and status text stay in sync.
12
+ * - Supports per-switch availability (enabled/disabled), preventing interaction and pending diffs for unavailable toggles.
12
13
  * - Emits `pref-viewer-menu-3d-apply` whenever the user confirms toggles, allowing PrefViewer/BabylonJSController to persist.
13
14
  * - Shows transient status/error messages and per-switch pending states while operations complete.
14
15
  *
@@ -17,6 +18,7 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
17
18
  * - `setApplying(isApplying, hasError?)`: locks the UI and optionally displays errors while async updates run.
18
19
  * - `setViewerHover(isHovering)`: opens/closes the panel based on viewer hover state.
19
20
  * - `setEnabled(isEnabled)`: hides the menu in 2D mode and clears hover data.
21
+ * - `setSwitchEnabled(settingKey, isEnabled)`: enables/disables a concrete toggle (e.g. IBL) based on runtime availability.
20
22
  * - `setCulture(cultureId)`: forces a locale change and re-renders copy from the i18n layer.
21
23
  *
22
24
  * Lifecycle & Integration:
@@ -52,6 +54,7 @@ export default class PrefViewerMenu3D extends HTMLElement {
52
54
  #MENU_SWITCHES = Object.keys(this.#DEFAULT_RENDER_SETTINGS);
53
55
  #appliedSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
54
56
  #draftSettings = { ...this.#DEFAULT_RENDER_SETTINGS };
57
+ #switchAvailability = {};
55
58
  #statusTimeout = null;
56
59
  #isApplying = false;
57
60
  #isEnabled = true;
@@ -352,6 +355,9 @@ export default class PrefViewerMenu3D extends HTMLElement {
352
355
  * @returns {void}
353
356
  */
354
357
  #handleSwitchChange(event) {
358
+ if (event.currentTarget?.disabled) {
359
+ return;
360
+ }
355
361
  const key = event.currentTarget?.dataset?.setting;
356
362
  if (!key) {
357
363
  return;
@@ -388,7 +394,10 @@ export default class PrefViewerMenu3D extends HTMLElement {
388
394
  #updateSwitches() {
389
395
  Object.entries(this.#elements.switches).forEach(([key, input]) => {
390
396
  if (input) {
397
+ const isEnabled = this.#switchAvailability[key] !== false;
391
398
  input.checked = Boolean(this.#draftSettings[key]);
399
+ input.disabled = !isEnabled;
400
+ this.#elements.switchWrappers[key]?.toggleAttribute("data-disabled", !isEnabled);
392
401
  }
393
402
  });
394
403
  }
@@ -416,7 +425,12 @@ export default class PrefViewerMenu3D extends HTMLElement {
416
425
  * @returns {string[]} Keys currently pending application.
417
426
  */
418
427
  #getPendingKeys() {
419
- return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => this.#draftSettings[key] !== this.#appliedSettings[key]);
428
+ return Object.keys(this.#DEFAULT_RENDER_SETTINGS).filter((key) => {
429
+ if (this.#elements.switches[key]?.disabled) {
430
+ return false;
431
+ }
432
+ return this.#draftSettings[key] !== this.#appliedSettings[key];
433
+ });
420
434
  }
421
435
 
422
436
  /**
@@ -561,4 +575,24 @@ export default class PrefViewerMenu3D extends HTMLElement {
561
575
  this.#closeMenu();
562
576
  }
563
577
  }
578
+
579
+ /**
580
+ * Enables or disables a specific render-setting switch in the menu.
581
+ * @public
582
+ * @param {string} settingKey - Switch key (e.g. "iblEnabled").
583
+ * @param {boolean} [isEnabled=true] - True to allow interaction, false to disable it.
584
+ * @returns {void}
585
+ */
586
+ setSwitchEnabled(settingKey, isEnabled = true) {
587
+ if (!settingKey || !this.#MENU_SWITCHES.includes(settingKey)) {
588
+ return;
589
+ }
590
+ const enabled = Boolean(isEnabled);
591
+ this.#switchAvailability[settingKey] = enabled;
592
+ if (!enabled) {
593
+ this.#draftSettings[settingKey] = this.#appliedSettings[settingKey];
594
+ }
595
+ this.#updateSwitches();
596
+ this.#updatePendingState();
597
+ }
564
598
  }
@@ -286,6 +286,7 @@ export default class PrefViewer extends HTMLElement {
286
286
 
287
287
  this.#wrapper.appendChild(this.#menu3D);
288
288
  this.#menu3DSyncSettings();
289
+ this.#menu3DUpdateSwitchAvailability();
289
290
  this.#menu3DUpdateAvailability();
290
291
  }
291
292
 
@@ -372,6 +373,19 @@ export default class PrefViewer extends HTMLElement {
372
373
  this.#menu3D.setEnabled?.(isMode3D);
373
374
  }
374
375
 
376
+ /**
377
+ * Enables/disables menu switches according to current 3D option availability.
378
+ * @private
379
+ * @returns {void}
380
+ */
381
+ #menu3DUpdateSwitchAvailability() {
382
+ if (!this.#menu3D) {
383
+ return;
384
+ }
385
+ const iblAvailable = this.#component3D?.isIBLAvailable?.() === true;
386
+ this.#menu3D.setSwitchEnabled?.("iblEnabled", iblAvailable);
387
+ }
388
+
375
389
  /**
376
390
  * Applies render toggles via PrefViewer3D, showing progress in the menu and resyncing on completion.
377
391
  * When no changes occur, the menu is simply refreshed to match the controller snapshot.
@@ -520,6 +534,9 @@ export default class PrefViewer extends HTMLElement {
520
534
  #on3DLoaded(detail) {
521
535
  this.#isLoaded = true;
522
536
  this.#isLoading = false;
537
+
538
+ this.#menu3DSyncSettings();
539
+ this.#menu3DUpdateSwitchAvailability();
523
540
 
524
541
  this.removeAttribute("loading-3d");
525
542
  this.setAttribute("loaded-3d", "");
package/src/styles.js CHANGED
@@ -234,7 +234,7 @@ export const PrefViewerMenu3DStyles = `
234
234
  border-bottom: none;
235
235
  }
236
236
 
237
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-copy {
237
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-copy {
238
238
  display: flex;
239
239
  flex-direction: column;
240
240
  gap: calc(var(--panel-spacing) / 4);
@@ -262,14 +262,14 @@ export const PrefViewerMenu3DStyles = `
262
262
  font-size: var(--font-size-small);
263
263
  }
264
264
 
265
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control {
265
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control {
266
266
  position: relative;
267
267
  width: var(--switch-control-width);
268
268
  min-width: var(--switch-control-width);
269
269
  height: var(--switch-control-height);
270
270
  }
271
271
 
272
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input {
272
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input {
273
273
  position: absolute;
274
274
  inset: 0;
275
275
  margin: 0;
@@ -277,6 +277,18 @@ export const PrefViewerMenu3DStyles = `
277
277
  cursor: pointer;
278
278
  }
279
279
 
280
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch[data-disabled]>.menu-switch-copy {
281
+ opacity: 0.6;
282
+ }
283
+
284
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch[data-disabled]>.menu-switch-control {
285
+ opacity: 0.4;
286
+ }
287
+
288
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:disabled {
289
+ cursor: not-allowed;
290
+ }
291
+
280
292
  pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-visual {
281
293
  position: absolute;
282
294
  inset: 0;
@@ -286,6 +298,10 @@ export const PrefViewerMenu3DStyles = `
286
298
  cursor: pointer;
287
299
  }
288
300
 
301
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:disabled + .menu-switch-visual {
302
+ cursor: not-allowed;
303
+ }
304
+
289
305
  pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-visual::after {
290
306
  content: "";
291
307
  position: absolute;
@@ -299,11 +315,11 @@ export const PrefViewerMenu3DStyles = `
299
315
  transition: transform 0.2s ease;
300
316
  }
301
317
 
302
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input:checked + .menu-switch-visual {
318
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:checked + .menu-switch-visual {
303
319
  background: var(--switch-control-bar-checked-color);
304
320
  }
305
321
 
306
- pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch .menu-switch-control input:checked + .menu-switch-visual::after {
322
+ pref-viewer-menu-3d>.menu-wrapper>.menu-panel>.menu-switches>.menu-switch>.menu-switch-control input:checked + .menu-switch-visual::after {
307
323
  transform: translateX(var(--switch-control-thumb-size));
308
324
  }
309
325