@preference-sl/pref-viewer 2.17.0-beta.3 → 2.17.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - Native custom elements: `<pref-viewer>`, `<pref-viewer-2d>`, `<pref-viewer-3d>`, `<pref-viewer-dialog>`, and `<pref-viewer-menu-3d>`.
8
8
  - Babylon.js-based 3D visualization for glTF and GLB assets.
9
- - Default-enabled ViewCube with 26 selectable orientations, synchronized camera feedback, smooth model framing and perspective/orthographic support.
9
+ - Default-enabled, draggable 3D ViewCube with 26 selectable orientations: click a face, edge or corner for a smooth, model-fitted view, or drag the cube to orbit the model. It follows the camera in real time and works with perspective and orthographic cameras.
10
10
  - Model-only, geometry-dependent orbital zoom protection with near-plane sampling, independent of ViewCube visibility.
11
11
  - Viewer dialogs above the render menu and ViewCube, with background pointer-input protection.
12
12
  - Proportional mouse-wheel zoom for unlocked orbital cameras: increments decrease as the camera approaches its target, using Babylon's native input without an additional viewer zoom increment. Wheel sensitivity is currently an internal setting, not a public attribute or option.
@@ -88,8 +88,8 @@ viewer.setOptions({
88
88
  intensity: 1,
89
89
  shadows: true,
90
90
  },
91
- lighting: {
92
- timeOfDay: 0.5,
91
+ render: {
92
+ lightingTimeOfDay: 0.5,
93
93
  },
94
94
  });
