@preference-sl/pref-viewer 2.17.0-beta.2 → 2.17.0-beta.3

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
@@ -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 input applies a radius-proportional increment once, so the same wheel input moves less at a smaller radius. Input wakes idle rendering. This applies to the default and selected unlocked orbital cameras independently of the cube; existing Free/Universal wheel navigation remains unchanged. The internal orbital wheel sensitivity (`wheelDeltaPercentage`) is 0.015. Wheel sensitivity has no public attribute or config option.
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
 
@@ -571,6 +573,7 @@ Focused regressions for this work live in `tests/babylonjs-selection-controller.
571
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.
572
574
  - `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
575
  - `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
576
+ - `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
577
  - `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
575
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.
576
579
  - `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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.17.0-beta.2",
3
+ "version": "2.17.0-beta.3",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -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) {
@@ -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 };
@@ -2656,6 +2658,8 @@ export default class BabylonJSController {
2656
2658
  cameraState.setSuccess(true);
2657
2659
  }
2658
2660
  }
2661
+ BabylonJSCameraWheelMotion.cancel(this.#scene.activeCamera);
2662
+ BabylonJSCameraWheelMotion.cancel(camera);
2659
2663
  this.#applyShowroomCameraFraming(camera);
2660
2664
  this.#fitDefaultCameraToModel(camera);
2661
2665
  this.#scene.activeCamera?.detachControl();
@@ -3458,6 +3462,8 @@ export default class BabylonJSController {
3458
3462
  * @returns {Promise<void>}
3459
3463
  */
3460
3464
  async disable() {
3465
+ BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
3466
+ BabylonJSCameraWheelMotion.cancel(this.#camera);
3461
3467
  this.#cameraZoom?.dispose();
3462
3468
  this.#cameraZoom = null;
3463
3469
  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;