From f75d4b1660f851d06691babc953f8f744e014dad Mon Sep 17 00:00:00 2001 From: ShaMan123 Date: Tue, 21 Jul 2026 07:50:36 +0300 Subject: [PATCH] fix(camera): zoom decay --- .../core/src/core/Worlds/src/simple-camera.ts | 36 ++++++++++++++++--- .../core/src/core/Worlds/src/simple-world.ts | 12 ++----- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/core/src/core/Worlds/src/simple-camera.ts b/packages/core/src/core/Worlds/src/simple-camera.ts index ed0cee9ce..c4bc99aef 100644 --- a/packages/core/src/core/Worlds/src/simple-camera.ts +++ b/packages/core/src/core/Worlds/src/simple-camera.ts @@ -8,6 +8,36 @@ import { ModelIdMap, } from "../../../fragments"; +/** + * Move the orbit point to an anchored surface without letting the camera + * jump or the zoom decay. + * + * `CameraControls.setOrbitPoint` internally calls `dollyTo(distance)`, which + * clamps the orbit radius to `[minDistance, maxDistance]`. If the anchored + * surface sits closer than `minDistance`, that clamp would snap the camera + * backwards, so the anchor has to be placed with a near-zero `minDistance`. + * + * But `minDistance` is also what stops the zoom from decaying: with + * `dollyToCursor` + `infinityDolly` the wheel step is proportional to the + * orbit radius, and the constant-speed "infinity" push only kicks in once the + * radius reaches `minDistance`. Leaving `minDistance` near zero (as dynamic + * anchoring used to, globally) let the radius shrink toward zero on every + * zoom-in, so each notch moved exponentially less — the zoom felt like it was + * grinding to a halt until a press re-anchored it. So we drop `minDistance` + * only for the placement itself and restore it immediately, keeping the real + * `minDistance` in force for the wheel. + */ +export function setOrbitPoint(controls: CameraControls, point: THREE.Vector3) { + const { minDistance } = controls; + controls.minDistance = 0.01; + controls.setOrbitPoint( + point.x, + point.y, + point.z, + ); + controls.minDistance = minDistance; +} + /** * A basic camera that uses [yomotsu's cameracontrols](https://github.com/yomotsu/camera-controls) to control the camera in 2D and 3D. Check out it's API to find out what features it offers. */ @@ -150,11 +180,7 @@ export class SimpleCamera extends BaseCamera implements Updateable, Disposable { async setOrbitToItems(items?: ModelIdMap) { const sphere = await this.getItemsBounding(items); - this.controls.setOrbitPoint( - sphere.center.x, - sphere.center.y, - sphere.center.z, - ); + setOrbitPoint(this.controls, sphere.center); } /** {@link Updateable.update} */ diff --git a/packages/core/src/core/Worlds/src/simple-world.ts b/packages/core/src/core/Worlds/src/simple-world.ts index 9debc3f41..d6292aa00 100644 --- a/packages/core/src/core/Worlds/src/simple-world.ts +++ b/packages/core/src/core/Worlds/src/simple-world.ts @@ -11,7 +11,7 @@ import { Updateable, } from "../../Types"; import { Disposer } from "../../Disposer"; -import { Worlds } from ".."; +import { setOrbitPoint, Worlds } from ".."; import { Raycasters } from "../../Raycasters"; /** @@ -70,7 +70,6 @@ export class SimpleWorld< ); } if (value) { - if (this.camera.controls) this.camera.controls.minDistance = 0.01; container.addEventListener("pointerdown", this.onPointerDown); } else { container.removeEventListener("pointerdown", this.onPointerDown); @@ -120,11 +119,7 @@ export class SimpleWorld< // snapping around as the pointer crosses gaps between objects. const planePoint = this.getPlaneAnchor(caster, position); if (planePoint) { - this.camera.controls.setOrbitPoint( - planePoint.x, - planePoint.y, - planePoint.z, - ); + setOrbitPoint(this.camera.controls, planePoint); } // Then refine to the real geometry depth once the (asynchronous) pick @@ -136,8 +131,7 @@ export class SimpleWorld< const isStale = requestId !== this._anchorRequestId; if (isStale || this.isDisposing || !this._dynamicAnchor) return; if (!result?.point || !this.camera.hasCameraControls()) return; - const { x, y, z } = result.point; - this.camera.controls.setOrbitPoint(x, y, z); + setOrbitPoint(this.camera.controls, result.point); }); };