95
95
  ```
@@ -239,13 +239,13 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
239
239
 
240
240
  ## ViewCube
241
241
 
242
- The ViewCube reflects the effective active camera orientation and offers **26 views: six faces, twelve edges and eight corners**. Click any region to change orientation smoothly and refit the model assetContainer. Every accepted click refits, even when selecting the current orientation: previous pan, framing and zoom are discarded. Environment geometry is excluded from this fit; only visible, enabled model meshes with valid geometry contribute to bounds. Hidden or disabled model geometry, including auxiliary walls/floors hidden by viewer options, does not enlarge the fit.
242
+ The ViewCube reflects the effective active camera orientation and offers **26 views: six faces, twelve edges and eight corners**. Click any region to change orientation smoothly and refit the model assetContainer, or drag the cube to orbit the model (see [Dragging the cube](#dragging-the-cube)). Every accepted click refits, even when selecting the current orientation: previous pan, framing and zoom are discarded. Environment geometry is excluded from this fit; only visible, enabled model meshes with valid geometry contribute to bounds. Hidden or disabled model geometry, including auxiliary walls/floors hidden by viewer options, does not enlarge the fit.
243
243
 
244
- The active camera retains its identity and perspective or orthographic projection. Navigation supports Babylon ArcRotateCamera, FreeCamera and UniversalCamera, including supported parent transforms. Other camera classes, locked cameras and active XR tracked cameras are display-only. Selection does nothing when usable model bounds, a supported camera or positive viewport dimensions are unavailable.
244
+ The active camera retains its identity and perspective or orthographic projection. Navigation supports Babylon ArcRotateCamera, FreeCamera and UniversalCamera, including supported parent transforms. Other camera classes, locked cameras and active XR tracked cameras are display-only: the cube keeps following the camera, but hover, click and drag do nothing. Selection does nothing when usable model bounds, a supported camera or positive viewport dimensions are unavailable.
245
245
 
246
246
  ### Orientation and feedback
247
247
 
248
- The inner view is the principal model view. English ViewCube labels use `Inner` and `Outer` (displayed as `INNER` and `OUTER`); Spanish labels remain `Interior` and `Exterior`. Accessible names and combined edge/corner labels use the same localized terms. Face names use this world-axis convention:
248
+ The inner view is the principal model view. English ViewCube face labels read `Inner` and `Outer`; Spanish labels are `Interior` and `Exterior`. Labels are drawn on the cube faces exactly as translated and follow culture changes. Face names use this world-axis convention:
249
249
 
250
250
  | Direction | World axis | Reference |
251
251
  | --- | --- | --- |
@@ -256,17 +256,37 @@ The inner view is the principal model view. English ViewCube labels use `Inner`
256
256
  | Top / Superior | +Y | Looks down toward the model. |
257
257
  | Bottom / Inferior | -Y | Looks up toward the model. |
258
258
 
259
- The cube has sharp edges without chamfers. Edge regions appear as prism strips on both adjacent faces, with boundaries parallel to the edge; corner regions appear as small cubes. All visible surfaces belonging to an edge or corner select the same orientation.
259
+ The cube is drawn on its own small canvas, with a separate Babylon.js engine and virtual scene, in a PrefSketch-style look: six square labelled faces, thin bars on the twelve edges and small spheres on the eight corners; there are no chamfers. Every part of an edge or corner selects the same orientation. The cube is always drawn with an orthographic camera, whatever the projection or FOV of the viewer camera, and it follows only the direction of the camera, independent of model zoom. It lives in its own scene, so it adds nothing to the model scene, to model picking or to exports.
260
+
261
+ Hover paints the region under the pointer with `styles.colorPrimary` (`--pref-viewer-color-primary`). Any CSS colour syntax is accepted: hex, `rgb()`, `hsl()` or a named colour. The angularly nearest of the 26 views is shown as the current region, painted with the primary colour mixed 25% into the face fill; hover takes precedence until the pointer leaves. A primary-colour change is picked up within 250 ms, or immediately after a resize, a culture change or re-enabling the cube. The rest of the palette (face fill, borders, label text, edge bars and corner spheres) is a fixed light palette and is not configurable. There are no region tooltips.
262
+
263
+ Edges and corners are thin, so selection has a tolerance: a front-facing edge or corner whose centre is within 8 CSS pixels of the pointer is picked ahead of the face under it, corners before edges. On cubes smaller than 96 px the tolerance shrinks to a twelfth of the cube side so face centres stay selectable. Hover and click use the same pick, so the highlighted region is the one a click selects.
264
+
265
+ ### Dragging the cube
266
+
267
+ Press on the cube and move the pointer more than 3 CSS pixels to drag it. Dragging orbits the viewer camera around its target at 0.012 radians per pixel, in the same direction as dragging the model with the mouse on the main canvas: the model follows the pointer, and the cube follows the camera. Elevation stops just short of the poles and the camera keeps world +Y as up. Dragging works with ArcRotateCamera, FreeCamera and UniversalCamera; authored alpha/beta limits of an ArcRotateCamera keep being enforced by Babylon on the next frame.
260
268
 
261
- Hover uses `styles.colorPrimary`, backed by `PrefViewerStyleVariables.colorPrimary`, with **50% fill opacity** and fully opaque borders around the complete clickable region. Face hover fills only its central clickable area, excluding edge/corner regions. The angularly nearest of the 26 views has **15% primary-color fill** and no highlighted border; hover takes precedence until the pointer leaves. Primary-color and culture changes update the existing overlay. There are no region tooltips; visible face labels use uppercase lettering and increased letter spacing for readability; accessible region names remain localized.
269
+ The whole cube canvas is the drag surface, including its transparent corners, and a press on it never starts a gesture on the main camera. A press that moves 3 pixels or less is a click. A drag never selects a region, even if it ends where it started, and it cancels a running click transition. Locked cameras, active XR tracked cameras and unsupported camera classes keep the cube display-only. The cursor is `grab` over the cube, `pointer` over a selectable region and `grabbing` while dragging.
262
270
 
263
- Cube perspective follows the active camera's projection and FOV. Mouse movement changes feedback without rebuilding SVG geometry; camera pose/projection changes update the projected geometry. The cube remains independent of model zoom and does not add Babylon scene meshes or appear in exports.
271
+ ### ViewCube accessibility
272
+
273
+ The cube canvas has `role="img"` and one localized accessible name (`View orientation` in English, `Orientación de vista` in Spanish). Individual regions have no accessible names. The cube is pointer-driven: the canvas is not a tab stop, does not take keyboard focus after pointer use and draws no focus ring. Keyboard operation of the cube is not provided.
264
274
 
265
275
  ### ViewCube layout and lifecycle
266
276
 
267
- The cube stays at **top 0px / right 0px**. When both overlays are enabled, the render-menu top equals the **measured cube SVG viewport height** and updates when that size changes. The maximum cube viewport is 100 × 100 CSS pixels and shrinks to fit smaller scene viewports; the menu offset is measured, not fixed to that value. Without the cube, the menu returns to its style-defined default top. The cube remains below the menu's layer, and viewer modals cover both.
277
+ The cube is a square at the top right of the 3D view: **top 0px**, **right 0px** unless the host sets the inset below. Its side, in CSS pixels, is `min(clamp(22% of min(viewer width, viewer height), 96, 180), 40% of min(viewer width, viewer height))`, rounded to an integer. For example, a viewer whose smaller side is 300 px gets a 96 px cube, 600 px gets 132 px, 900 px or more gets the 180 px maximum, and a 200 px viewer gets 80 px (the 40% cap). A viewer without a usable size shows no cube.
278
+
279
+ Hosts can inset the cube from the right edge with the CSS custom property `--pref-viewer-view-cube-inset-right` (default `0px`). Set it on `<pref-viewer>` or on any ancestor; it inherits into the viewer:
280
+
281
+ ```css
282
+ pref-viewer {
283
+ --pref-viewer-view-cube-inset-right: 24px;
284
+ }
285
+ ```
286
+
287
+ When both overlays are enabled, the render-menu top equals the **measured side of the cube canvas** and updates when that size changes; the menu offset is measured, not a fixed value. Without the cube, the menu returns to its style-defined default top. The cube remains below the menu's layer, and viewer modals cover both. The internal `view-cube-layout` event carries `{ offset, height }` in CSS pixels: `offset` is the measured cube side (0 when there is no cube) and `height` is the scene viewport height.
268
288
 
269
- Cube resources are created only while enabled in a visible 3D view. Disabling it or switching to 2D releases its overlay, listeners, observers and pending transition. Reactivation uses the current camera without reloading assets. Layout subscriptions connect before activation and are removed when inactive or disconnected.
289
+ Cube resources are created only while enabled in a visible 3D view. Disabling it or switching to 2D releases its overlay, listeners, observers, pending transition, engine and WebGL context. An active cube uses a second WebGL context next to the viewer's; it is repainted after a context restore and after a device pixel ratio change. Reactivation uses the current camera without reloading assets. Layout subscriptions connect before activation and are removed when inactive or disconnected.
270
290
 
271
291
  Rapid selections retarget the transition from the displayed pose. Canvas pointer/wheel/key interaction, resize, projection change, camera/model replacement, locking, disabling or disposal cancels pending navigation; unlocking/reactivation does not replay it. Transitions use the existing demand-rendering scheduler and release their activity when finished without stopping unrelated model animations.
272
292
 
@@ -449,7 +469,7 @@ The component emits custom events so host applications can integrate loading ind
449
469
 
450
470
  ### Internal Listeners
451
471
 
452
- `<pref-viewer>` listens to `pref-viewer-2d-viewport-changed` from `<pref-viewer-2d>`, `pref-viewer-3d-loaded` and `pref-viewer-3d-error` from `<pref-viewer-3d>`, and `pref-viewer-3d-render-settings-change-requested` from `<pref-viewer-menu-3d>`. Internal `pref-viewer-2d-element-click` and `pref-viewer-3d-element-click` events are intercepted and forwarded through the single public `pref-viewer-element-click` contract. `BabylonJSController` listens to `pref-viewer-3d-animation-changed` from the nearest `<pref-viewer-3d>` host. The root also conditionally subscribes to the internal, non-bubbling `view-cube-layout` event while the cube is active in 3D. It carries measured layout dimensions for the render menu and is not a new public root event.
472
+ `<pref-viewer>` listens to `pref-viewer-2d-viewport-changed` from `<pref-viewer-2d>`, `pref-viewer-3d-loaded` and `pref-viewer-3d-error` from `<pref-viewer-3d>`, and `pref-viewer-3d-render-settings-change-requested` from `<pref-viewer-menu-3d>`. Internal `pref-viewer-2d-element-click` and `pref-viewer-3d-element-click` events are intercepted and forwarded through the single public `pref-viewer-element-click` contract. `BabylonJSController` listens to `pref-viewer-3d-animation-changed` from the nearest `<pref-viewer-3d>` host. The root also conditionally subscribes to the internal, non-bubbling `view-cube-layout` event while the cube is active in 3D. It carries `{ offset, height }` in CSS pixels, where `offset` is the measured side of the square cube and `height` is the scene viewport height, so the render menu can sit below the cube; it is not a new public root event.
453
473
 
454
474
  Example:
455
475
 
@@ -501,6 +521,23 @@ Render settings are persisted in `localStorage` under the `pref-viewer/render-se
501
521
 
