@preference-sl/pref-viewer 2.17.0-beta.2 → 2.17.0-beta.4
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
|
@@ -88,8 +88,8 @@ viewer.setOptions({
|
|
|
88
88
|
intensity: 1,
|
|
89
89
|
shadows: true,
|
|
90
90
|
},
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
render: {
|
|
92
|
+
lightingTimeOfDay: 0.5,
|
|
93
93
|
},
|
|
94
94
|
});
|
|
95
95
|
```
|
|
@@ -289,7 +289,9 @@ Increasing the factor makes the fitted model smaller. The other axis can have mo
|
|
|
289
289
|
|
|
290
290
|
The default orbital camera starts without `lowerBetaLimit` or `upperBetaLimit` and disables upside-down orbiting, whether or not the cube is enabled. ViewCube navigation keeps the camera up direction on world **+Y**, including top/bottom views and subsequent orbiting. Automatic loading preserves authored camera composition and navigation limits; an explicit unlocked cube action may relax incompatible orbit limits and adjust clipping/radius ranges to reach and fit the selected view. Locked cameras retain their view and limits.
|
|
291
291
|
|
|
292
|
-
Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. Babylon's native
|
|
292
|
+
Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. The default camera uses Babylon's native `zoomToMouseLocation`: pointing at an off-center detail shifts the target toward that region as you zoom in, with the ViewCube shown or hidden. Selecting any cube preset, including the current view, cancels pending wheel motion and recenters the target on the visible model's world-bounds center using the existing fit. This does not guarantee an exact surface point stays at the same screen pixel.
|
|
293
|
+
|
|
294
|
+
Babylon's native input applies a radius-proportional increment once, so the same wheel input moves less at a smaller radius. Input wakes idle rendering, which continues until native wheel inertia settles. Named imported orbital cameras retain their existing input settings, and Free/Universal wheel navigation remains unchanged. The internal orbital wheel sensitivity (`wheelDeltaPercentage`) is 0.015. Wheel sensitivity has no public attribute or config option.
|
|
293
295
|
|
|
294
296
|
The orbital minimum radius adapts to eligible triangle surfaces in the **current visible model assetContainer only**. Hidden, disabled, non-pickable, disposed or zero-visibility meshes do not contribute. Environment meshes are excluded from both hits and raycast traversal, and model replacement/removal cannot retain an obsolete geometry floor. Clear checked paths allow close inspection without a minimum based on overall model bounds.
|
|
295
297
|
|
|
@@ -499,6 +501,23 @@ Render settings are persisted in `localStorage` under the `pref-viewer/render-se
|
|
|
499
501
|
|
|
500
502
|
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.
|
|
501
503
|
|
|
504
|
+
## Environment map and scene lights
|
|
505
|
+
|
|
506
|
+
`options.ibl.url` accepts two formats, chosen by the URL's extension (query and hash ignored):
|
|
507
|
+
|
|
508
|
+
| Extension | Loaded as | Notes |
|
|
509
|
+
| --- | --- | --- |
|
|
510
|
+
| `.env` | Prefiltered Babylon environment (`CubeTexture.CreateFromPrefilteredData`) | No prefiltering in the browser; a 4K environment is typically under 1 MB. |
|
|
511
|
+
| anything else | Radiance HDR (`HDRCubeTexture`) | Prefiltered on load, which costs time and memory for large files. |
|
|
512
|
+
|
|
513
|
+
Either file is downloaded once, cached in IndexedDB, and reused across reloads.
|
|
514
|
+
|
|
515
|
+
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:
|
|
516
|
+
|
|
517
|
+
```json
|
|
518
|
+
{ "name": "Sun", "rotation": [-0.5, 0, 0, 0.866], "extras": { "shadowDarkness": 0.15 }, "extensions": { "KHR_lights_punctual": { "light": 0 } } }
|
|
519
|
+
```
|
|
520
|
+
|
|
502
521
|
## Runtime styles
|
|
503
522
|
|
|
504
523
|
Runtime styles can be passed through the `styles` attribute, the `styles` property setter, `setStyles(styles)`, or `loadConfig({ styles })`.
|
|
@@ -571,6 +590,7 @@ Focused regressions for this work live in `tests/babylonjs-selection-controller.
|
|
|
571
590
|
- `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.
|
|
572
591
|
- `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.
|
|
573
592
|
- `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
|
|
593
|
+
- `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.
|
|
574
594
|
- `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
|
|
575
595
|
- `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.
|
|
576
596
|
- `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.
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges native mouse-location wheel inertia to the viewer's render and navigation lifecycle.
|
|
3
|
+
* Babylon 9.11 keeps coupled pan/radius inertia on its wheel input, outside the public
|
|
4
|
+
* camera offsets. Keep that version-sensitive access here; Babylon still computes all zoom.
|
|
5
|
+
*/
|
|
6
|
+
export default class BabylonJSCameraWheelMotion {
|
|
7
|
+
/**
|
|
8
|
+
* Returns the active native mouse-location input, without creating or replacing inputs.
|
|
9
|
+
* @param {object|null|undefined} camera - Camera to inspect.
|
|
10
|
+
* @returns {object|null} Native input when its mouse-location mode is enabled.
|
|
11
|
+
*/
|
|
12
|
+
static #input(camera) {
|
|
13
|
+
const input = camera?.inputs?.attached?.mousewheel;
|
|
14
|
+
return input?.zoomToMouseLocation ? input : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detects the complete native inertia tail, including pan without a public radius offset.
|
|
19
|
+
* @param {object|null|undefined} camera - Camera to inspect.
|
|
20
|
+
* @returns {boolean} Whether native wheel motion is pending.
|
|
21
|
+
*/
|
|
22
|
+
static isMoving(camera) {
|
|
23
|
+
const input = this.#input(camera);
|
|
24
|
+
return Boolean(input && (Math.abs(input._zoomToMouseRadiusOffset || 0) > 0 || input._inertialPanning?.lengthSquared() > 0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stops coupled inward motion when geometry protection clamps the camera.
|
|
29
|
+
* @param {object|null|undefined} camera - Camera constrained by the geometry guard.
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*/
|
|
32
|
+
static stopInward(camera) {
|
|
33
|
+
if (this.#input(camera)?._zoomToMouseRadiusOffset > 0) {
|
|
34
|
+
this.cancel(camera);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cancels native wheel inertia and invalidates its old focal plane without altering orbit/pan inputs.
|
|
40
|
+
* @param {object|null|undefined} camera - Camera being reframed, deactivated or disposed.
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
static cancel(camera) {
|
|
44
|
+
const input = this.#input(camera);
|
|
45
|
+
if (!input) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof input._zoomToMouseRadiusOffset === "number") {
|
|
49
|
+
input._zoomToMouseRadiusOffset = 0;
|
|
50
|
+
}
|
|
51
|
+
input._inertialPanning?.setAll(0);
|
|
52
|
+
input._hitPlane = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Camera } from "@babylonjs/core/Cameras/camera.js";
|
|
|
2
2
|
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera.js";
|
|
3
3
|
import { Ray } from "@babylonjs/core/Culling/ray.js";
|
|
4
4
|
import { Vector3 } from "@babylonjs/core/Maths/math.vector.js";
|
|
5
|
+
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Maintains a geometry-dependent orbital zoom floor without moving the camera target.
|
|
@@ -245,6 +246,9 @@ export default class BabylonJSCameraZoomController {
|
|
|
245
246
|
const length = Math.max(basis.distance, this.#previousDistance);
|
|
246
247
|
const minimum = Math.max(this.#originalLimit ?? 0, this.#minimumDistance(basis, length) / basis.scale);
|
|
247
248
|
camera.lowerRadiusLimit = minimum;
|
|
249
|
+
if (camera.radius <= minimum) {
|
|
250
|
+
BabylonJSCameraWheelMotion.stopInward(camera);
|
|
251
|
+
}
|
|
248
252
|
if (camera.radius < minimum) {
|
|
249
253
|
camera.radius = minimum;
|
|
250
254
|
if (camera.inertialRadiusOffset > 0) {
|
|
@@ -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";
|
|
@@ -11,6 +11,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
11
|
import BabylonJSViewCubeController from "./babylonjs-view-cube-controller.js";
|
|
12
12
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
13
13
|
import BabylonJSCameraZoomController from "./babylonjs-camera-zoom-controller.js";
|
|
14
|
+
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
14
15
|
import BabylonJSSelectionController from "./babylonjs-selection-controller.js";
|
|
15
16
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
16
17
|
import I18n from "./localization/i18n.js";
|
|
@@ -838,7 +839,7 @@ export default class BabylonJSController {
|
|
|
838
839
|
*/
|
|
839
840
|
#isArcRotateCameraInMotion(camera) {
|
|
840
841
|
const EPSILON = 0.00001;
|
|
841
|
-
return Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
842
|
+
return BabylonJSCameraWheelMotion.isMoving(camera) || Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
842
843
|
}
|
|
843
844
|
|
|
844
845
|
/**
|
|
@@ -1076,6 +1077,7 @@ export default class BabylonJSController {
|
|
|
1076
1077
|
this.#camera.lowerBetaLimit = null;
|
|
1077
1078
|
this.#camera.allowUpsideDown = false;
|
|
1078
1079
|
this.#camera.wheelDeltaPercentage = BabylonJSController.config.camera.wheelDeltaPercentage;
|
|
1080
|
+
this.#camera.zoomToMouseLocation = true;
|
|
1079
1081
|
this.#camera.lowerRadiusLimit = null;
|
|
1080
1082
|
this.#camera.upperRadiusLimit = 20;
|
|
1081
1083
|
this.#camera.metadata = { locked: false };
|
|
@@ -1332,7 +1334,7 @@ export default class BabylonJSController {
|
|
|
1332
1334
|
* @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
|
|
1333
1335
|
* @description
|
|
1334
1336
|
* Lifecycle implemented here:
|
|
1335
|
-
* 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`).
|
|
1336
1338
|
* 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
|
|
1337
1339
|
* 3. Call `options.ibl.consumeCachedUrl(true)` to revoke temporary object URLs and clear `cachedUrl`.
|
|
1338
1340
|
* 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
|
|
@@ -1346,8 +1348,13 @@ export default class BabylonJSController {
|
|
|
1346
1348
|
let hdrTexture = null;
|
|
1347
1349
|
if (this.#options.ibl?.cachedUrl) {
|
|
1348
1350
|
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
+
}
|
|
1351
1358
|
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
1352
1359
|
hdrTexture = this.#hdrTexture.clone();
|
|
1353
1360
|
} else {
|
|
@@ -1584,7 +1591,7 @@ export default class BabylonJSController {
|
|
|
1584
1591
|
return generator.getLight()?.name === "PrefViewerDirLight";
|
|
1585
1592
|
});
|
|
1586
1593
|
|
|
1587
|
-
this.#containers.environment?.
|
|
1594
|
+
this.#containers.environment?.assetContainer?.lights.forEach((light) => {
|
|
1588
1595
|
// Only shadows for DirectionalLight and SpotLight types
|
|
1589
1596
|
if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
|
|
1590
1597
|
return;
|
|
@@ -1593,7 +1600,9 @@ export default class BabylonJSController {
|
|
|
1593
1600
|
const shadowGenerator = new ShadowGenerator(1024, light);
|
|
1594
1601
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
1595
1602
|
shadowGenerator.blurKernel = 16;
|
|
1596
|
-
|
|
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;
|
|
1597
1606
|
shadowGenerator.bias = 0.0005;
|
|
1598
1607
|
shadowGenerator.normalBias = 0.02;
|
|
1599
1608
|
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
@@ -2656,6 +2665,8 @@ export default class BabylonJSController {
|
|
|
2656
2665
|
cameraState.setSuccess(true);
|
|
2657
2666
|
}
|
|
2658
2667
|
}
|
|
2668
|
+
BabylonJSCameraWheelMotion.cancel(this.#scene.activeCamera);
|
|
2669
|
+
BabylonJSCameraWheelMotion.cancel(camera);
|
|
2659
2670
|
this.#applyShowroomCameraFraming(camera);
|
|
2660
2671
|
this.#fitDefaultCameraToModel(camera);
|
|
2661
2672
|
this.#scene.activeCamera?.detachControl();
|
|
@@ -3458,6 +3469,8 @@ export default class BabylonJSController {
|
|
|
3458
3469
|
* @returns {Promise<void>}
|
|
3459
3470
|
*/
|
|
3460
3471
|
async disable() {
|
|
3472
|
+
BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
|
|
3473
|
+
BabylonJSCameraWheelMotion.cancel(this.#camera);
|
|
3461
3474
|
this.#cameraZoom?.dispose();
|
|
3462
3475
|
this.#cameraZoom = null;
|
|
3463
3476
|
this.#viewCubeSuspended = true;
|
|
@@ -3,6 +3,7 @@ import { PrefViewerViewCubeStyles } from "./styles.js";
|
|
|
3
3
|
import I18n from "./localization/i18n.js";
|
|
4
4
|
import BabylonJSController from "./babylonjs-controller.js";
|
|
5
5
|
import BabylonJSCameraFraming from "./babylonjs-camera-framing.js";
|
|
6
|
+
import BabylonJSCameraWheelMotion from "./babylonjs-camera-wheel-motion.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {import("./babylonjs-camera-framing.js").FramingCamera} ViewCubeCamera
|
|
@@ -395,6 +396,7 @@ export default class BabylonJSViewCubeController {
|
|
|
395
396
|
* @returns {void}
|
|
396
397
|
*/
|
|
397
398
|
#clearInertia(camera) {
|
|
399
|
+
BabylonJSCameraWheelMotion.cancel(camera);
|
|
398
400
|
for (const key of ["inertialAlphaOffset", "inertialBetaOffset", "inertialRadiusOffset", "inertialPanningX", "inertialPanningY"]) {
|
|
399
401
|
if (key in camera) {
|
|
400
402
|
camera[key] = 0;
|