@ericchen1990/pano-view 3.6.1 → 3.6.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
@@ -879,7 +879,7 @@ function TourPlayer() {
879
879
 
880
880
  The two deprecated auto-rotation handle methods (`startAutoRotate`, `stopAutoRotate`) remain for compatibility. Mouse, touch, and keyboard input are enabled by default — you do not need to render control components for ordinary viewing. Render `MouseControls`, `TouchControls`, or `KeyboardControls` only to override properties (the child replaces that channel's default instance). Tune shared behaviour through `controls` (`inertia`, `invert`, `bouncingLimits`, `fovSpeed`, `frictionStop`, `rotateDamping`, `zoomDamping`, and top-level `rotateSpeed` / `zoomSpeed`). Disable a channel with `controls.mouse` / `touch` / `keyboard` set to `false`, or pass an options object (including `enabled`) to override defaults without mounting a child. Auto-rotation is off until you render `AutoRotate`. A default context menu (Reset view / Enter fullscreen) is also mounted — see [`PanoContextMenu`](#panocontextmenu).
881
881
 
882
- Mouse, pen, and single-touch drags anchor the grabbed panorama point and update rotation once per render frame, with a short time-based filter (about 10ms of following delay during steady movement) to smooth uneven input samples. The grabbed point aligns exactly once movement settles. View limits and positions near the poles can restrict tracking. The default `rotateSpeed` of `0.35` gives 1:1 tracking; other values scale pointer displacement relative to it, `0` disables rotation, and `invert` reverses direction. The separate `rotateDamping` setting does not apply during anchored drags; release inertia is retained. Pinch and wheel zoom continue to follow the target FOV smoothly.
882
+ Mouse, pen, and single-touch drags anchor the grabbed panorama point and update rotation once per render frame, with a short time-based filter (about 10ms of following delay during steady movement) to smooth uneven input samples. The grabbed point aligns once movement settles, and releasing the pointer completes that alignment immediately. Flick inertia follows the latest pointer motion, so a pause before release does not continue an earlier flick. View limits and positions near the poles can restrict tracking. The default `rotateSpeed` of `0.35` gives 1:1 tracking; other values scale pointer displacement relative to it, `0` disables rotation, and `invert` reverses direction. The separate `rotateDamping` setting does not apply during anchored drags; release inertia is retained. Pinch and wheel zoom continue to follow the target FOV smoothly.
883
883
 
884
884
  Other rotation input and zoom update a target view that the camera follows smoothly. `rotateDamping` and `zoomDamping` control that following speed in seconds^-1 (defaults: `14` and `16` respectively); smaller positive values make the camera approach the target more gradually, while `0` disables smoothing for that axis. Both values must be non-negative finite numbers. Imperative `setView()` and `reset()` remain immediate.
885
885
 
package/README.zh-CN.md CHANGED
@@ -852,7 +852,7 @@ function TourPlayer() {
852
852
 
853
853
  两个已废弃的自动旋转方法(`startAutoRotate`、`stopAutoRotate`)继续保留以兼容。鼠标、触控与键盘输入默认启用 — 普通浏览无需再渲染控件组件。仅当需要覆盖属性时渲染 `MouseControls`、`TouchControls` 或 `KeyboardControls`(子组件会替换该通道的默认实例)。通过 `controls` 调节共享行为(`inertia`、`invert`、`bouncingLimits`、`fovSpeed`、`frictionStop`、`rotateDamping`、`zoomDamping`,以及顶层的 `rotateSpeed` / `zoomSpeed`)。将 `controls.mouse` / `touch` / `keyboard` 设为 `false` 可关闭某通道,或传入选项对象(含 `enabled`)在不挂载子组件的情况下覆盖默认值。自动旋转在渲染 `AutoRotate` 之前处于关闭状态。默认右键菜单(重置视图 / 进入全屏)也会挂载 — 见 [`PanoContextMenu`](#panocontextmenu)。
854
854
 
855
- 鼠标、笔和单指拖拽会锚定按下的全景点,并在每个渲染帧更新旋转;使用短暂的时间平滑吸收输入采样的波动(匀速移动时约 10ms 跟随延迟),停住并收敛后按下点会准确对齐指针;视角边界或极点附近可能限制跟随。`rotateSpeed` 默认 `0.35` 表示 1:1 跟随,其他值按其相对倍率调整指针位移,`0` 禁止旋转;`invert` 反转方向。双指缩放和滚轮继续更新目标 FOV,相机平滑跟随。拖拽释放后保留惯性。`rotateDamping` 与 `zoomDamping` 控制跟随速度(旋转平滑不作用于锚定拖拽期间),单位为秒⁻¹(默认分别为 `14` 与 `16`);较小的正值使相机更缓慢地接近目标视角,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
855
+ 鼠标、笔和单指拖拽会锚定按下的全景点,并在每个渲染帧更新旋转;使用短暂的时间平滑吸收输入采样的波动(匀速移动时约 10ms 跟随延迟)。指针停住并收敛后,按下点会与指针对齐;松手时也会立刻完成这次对齐。甩动惯性只跟随最近一段指针运动,松手前如果已经停住,不会把更早的速度继续带出去。视角边界或极点附近可能限制跟随。`rotateSpeed` 默认 `0.35` 表示 1:1 跟随,其他值按其相对倍率调整指针位移,`0` 禁止旋转;`invert` 反转方向。双指缩放和滚轮继续更新目标 FOV,相机平滑跟随。拖拽释放后保留惯性。`rotateDamping` 与 `zoomDamping` 控制跟随速度(旋转平滑不作用于锚定拖拽期间),单位为秒⁻¹(默认分别为 `14` 与 `16`);较小的正值使相机更缓慢地接近目标视角,`0` 表示该轴不做平滑。两者必须为非负有限数。命令式 `setView()` 与 `reset()` 仍为即时生效。
856
856
 
857
857
  画布光标默认为 `grab`,鼠标拖拽时为 `grabbing`,悬停在可交互热点上为 `pointer`,拖拽热点时为 `move`。可通过 `cursors`(`default`、`dragging`、`hotspot`、`hotspotDragging`)覆盖,或传 `cursors={false}` 保持画布光标不变。热点可设置 `cursor`,仅替换该热点的悬停光标。
858
858
 
package/dist/index.cjs CHANGED
@@ -2896,7 +2896,12 @@ var PanoramaViewRuntime = (0, import_react3.forwardRef)(function PanoramaViewRun
2896
2896
  }
2897
2897
  }
2898
2898
  markDirty();
2899
- if (applyOptions?.recordVelocity && !gyroControlsView) {
2899
+ const releaseVelocity = applyOptions?.velocity;
2900
+ if (!gyroControlsView && releaseVelocity && Number.isFinite(releaseVelocity.yaw) && Number.isFinite(releaseVelocity.pitch)) {
2901
+ yawVelocityRef.current = releaseVelocity.yaw;
2902
+ pitchVelocityRef.current = releaseVelocity.pitch;
2903
+ lastVelocitySampleAtRef.current = performance.now();
2904
+ } else if (applyOptions?.recordVelocity && !gyroControlsView) {
2900
2905
  const now = performance.now();
2901
2906
  const previousAt = lastVelocitySampleAtRef.current;
2902
2907
  const elapsed = previousAt === null ? 1 / 60 : Math.max((now - previousAt) / 1e3, 1 / 120);
@@ -3736,6 +3741,26 @@ var import_react9 = require("react");
3736
3741
  var import_three2 = require("three");
3737
3742
  var DEFAULT_POINTER_DRAG_SPEED = 0.35;
3738
3743
  var POINTER_FOLLOW_TIME_SECONDS = 0.01;
3744
+ var MIN_VELOCITY_SAMPLE_SECONDS = 1 / 120;
3745
+ function pointerReleaseVelocity(samples, releasedAt, solve, maxAgeMs = POINTER_VELOCITY_MAX_AGE_MS) {
3746
+ if (samples.length < 2) return { yaw: 0, pitch: 0 };
3747
+ const newest = samples[samples.length - 1];
3748
+ if (releasedAt < newest.at || releasedAt - newest.at > maxAgeMs) return { yaw: 0, pitch: 0 };
3749
+ let oldest = newest;
3750
+ for (let index = samples.length - 1; index >= 0; index -= 1) {
3751
+ const sample = samples[index];
3752
+ if (releasedAt - sample.at <= maxAgeMs) oldest = sample;
3753
+ else break;
3754
+ }
3755
+ if (oldest === newest) return { yaw: 0, pitch: 0 };
3756
+ const elapsed = Math.max((newest.at - oldest.at) / 1e3, MIN_VELOCITY_SAMPLE_SECONDS);
3757
+ const from = solve(oldest);
3758
+ const to = solve(newest);
3759
+ return {
3760
+ yaw: normalizeViewYaw(to.yaw - from.yaw) / elapsed,
3761
+ pitch: (to.pitch - from.pitch) / elapsed
3762
+ };
3763
+ }
3739
3764
  var PointerDragAnchor = class {
3740
3765
  constructor(start, origin, rect, projection) {
3741
3766
  this.start = start;
@@ -3744,12 +3769,18 @@ var PointerDragAnchor = class {
3744
3769
  this.projection = projection;
3745
3770
  this.local = new import_three2.Vector3();
3746
3771
  this.rotation = new import_three2.Euler(0, 0, 0, "YXZ");
3747
- this.anchor = this.ray(start).clone().applyEuler(this.rotation.set(
3772
+ const startRay = this.ray(start).clone();
3773
+ this.baseAzimuth = Math.atan2(startRay.x, -startRay.z);
3774
+ this.baseElevation = Math.atan2(startRay.y, -startRay.z);
3775
+ this.anchor = startRay.clone().applyEuler(this.rotation.set(
3748
3776
  import_three2.MathUtils.degToRad(origin.pitch),
3749
3777
  import_three2.MathUtils.degToRad(-origin.yaw),
3750
3778
  0,
3751
3779
  "YXZ"
3752
3780
  ));
3781
+ const originPitch = import_three2.MathUtils.degToRad(origin.pitch);
3782
+ const roots = this.pitchRoots(startRay, originPitch);
3783
+ this.useSupplementary = Math.abs(roots.supplementary - originPitch) + 1e-6 < Math.abs(roots.principal - originPitch);
3753
3784
  }
3754
3785
  ray(point) {
3755
3786
  return this.local.set(
@@ -3758,6 +3789,21 @@ var PointerDragAnchor = class {
3758
3789
  0.5
3759
3790
  ).applyMatrix4(this.projection.projectionMatrixInverse).normalize();
3760
3791
  }
3792
+ /** Pitch roots of the level-camera constraint. The supplementary root is unwrapped toward the press. */
3793
+ pitchRoots(ray, originPitch) {
3794
+ const latitude = Math.asin(import_three2.MathUtils.clamp(
3795
+ this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12),
3796
+ -1,
3797
+ 1
3798
+ ));
3799
+ const offset = Math.atan2(ray.y, -ray.z);
3800
+ const principal = latitude - offset;
3801
+ const turned = Math.PI - latitude - offset;
3802
+ return {
3803
+ principal,
3804
+ supplementary: originPitch + Math.atan2(Math.sin(turned - originPitch), Math.cos(turned - originPitch))
3805
+ };
3806
+ }
3761
3807
  solve(point, speed = DEFAULT_POINTER_DRAG_SPEED, invert = false) {
3762
3808
  const multiplier = (Number.isFinite(speed) && speed >= 0 ? speed : DEFAULT_POINTER_DRAG_SPEED) / DEFAULT_POINTER_DRAG_SPEED * (invert ? -1 : 1);
3763
3809
  if (multiplier === 0) return { yaw: 0, pitch: 0 };
@@ -3765,22 +3811,27 @@ var PointerDragAnchor = class {
3765
3811
  x: this.start.x + (point.x - this.start.x) * multiplier,
3766
3812
  y: this.start.y + (point.y - this.start.y) * multiplier
3767
3813
  });
3768
- const latitude = Math.asin(import_three2.MathUtils.clamp(this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12), -1, 1));
3769
- const offset = Math.atan2(ray.y, -ray.z);
3770
3814
  const originPitch = import_three2.MathUtils.degToRad(this.origin.pitch);
3771
- const candidates = [latitude - offset, Math.PI - latitude - offset, -Math.PI - latitude - offset];
3772
- let pitch = candidates[0];
3773
- let distance = Infinity;
3774
- for (const candidate of candidates) {
3775
- const bounded = import_three2.MathUtils.clamp(candidate, -Math.PI / 2, Math.PI / 2);
3776
- const nextDistance = Math.abs(candidate - bounded) * 10 + Math.abs(bounded - originPitch);
3777
- if (nextDistance < distance) {
3778
- pitch = bounded;
3779
- distance = nextDistance;
3780
- }
3781
- }
3782
- const z = Math.sin(pitch) * ray.y + Math.cos(pitch) * ray.z;
3783
- const yaw = import_three2.MathUtils.radToDeg(Math.atan2(this.anchor.x, -this.anchor.z) - Math.atan2(ray.x, -z));
3815
+ const roots = this.pitchRoots(ray, originPitch);
3816
+ const exactPitch = import_three2.MathUtils.clamp(
3817
+ this.useSupplementary ? roots.supplementary : roots.principal,
3818
+ -Math.PI / 2,
3819
+ Math.PI / 2
3820
+ );
3821
+ const stablePitch = originPitch - (Math.atan2(ray.y, -ray.z) - this.baseElevation);
3822
+ const fromPole = Math.asin(Math.min(1, Math.hypot(this.anchor.x, this.anchor.z)));
3823
+ const pointerLock = import_three2.MathUtils.smoothstep(fromPole, import_three2.MathUtils.degToRad(8), import_three2.MathUtils.degToRad(18));
3824
+ const pitchTurn = Math.atan2(Math.sin(exactPitch - stablePitch), Math.cos(exactPitch - stablePitch));
3825
+ const pitch = import_three2.MathUtils.clamp(
3826
+ pointerLock >= 1 ? exactPitch : stablePitch + pointerLock * pitchTurn,
3827
+ -Math.PI / 2,
3828
+ Math.PI / 2
3829
+ );
3830
+ const pitchedZ = Math.sin(exactPitch) * ray.y + Math.cos(exactPitch) * ray.z;
3831
+ const exactYaw = Math.atan2(this.anchor.x, -this.anchor.z) - Math.atan2(ray.x, -pitchedZ);
3832
+ const stableYaw = import_three2.MathUtils.degToRad(this.origin.yaw) + Math.atan2(ray.x, -ray.z) - this.baseAzimuth;
3833
+ const yawTurn = Math.atan2(Math.sin(exactYaw - stableYaw), Math.cos(exactYaw - stableYaw));
3834
+ const yaw = import_three2.MathUtils.radToDeg(stableYaw + pointerLock * yawTurn);
3784
3835
  return { yaw: normalizeViewYaw(yaw - this.origin.yaw), pitch: import_three2.MathUtils.radToDeg(pitch) - this.origin.pitch };
3785
3836
  }
3786
3837
  };
@@ -3793,8 +3844,11 @@ function createPointerDrag(camera, element, runtime, source) {
3793
3844
  let lastPoint;
3794
3845
  let fresh = false;
3795
3846
  let lastFrameDelta = 1 / 60;
3847
+ let samples = [];
3848
+ let gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3849
+ let gestureInvert = false;
3796
3850
  const key = (rect) => `${rect.left},${rect.top},${rect.width},${rect.height},${camera.projectionMatrix.elements.join(",")}`;
3797
- const start = (point) => {
3851
+ const place = (point) => {
3798
3852
  const handle = runtime();
3799
3853
  displayed = { ...point };
3800
3854
  lastPoint = { ...point };
@@ -3806,69 +3860,113 @@ function createPointerDrag(camera, element, runtime, source) {
3806
3860
  projectionKey = key(rect);
3807
3861
  anchor = new PointerDragAnchor(point, handle.getView(), rect, camera.clone());
3808
3862
  };
3809
- const apply = (point, previous, speed, invert, recordVelocity) => {
3863
+ const remember = (point, at) => {
3864
+ samples.push({ x: point.x, y: point.y, at });
3865
+ const cutoff = at - POINTER_VELOCITY_MAX_AGE_MS;
3866
+ while (samples.length > 2 && samples[0].at < cutoff) samples.shift();
3867
+ };
3868
+ const apply = (point, previous, speed, invert, recordVelocity, releaseVelocity) => {
3810
3869
  const handle = runtime();
3811
3870
  if (!handle) return;
3812
3871
  const rect = element.getBoundingClientRect();
3813
3872
  if (anchor && key(rect) !== projectionKey) {
3814
3873
  const queued = pending;
3815
- start(displayed ?? previous);
3874
+ place(displayed ?? previous);
3816
3875
  pending = queued;
3817
3876
  }
3877
+ const options = {
3878
+ recordVelocity: releaseVelocity ? false : recordVelocity,
3879
+ source,
3880
+ ...releaseVelocity ? { velocity: releaseVelocity } : {}
3881
+ };
3818
3882
  if (anchor) {
3819
3883
  handle.applyViewDelta(anchor.solve(point, speed, invert), {
3820
- pointerOrigin: anchor.origin,
3821
- recordVelocity,
3822
- source
3884
+ ...options,
3885
+ pointerOrigin: anchor.origin
3823
3886
  });
3824
3887
  } else {
3825
3888
  handle.applyViewDelta({
3826
3889
  yaw: -(point.x - previous.x) * 360 * speed / Math.max(rect.width, 1) * (invert ? -1 : 1),
3827
3890
  pitch: (point.y - previous.y) * 180 * speed / Math.max(rect.height, 1) * (invert ? -1 : 1)
3828
- }, { recordVelocity, source });
3891
+ }, options);
3829
3892
  }
3830
3893
  };
3894
+ const valid = () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked();
3895
+ const clear = () => {
3896
+ anchor = void 0;
3897
+ revision = -1;
3898
+ pending = void 0;
3899
+ displayed = void 0;
3900
+ samples = [];
3901
+ };
3831
3902
  return {
3832
- start,
3833
- valid: () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked(),
3834
- clear: () => {
3835
- anchor = void 0;
3836
- revision = -1;
3837
- pending = void 0;
3838
- displayed = void 0;
3903
+ start(point, at = performance.now()) {
3904
+ gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3905
+ gestureInvert = false;
3906
+ samples = [];
3907
+ remember(point, at);
3908
+ place(point);
3839
3909
  },
3910
+ valid,
3911
+ clear,
3840
3912
  suspend: () => {
3841
3913
  anchor = void 0;
3842
3914
  pending = void 0;
3843
3915
  displayed = void 0;
3916
+ samples = [];
3844
3917
  },
3845
- move(point, previous, speed, invert) {
3918
+ move(point, previous, speed, invert, at = performance.now()) {
3846
3919
  fresh = true;
3920
+ gestureSpeed = speed;
3921
+ gestureInvert = invert;
3922
+ remember(point, at);
3847
3923
  pending = { point, previous: lastPoint ?? previous, speed, invert };
3848
3924
  },
3849
- flush(deltaSeconds = lastFrameDelta, immediate = false) {
3850
- lastFrameDelta = deltaSeconds;
3925
+ flush(deltaSeconds = lastFrameDelta, immediate = false, releaseVelocity) {
3926
+ if (deltaSeconds > 0) lastFrameDelta = deltaSeconds;
3851
3927
  if (!pending) return;
3852
3928
  const sample = pending;
3853
3929
  if (!anchor || immediate) {
3854
3930
  pending = void 0;
3855
- apply(sample.point, sample.previous, sample.speed, sample.invert, fresh);
3931
+ apply(sample.point, sample.previous, sample.speed, sample.invert, fresh, releaseVelocity);
3856
3932
  fresh = false;
3857
3933
  lastPoint = sample.point;
3858
3934
  displayed = sample.point;
3859
3935
  return;
3860
3936
  }
3937
+ const dt = deltaSeconds > 0 ? deltaSeconds : lastFrameDelta;
3861
3938
  const previousDisplay = displayed ?? sample.previous;
3862
3939
  const dx = sample.point.x - previousDisplay.x;
3863
3940
  const dy = sample.point.y - previousDisplay.y;
3864
3941
  const distance = Math.hypot(dx, dy);
3865
- const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + deltaSeconds) : 0;
3942
+ const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + dt) : 0;
3866
3943
  const point = { x: sample.point.x - dx * ratio, y: sample.point.y - dy * ratio };
3867
3944
  apply(point, sample.previous, sample.speed, sample.invert, fresh);
3868
3945
  fresh = false;
3869
3946
  lastPoint = sample.point;
3870
3947
  displayed = point;
3871
3948
  if (ratio === 0) pending = void 0;
3949
+ },
3950
+ release() {
3951
+ const handle = runtime();
3952
+ if (!handle || !valid()) {
3953
+ clear();
3954
+ return;
3955
+ }
3956
+ const newestAt = samples[samples.length - 1]?.at ?? 0;
3957
+ const velocity = pointerReleaseVelocity(samples, Math.max(performance.now(), newestAt), (point) => {
3958
+ if (anchor) return anchor.solve(point, gestureSpeed, gestureInvert);
3959
+ const origin = samples[0] ?? point;
3960
+ const rect = element.getBoundingClientRect();
3961
+ const sign = gestureInvert ? -1 : 1;
3962
+ return {
3963
+ yaw: -(point.x - origin.x) * 360 * gestureSpeed / Math.max(rect.width, 1) * sign,
3964
+ pitch: (point.y - origin.y) * 180 * gestureSpeed / Math.max(rect.height, 1) * sign
3965
+ };
3966
+ });
3967
+ if (pending) this.flush(lastFrameDelta, true, velocity);
3968
+ else handle.applyViewDelta({}, { velocity, source });
3969
+ clear();
3872
3970
  }
3873
3971
  };
3874
3972
  }
@@ -3884,6 +3982,9 @@ var BUTTON_FLAG = {
3884
3982
  function resolveSpeed2(value, fallback) {
3885
3983
  return Number.isFinite(value) && value >= 0 ? value : fallback;
3886
3984
  }
3985
+ function pointerAt(event) {
3986
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
3987
+ }
3887
3988
  function resolveButtons(buttons) {
3888
3989
  const list = buttons && buttons.length > 0 ? buttons : ["left"];
3889
3990
  return new Set(list.map((button) => BUTTON_FLAG[button]));
@@ -3970,7 +4071,7 @@ function MouseControls({
3970
4071
  });
3971
4072
  element.setPointerCapture?.(event.pointerId);
3972
4073
  syncActive();
3973
- drag.start({ x: event.clientX, y: event.clientY });
4074
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
3974
4075
  };
3975
4076
  const onPointerMove = (event) => {
3976
4077
  if (!isMouseLike(event)) {
@@ -3991,15 +4092,15 @@ function MouseControls({
3991
4092
  };
3992
4093
  pointersRef.current.set(event.pointerId, next);
3993
4094
  const speed = resolveSpeed2(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED2);
3994
- drag.move(next, previous, speed, optionsRef.current.invert);
4095
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
3995
4096
  };
3996
4097
  const releasePointer = (event) => {
3997
4098
  if (!pointersRef.current.has(event.pointerId)) {
3998
4099
  return;
3999
4100
  }
4000
- if (event.type === "pointerup") drag.flush();
4101
+ if (event.type === "pointerup") drag.release();
4102
+ else drag.clear();
4001
4103
  pointersRef.current.delete(event.pointerId);
4002
- drag.clear();
4003
4104
  if (element.hasPointerCapture?.(event.pointerId)) {
4004
4105
  element.releasePointerCapture?.(event.pointerId);
4005
4106
  }
@@ -5642,6 +5743,9 @@ function notifyScenesHost(host) {
5642
5743
  var import_fiber8 = require("@react-three/fiber");
5643
5744
  var import_react18 = require("react");
5644
5745
  var DEFAULT_ROTATE_SPEED3 = 0.35;
5746
+ function pointerAt2(event) {
5747
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
5748
+ }
5645
5749
  function resolveSpeed3(value, fallback) {
5646
5750
  return Number.isFinite(value) && value >= 0 ? value : fallback;
5647
5751
  }
@@ -5710,7 +5814,7 @@ function TouchControls({
5710
5814
  });
5711
5815
  element.setPointerCapture?.(event.pointerId);
5712
5816
  syncActive();
5713
- drag.start({ x: event.clientX, y: event.clientY });
5817
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5714
5818
  if (pointersRef.current.size >= 2) {
5715
5819
  drag.suspend();
5716
5820
  const [first, second] = Array.from(pointersRef.current.values());
@@ -5757,15 +5861,16 @@ function TouchControls({
5757
5861
  return;
5758
5862
  }
5759
5863
  const speed = resolveSpeed3(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED3);
5760
- drag.move(next, previous, speed, optionsRef.current.invert);
5864
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5761
5865
  };
5762
5866
  const releasePointer = (event) => {
5763
5867
  if (!pointersRef.current.has(event.pointerId)) {
5764
5868
  return;
5765
5869
  }
5766
- if (event.type === "pointerup") drag.flush();
5870
+ const pinch = pointersRef.current.size >= 2;
5871
+ if (event.type === "pointerup" && !pinch) drag.release();
5872
+ else drag.clear();
5767
5873
  pointersRef.current.delete(event.pointerId);
5768
- drag.clear();
5769
5874
  if (element.hasPointerCapture?.(event.pointerId)) {
5770
5875
  element.releasePointerCapture?.(event.pointerId);
5771
5876
  }
@@ -5774,7 +5879,7 @@ function TouchControls({
5774
5879
  }
5775
5880
  syncActive(event.type !== "pointerup");
5776
5881
  const remaining = pointersRef.current.values().next().value;
5777
- if (remaining) drag.start(remaining);
5882
+ if (remaining) drag.start(remaining, pointerAt2(event));
5778
5883
  };
5779
5884
  element.addEventListener("pointerdown", onPointerDown);
5780
5885
  element.addEventListener("pointermove", onPointerMove);