502
522
  The showroom daylight background uses a shared linear HDR color calibrated with exposure and ACES tone mapping for the current light studio gray target, `#d9d9d9`, in a scene-less view. Both the static look and time-of-day cycle use this same daylight value; the cycle retains its dark night endpoint. Scene geometry can cover the background. The internal `BabylonJSController.config.look.showroom.backgroundDay` value is not a public background-color option and must be recalibrated if tone mapping or exposure changes.
503
523
 
524
+ ## Environment map and scene lights
525
+
526
+ `options.ibl.url` accepts two formats, chosen by the URL's extension (query and hash ignored):
527
+
528
+ | Extension | Loaded as | Notes |
529
+ | --- | --- | --- |
530
+ | `.env` | Prefiltered Babylon environment (`CubeTexture.CreateFromPrefilteredData`) | No prefiltering in the browser; a 4K environment is typically under 1 MB. |
531
+ | anything else | Radiance HDR (`HDRCubeTexture`) | Prefiltered on load, which costs time and memory for large files. |
532
+
533
+ Either file is downloaded once, cached in IndexedDB, and reused across reloads.
534
+
535
+ Directional and spot lights that come with the scene glTF (`KHR_lights_punctual`) cast shadows when `render.shadowsEnabled` is `true` and `ibl.shadows` is off, alongside the built-in light's. Every scene and model mesh casts unless its node sets `extras.castShadows: false`, and receives unless it sets `extras.receiveShadows: false`. A light's shadow darkness comes from its node's `extras.shadowDarkness`, from `0` (black) to `1` (no shadow), and is `0.5` when absent:
536
+
537
+ ```json
538
+ { "name": "Sun", "rotation": [-0.5, 0, 0, 0.866], "extras": { "shadowDarkness": 0.15 }, "extensions": { "KHR_lights_punctual": { "light": 0 } } }
539
+ ```
540
+
504
541
  ## Runtime styles
