@preference-sl/pref-viewer 2.18.0-beta.0 → 2.18.0-beta.1

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
@@ -309,7 +309,7 @@ Increasing the factor makes the fitted model smaller. The other axis can have mo
309
309
 
310
310
  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.
311
311
 
312
- 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.
312
+ Use the mouse wheel over the 3D canvas to zoom an unlocked orbital camera. The default camera uses Babylon's native `zoomToMouseLocation` focused on the visible model surface under the pointer: the picked model point stays at the same screen pixel as you zoom, and the target moves toward it, with the ViewCube shown or hidden. Only visible, enabled, pickable model meshes supply the focus; environment geometry never does. When the pointer is not over the model, the focus falls back to Babylon's native plane through the current target. 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.
313
313
 
314
314
  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.
315
315
 
@@ -614,7 +614,7 @@ Focused regressions for this work live in `tests/babylonjs-selection-controller.
614
614
  - `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.
615
615
  - `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.
616
616
  - `src/babylonjs-camera-zoom-controller.js`: independent `BabylonJSCameraZoomController` for model-only dynamic radius protection and authored-floor restoration. It works without cube resources.
617
- - `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.
617
+ - `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, and for focusing the native mouse-location zoom on the model surface under the pointer. Babylon computes the zoom; this boundary isolates version-sensitive native input state.
618
618
  - `src/babylonjs-controller.js`: owns the scene, active camera, containers, native wheel settings and render scheduler; coordinates both controllers.
619
619
  - `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.
620
620
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.18.0-beta.0",
3
+ "version": "2.18.0-beta.1",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -4,6 +4,12 @@
4
4
  * camera offsets. Keep that version-sensitive access here; Babylon still computes all zoom.
5
5
  */
6
6
  export default class BabylonJSCameraWheelMotion {
7
+ /**
8
+ * Native focal lookups replaced by surface focus, keyed by the wheel input that owns them.
9
+ * @type {WeakMap<object, {original: (function(): object), hadOwn: boolean, getMeshes: function(): (Array<object>|null|undefined)}>}
10
+ */
11
+ static #patched = new WeakMap();
12
+
7
13
  /**
8
14
  * Returns the active native mouse-location input, without creating or replacing inputs.
9
15
  * @param {object|null|undefined} camera - Camera to inspect.
@@ -51,4 +57,69 @@ export default class BabylonJSCameraWheelMotion {
51
57
  input._inertialPanning?.setAll(0);
52
58
  input._hitPlane = null;
53
59
  }
60
+
61
+ /**
62
+ * Makes native mouse-location zoom focus on the model surface under the pointer.
63
+ * Babylon otherwise intersects the pointer ray with a plane through the camera target, so the
64
+ * visible surface drifts. Returning the picked point keeps Babylon's coupled pan an exact homothety
65
+ * around it. Falls back to the native lookup on a miss, without eligible meshes or with a screen offset.
66
+ * Idempotent: repeated calls only replace the mesh provider.
67
+ * @param {object|null|undefined} camera - Camera whose native wheel input is patched.
68
+ * @param {function(): (Array<object>|null|undefined)} getMeshes - Returns the current model meshes; the caller must exclude environment meshes.
69
+ * @returns {void}
70
+ */
71
+ static focusOnSurface(camera, getMeshes) {
72
+ const input = this.#input(camera);
73
+ if (!input || typeof getMeshes !== "function") {
74
+ return;
75
+ }
76
+ const state = this.#patched.get(input);
77
+ if (state) {
78
+ state.getMeshes = getMeshes;
79
+ return;
80
+ }
81
+ const entry = { original: input._getPosition, hadOwn: Object.prototype.hasOwnProperty.call(input, "_getPosition"), getMeshes };
82
+ this.#patched.set(input, entry);
83
+ input._getPosition = () => this.#surfacePoint(input, entry) ?? entry.original.call(input);
84
+ }
85
+
86
+ /**
87
+ * Restores the native focal lookup replaced by focusOnSurface.
88
+ * @param {object|null|undefined} camera - Camera whose wheel input was patched.
89
+ * @returns {void}
90
+ */
91
+ static releaseSurfaceFocus(camera) {
92
+ const input = camera?.inputs?.attached?.mousewheel;
93
+ const entry = input && this.#patched.get(input);
94
+ if (!entry) {
95
+ return;
96
+ }
97
+ if (entry.hadOwn) {
98
+ input._getPosition = entry.original;
99
+ } else {
100
+ delete input._getPosition;
101
+ }
102
+ this.#patched.delete(input);
103
+ }
104
+
105
+ /**
106
+ * Picks the eligible model mesh under the pointer.
107
+ * @param {object} input - Patched native wheel input.
108
+ * @param {{getMeshes: function(): (Array<object>|null|undefined)}} entry - Patch state.
109
+ * @returns {object|null} World-space surface point, or null when native behavior must apply.
110
+ */
111
+ static #surfacePoint(input, entry) {
112
+ const camera = input.camera;
113
+ const scene = camera?.getScene?.();
114
+ if (!scene || camera.targetScreenOffset?.x || camera.targetScreenOffset?.y) {
115
+ return null;
116
+ }
117
+ const meshes = new Set((entry.getMeshes() ?? []).filter((mesh) =>
118
+ !mesh.isDisposed() && mesh.isPickable && mesh.isEnabled() && mesh.isVisible && mesh.visibility > 0));
119
+ if (meshes.size === 0) {
120
+ return null;
121
+ }
122
+ const point = scene.pick(scene.pointerX, scene.pointerY, (mesh) => meshes.has(mesh), false, camera)?.pickedPoint;
123
+ return point && Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z) ? point.clone() : null;
124
+ }
54
125
  }
@@ -1092,6 +1092,19 @@ export default class BabylonJSController {
1092
1092
  this.#applyShowroomCameraFraming(this.#camera);
1093
1093
  this.#camera.attachControl(this.#canvas, true);
1094
1094
  this.#scene.activeCamera = this.#camera;
1095
+ this.#focusWheelZoomOnModel(this.#camera);
1096
+ }
1097
+
1098
+ /**
1099
+ * Makes native mouse-location wheel zoom focus on the visible model surface under the pointer.
1100
+ * Only meshes of the current visible model container are eligible; environment geometry never is.
1101
+ * Falls back to Babylon's target-plane focus when the pointer is not over the model.
1102
+ * @private
1103
+ * @param {import("@babylonjs/core").ArcRotateCamera} camera - Camera with native mouse-location zoom.
1104
+ * @returns {void}
1105
+ */
1106
+ #focusWheelZoomOnModel(camera) {
1107
+ BabylonJSCameraWheelMotion.focusOnSurface(camera, () => this.#containers.model?.state?.isVisible ? this.#containers.model.assetContainer?.meshes : null);
1095
1108
  }
1096
1109
 
1097
1110
  /**
@@ -3496,6 +3509,7 @@ export default class BabylonJSController {
3496
3509
  async disable() {
3497
3510
  BabylonJSCameraWheelMotion.cancel(this.#scene?.activeCamera);
3498
3511
  BabylonJSCameraWheelMotion.cancel(this.#camera);
3512
+ BabylonJSCameraWheelMotion.releaseSurfaceFocus(this.#camera);
3499
3513
  this.#cameraZoom?.dispose();
3500
3514
  this.#cameraZoom = null;
3501
3515
  this.#viewCubeSuspended = true;