505
542
 
506
543
  Runtime styles can be passed through the `styles` attribute, the `styles` property setter, `setStyles(styles)`, or `loadConfig({ styles })`.
@@ -515,7 +552,7 @@ viewer.setStyles({
515
552
  });
516
553
  ```
517
554
 
518
- The ViewCube uses this same primary color for its hover and active-orientation feedback. Changing `colorPrimary` through `styles` or `config` attributes, `setStyles`, or `loadConfig({ styles })` also updates animation-opening highlight color, including an opening currently highlighted, without reloading the model or changing whether highlighting is enabled. The render menu color control stays synchronized. Persisted primary styles are applied to highlights when the viewer connects. `options.render.highlightColor` can still set an explicit highlight color; a later primary-style update replaces it.
555
+ The ViewCube uses this same primary color for its hover feedback and, mixed into the face fill, for its current-orientation feedback. Changing `colorPrimary` through `styles` or `config` attributes, `setStyles`, or `loadConfig({ styles })` also updates animation-opening highlight color, including an opening currently highlighted, without reloading the model or changing whether highlighting is enabled. The render menu color control stays synchronized. Persisted primary styles are applied to highlights when the viewer connects. `options.render.highlightColor` can still set an explicit highlight color; a later primary-style update replaces it.
519
556
 
520
557
  Only `colorPrimary` is currently supported. The value updates `PrefViewerStyleVariables.colorPrimary`, is exposed on the host as `--pref-viewer-color-primary`, and is persisted in `localStorage` under `pref-viewer/styles`. Persisted styles are restored when a new `<pref-viewer>` instance is created and connected.
521
558
 
@@ -570,38 +607,24 @@ Focused regressions for this work live in `tests/babylonjs-selection-controller.
570
607
 
571
608
  ### Camera implementation and maintenance
572
609
 
573
- - `src/babylonjs-view-cube-controller.js`: dedicated `BabylonJSViewCubeController` for SVG regions, orientation/projection feedback, transitions and cleanup. It delegates model bounds and destination-fit calculations to BabylonJSCameraFraming. Cube-only helpers and constants remain private; region generation remains a public static utility. Its constructor JSDoc documents the host object and callbacks.
610
+ - `src/babylonjs-view-cube-controller.js`: dedicated `BabylonJSViewCubeController` for pointer interaction (hover, click, drag), camera reading and writing, click transitions, size, colours and cleanup. It delegates model bounds and destination-fit calculations to BabylonJSCameraFraming and the drawing to the renderer. Cube-only helpers and constants remain private; region generation remains a public static utility. Its constructor JSDoc documents the host object and callbacks.
611
+ - `src/babylonjs-view-cube-renderer.js`: `BabylonJSViewCubeRenderer`, the cube's own Babylon.js engine and virtual scene with its 26 region meshes, picking and painting. It owns no render loop and no interaction logic; the controller renders it from the host loop.
612
+ - `src/view-cube-orbit-math.js`: pure helpers for orbit angles, the 26 view directions, drag deltas and the cube size rule, without Babylon or DOM dependencies.
574
613
  - `src/babylonjs-camera-framing.js`: stateless `BabylonJSCameraFraming` with static `getModelBounds`, `calculateCameraFit` and `isCameraSupported` utilities. Bounds use only visible, enabled geometry in the supplied model container; fit calculations preserve camera state and projection, use the shared camera margin and return a destination for the caller to apply. Default-camera automatic fitting uses these same utilities, preserving the current orientation and applying the destination immediately.
575
614
  - `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
576
615
  - `src/babylonjs-camera-wheel-motion.js`: internal boundary for inspecting and cancelling the coupled native wheel inertia during rendering, geometry protection and camera lifecycle changes. Babylon computes the zoom; this boundary isolates version-sensitive native input state.
577
616
  - `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
578
- - `src/styles.js`: exports `PrefViewerViewCubeStyles`, imported by the cube controller. The SVG stylesheet uses multiline CSS and scoped variables for the main visual properties; no JSDoc is required for this styles constant.
579
617
  - `src/pref-viewer-3d.js` and `src/pref-viewer.js`: bridge lazy visibility, queued config, localization and measured menu layout. The root stores the layout callback in `#handlers` and removes its listener during teardown. Shared styles place dialogs above viewer overlays.
580
618
 
581
- ViewCube class members are ordered as properties, constructor, static methods, private methods and public methods/getters. New classes and new/modified methods carry JSDoc describing their parameters, return values and current lifecycle/side effects. JavaScript `if` and `for` bodies always use braces, including single-statement bodies. Focused tests live in `tests/babylonjs-camera-framing.test.js`, `tests/babylonjs-view-cube-controller.test.js`, `tests/babylonjs-camera-zoom-controller.test.js`, `tests/babylonjs-controller-wheel.test.js` and `tests/pref-viewer.test.js`. Browser checks in the existing playground remain necessary for visual projection, hover regions and modal stacking.
619
+ ViewCube class members are ordered as properties, constructor, static methods, private methods and public methods/getters. New classes and new/modified methods carry JSDoc describing their parameters, return values and current lifecycle/side effects. JavaScript `if` and `for` bodies always use braces, including single-statement bodies. Focused tests live in `tests/babylonjs-camera-framing.test.js`, `tests/babylonjs-view-cube-controller.test.js`, `tests/babylonjs-view-cube-renderer.test.js`, `tests/view-cube-orbit-math.test.js`, `tests/babylonjs-camera-zoom-controller.test.js`, `tests/babylonjs-controller-wheel.test.js` and `tests/pref-viewer.test.js`. Browser checks in the existing playground remain necessary for cube rendering, hover regions, dragging, focus and cursor behavior, and modal stacking, because the tests run on a null engine.
582
620
 
583
- ### ViewCube SVG style maintenance
621
+ ### ViewCube rendering maintenance
584
622
 
585
- The following variables are defined in the root rule of `PrefViewerViewCubeStyles` in `src/styles.js`. They centralize defaults for future stylesheet changes; they are not additional properties supported by `setStyles` or `config.styles`.
623
+ The cube's visual constants (the light palette, face label font and size, edge and corner sizes, lighting) are internal to `src/babylonjs-view-cube-renderer.js`. They are not properties supported by `setStyles` or `config.styles`; only `colorPrimary` reaches the cube, through the hover and current colours. The renderer module also contains a dark palette constant; it is not exported from the package and is not part of the public API.
586
624
 
587
- | CSS variable | Default |
588
- | --- | --- |
589
- | `--cube-fill-color` | `#eef0f3` |
590
- | `--cube-border-color` | `#78838f` |
591
- | `--cube-border-width` | `1.0px` in the SVG coordinate system; also used by the hover outline |
592
- | `--cube-hover-color` | Shared primary color; used by the fill and opaque outline |
593
- | `--cube-hover-opacity` | `0.50` fill opacity |
594
- | `--cube-active-color` | Shared primary color |
595
- | `--cube-active-opacity` | `0.15` fill opacity |
596
- | `--cube-text-font-family` | `Arial, sans-serif` |
597
- | `--cube-text-font-size` | `9px` |
598
- | `--cube-text-color` | `#29323b` |
599
- | `--cube-text-font-weight` | `600` |
600
- | `--cube-text-letter-spacing` | `1.0px` |
601
-
602
- Both feedback colors default to `var(--pref-viewer-color-primary, ...)`, preserving supported runtime primary-color updates. Text size and spacing apply in the SVG coordinate system and scale with its viewport. Active-view borders remain transparent; hover borders remain fully opaque regardless of fill opacity. Geometry and input behavior remain owned by the controller.
603
-
604
- ViewCube listener registration and hover updates are private instance methods. Surface creation, region construction, hit-region lookup, extent interpolation and SVG projection are private static helpers; per-update projection values and interpolation progress are passed explicitly. The render menu also encapsulates hue-channel calculation and lighting playback as private methods. Playback uses one callback bound to the menu instance and cancels its scheduled frame on stop or disconnection.
625
+ Keep these rules when changing the renderer: the cube scene stays virtual and its engine stays out of `EngineStore.Instances` while it is alive (otherwise the viewer can create resources on the cube's WebGL context); Babylon input stays detached from the cube canvas (otherwise the cursor and keyboard focus are disturbed); and the engine keeps `loseContextOnDispose` so its WebGL context is released.
626
+
627
+ ViewCube listener registration, drag handling and hover updates are private instance methods. Region construction, colour parsing and extent interpolation are private static helpers; interpolation progress is passed explicitly. The render menu also encapsulates hue-channel calculation and lighting playback as private methods. Playback uses one callback bound to the menu instance and cancels its scheduled frame on stop or disconnection.
605
628
 
606
629
  ## Package scripts
607
630
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.17.0-beta.3",
3
+ "version": "2.17.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": {
@@ -1,4 +1,4 @@
1
- import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
1
+ import { ArcRotateCamera, AssetContainer, BRDFTextureTools, Camera, Color3, Color4, CubeTexture, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, ImageProcessingConfiguration, LoadAssetContainerAsync, Material, Matrix, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Texture, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
2
2
  import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
3
3
  import "@babylonjs/loaders";
4
4
  import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
@@ -1334,7 +1334,7 @@ export default class BabylonJSController {
1334
1334
  * @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
1335
1335
  * @description
1336
1336
  * Lifecycle implemented here:
1337
- * 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it.
1337
+ * 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it (a prefiltered `CubeTexture` when `options.ibl.url` is a `.env`).
1338
1338
  * 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
1339
1339
  * 3. Call `options.ibl.consumeCachedUrl(true)` to revoke temporary object URLs and clear `cachedUrl`.
1340
1340
  * 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
@@ -1348,8 +1348,13 @@ export default class BabylonJSController {
1348
1348
  let hdrTexture = null;
1349
1349
  if (this.#options.ibl?.cachedUrl) {
1350
1350
  const hdrTextureURI = this.#options.ibl.cachedUrl;
1351
- const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
1352
- hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
1351
+ if (/\.env$/i.test(String(this.#options.ibl.url ?? "").split(/[?#]/)[0])) {
1352
+ // The cached file is a blob: URL with no extension, so the loader is told it is a .env.
1353
+ hdrTexture = CubeTexture.CreateFromPrefilteredData(hdrTextureURI, this.#scene, ".env");
1354
+ } else {
1355
+ const envTextureSize = BabylonJSController.config.quality.environmentTextureSize || 1024;
1356
+ hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, envTextureSize, false, false, false, true, undefined, undefined, false, true, true);
1357
+ }
1353
1358
  } else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
1354
1359
  hdrTexture = this.#hdrTexture.clone();
1355
1360
  } else {
@@ -1586,7 +1591,7 @@ export default class BabylonJSController {
1586
1591
  return generator.getLight()?.name === "PrefViewerDirLight";
1587
1592
  });
1588
1593
 
1589
- this.#containers.environment?.AssetContainer?.lights.forEach((light) => {
1594
+ this.#containers.environment?.assetContainer?.lights.forEach((light) => {
1590
1595
  // Only shadows for DirectionalLight and SpotLight types
1591
1596
  if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
1592
1597
  return;
@@ -1595,7 +1600,9 @@ export default class BabylonJSController {
1595
1600
  const shadowGenerator = new ShadowGenerator(1024, light);
1596
1601
  shadowGenerator.useBlurExponentialShadowMap = true;
1597
1602
  shadowGenerator.blurKernel = 16;
1598
- shadowGenerator.darkness = 0.5;
1603
+ // KHR_lights_punctual parents the light to its glTF node, which carries the node's extras.
1604
+ const extrasShadowDarkness = light.parent?.metadata?.gltf?.extras?.shadowDarkness;
1605
+ shadowGenerator.darkness = typeof extrasShadowDarkness === "number" && Number.isFinite(extrasShadowDarkness) ? Math.min(Math.max(extrasShadowDarkness, 0), 1) : 0.5;
1599
1606
  shadowGenerator.bias = 0.0005;
1600
1607
  shadowGenerator.normalBias = 0.02;
1601
1608
  shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;