@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/dist/index.mjs CHANGED
@@ -2842,7 +2842,12 @@ var PanoramaViewRuntime = forwardRef(function PanoramaViewRuntime2({ initialView
2842
2842
  }
2843
2843
  }
2844
2844
  markDirty();
2845
- if (applyOptions?.recordVelocity && !gyroControlsView) {
2845
+ const releaseVelocity = applyOptions?.velocity;
2846
+ if (!gyroControlsView && releaseVelocity && Number.isFinite(releaseVelocity.yaw) && Number.isFinite(releaseVelocity.pitch)) {
2847
+ yawVelocityRef.current = releaseVelocity.yaw;
2848
+ pitchVelocityRef.current = releaseVelocity.pitch;
2849
+ lastVelocitySampleAtRef.current = performance.now();
2850
+ } else if (applyOptions?.recordVelocity && !gyroControlsView) {
2846
2851
  const now = performance.now();
2847
2852
  const previousAt = lastVelocitySampleAtRef.current;
2848
2853
  const elapsed = previousAt === null ? 1 / 60 : Math.max((now - previousAt) / 1e3, 1 / 120);
@@ -3690,6 +3695,26 @@ import { useContext as useContext5, useEffect as useEffect5, useRef as useRef5 }
3690
3695
  import { Euler as Euler2, MathUtils as MathUtils2, PerspectiveCamera as PerspectiveCamera2, Vector3 } from "three";
3691
3696
  var DEFAULT_POINTER_DRAG_SPEED = 0.35;
3692
3697
  var POINTER_FOLLOW_TIME_SECONDS = 0.01;
3698
+ var MIN_VELOCITY_SAMPLE_SECONDS = 1 / 120;
3699
+ function pointerReleaseVelocity(samples, releasedAt, solve, maxAgeMs = POINTER_VELOCITY_MAX_AGE_MS) {
3700
+ if (samples.length < 2) return { yaw: 0, pitch: 0 };
3701
+ const newest = samples[samples.length - 1];
3702
+ if (releasedAt < newest.at || releasedAt - newest.at > maxAgeMs) return { yaw: 0, pitch: 0 };
3703
+ let oldest = newest;
3704
+ for (let index = samples.length - 1; index >= 0; index -= 1) {
3705
+ const sample = samples[index];
3706
+ if (releasedAt - sample.at <= maxAgeMs) oldest = sample;
3707
+ else break;
3708
+ }
3709
+ if (oldest === newest) return { yaw: 0, pitch: 0 };
3710
+ const elapsed = Math.max((newest.at - oldest.at) / 1e3, MIN_VELOCITY_SAMPLE_SECONDS);
3711
+ const from = solve(oldest);
3712
+ const to = solve(newest);
3713
+ return {
3714
+ yaw: normalizeViewYaw(to.yaw - from.yaw) / elapsed,
3715
+ pitch: (to.pitch - from.pitch) / elapsed
3716
+ };
3717
+ }
3693
3718
  var PointerDragAnchor = class {
3694
3719
  constructor(start, origin, rect, projection) {
3695
3720
  this.start = start;
@@ -3698,12 +3723,18 @@ var PointerDragAnchor = class {
3698
3723
  this.projection = projection;
3699
3724
  this.local = new Vector3();
3700
3725
  this.rotation = new Euler2(0, 0, 0, "YXZ");
3701
- this.anchor = this.ray(start).clone().applyEuler(this.rotation.set(
3726
+ const startRay = this.ray(start).clone();
3727
+ this.baseAzimuth = Math.atan2(startRay.x, -startRay.z);
3728
+ this.baseElevation = Math.atan2(startRay.y, -startRay.z);
3729
+ this.anchor = startRay.clone().applyEuler(this.rotation.set(
3702
3730
  MathUtils2.degToRad(origin.pitch),
3703
3731
  MathUtils2.degToRad(-origin.yaw),
3704
3732
  0,
3705
3733
  "YXZ"
3706
3734
  ));
3735
+ const originPitch = MathUtils2.degToRad(origin.pitch);
3736
+ const roots = this.pitchRoots(startRay, originPitch);
3737
+ this.useSupplementary = Math.abs(roots.supplementary - originPitch) + 1e-6 < Math.abs(roots.principal - originPitch);
3707
3738
  }
3708
3739
  ray(point) {
3709
3740
  return this.local.set(
@@ -3712,6 +3743,21 @@ var PointerDragAnchor = class {
3712
3743
  0.5
3713
3744
  ).applyMatrix4(this.projection.projectionMatrixInverse).normalize();
3714
3745
  }
3746
+ /** Pitch roots of the level-camera constraint. The supplementary root is unwrapped toward the press. */
3747
+ pitchRoots(ray, originPitch) {
3748
+ const latitude = Math.asin(MathUtils2.clamp(
3749
+ this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12),
3750
+ -1,
3751
+ 1
3752
+ ));
3753
+ const offset = Math.atan2(ray.y, -ray.z);
3754
+ const principal = latitude - offset;
3755
+ const turned = Math.PI - latitude - offset;
3756
+ return {
3757
+ principal,
3758
+ supplementary: originPitch + Math.atan2(Math.sin(turned - originPitch), Math.cos(turned - originPitch))
3759
+ };
3760
+ }
3715
3761
  solve(point, speed = DEFAULT_POINTER_DRAG_SPEED, invert = false) {
3716
3762
  const multiplier = (Number.isFinite(speed) && speed >= 0 ? speed : DEFAULT_POINTER_DRAG_SPEED) / DEFAULT_POINTER_DRAG_SPEED * (invert ? -1 : 1);
3717
3763
  if (multiplier === 0) return { yaw: 0, pitch: 0 };
@@ -3719,22 +3765,27 @@ var PointerDragAnchor = class {
3719
3765
  x: this.start.x + (point.x - this.start.x) * multiplier,
3720
3766
  y: this.start.y + (point.y - this.start.y) * multiplier
3721
3767
  });
3722
- const latitude = Math.asin(MathUtils2.clamp(this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12), -1, 1));
3723
- const offset = Math.atan2(ray.y, -ray.z);
3724
3768
  const originPitch = MathUtils2.degToRad(this.origin.pitch);
3725
- const candidates = [latitude - offset, Math.PI - latitude - offset, -Math.PI - latitude - offset];
3726
- let pitch = candidates[0];
3727
- let distance = Infinity;
3728
- for (const candidate of candidates) {
3729
- const bounded = MathUtils2.clamp(candidate, -Math.PI / 2, Math.PI / 2);
3730
- const nextDistance = Math.abs(candidate - bounded) * 10 + Math.abs(bounded - originPitch);
3731
- if (nextDistance < distance) {
3732
- pitch = bounded;
3733
- distance = nextDistance;
3734
- }
3735
- }
3736
- const z = Math.sin(pitch) * ray.y + Math.cos(pitch) * ray.z;
3737
- const yaw = MathUtils2.radToDeg(Math.atan2(this.anchor.x, -this.anchor.z) - Math.atan2(ray.x, -z));
3769
+ const roots = this.pitchRoots(ray, originPitch);
3770
+ const exactPitch = MathUtils2.clamp(
3771
+ this.useSupplementary ? roots.supplementary : roots.principal,
3772
+ -Math.PI / 2,
3773
+ Math.PI / 2
3774
+ );
3775
+ const stablePitch = originPitch - (Math.atan2(ray.y, -ray.z) - this.baseElevation);
3776
+ const fromPole = Math.asin(Math.min(1, Math.hypot(this.anchor.x, this.anchor.z)));
3777
+ const pointerLock = MathUtils2.smoothstep(fromPole, MathUtils2.degToRad(8), MathUtils2.degToRad(18));
3778
+ const pitchTurn = Math.atan2(Math.sin(exactPitch - stablePitch), Math.cos(exactPitch - stablePitch));
3779
+ const pitch = MathUtils2.clamp(
3780
+ pointerLock >= 1 ? exactPitch : stablePitch + pointerLock * pitchTurn,
3781
+ -Math.PI / 2,
3782
+ Math.PI / 2
3783
+ );
3784
+ const pitchedZ = Math.sin(exactPitch) * ray.y + Math.cos(exactPitch) * ray.z;
3785
+ const exactYaw = Math.atan2(this.anchor.x, -this.anchor.z) - Math.atan2(ray.x, -pitchedZ);
3786
+ const stableYaw = MathUtils2.degToRad(this.origin.yaw) + Math.atan2(ray.x, -ray.z) - this.baseAzimuth;
3787
+ const yawTurn = Math.atan2(Math.sin(exactYaw - stableYaw), Math.cos(exactYaw - stableYaw));
3788
+ const yaw = MathUtils2.radToDeg(stableYaw + pointerLock * yawTurn);
3738
3789
  return { yaw: normalizeViewYaw(yaw - this.origin.yaw), pitch: MathUtils2.radToDeg(pitch) - this.origin.pitch };
3739
3790
  }
3740
3791
  };
@@ -3747,8 +3798,11 @@ function createPointerDrag(camera, element, runtime, source) {
3747
3798
  let lastPoint;
3748
3799
  let fresh = false;
3749
3800
  let lastFrameDelta = 1 / 60;
3801
+ let samples = [];
3802
+ let gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3803
+ let gestureInvert = false;
3750
3804
  const key = (rect) => `${rect.left},${rect.top},${rect.width},${rect.height},${camera.projectionMatrix.elements.join(",")}`;
3751
- const start = (point) => {
3805
+ const place = (point) => {
3752
3806
  const handle = runtime();
3753
3807
  displayed = { ...point };
3754
3808
  lastPoint = { ...point };
@@ -3760,69 +3814,113 @@ function createPointerDrag(camera, element, runtime, source) {
3760
3814
  projectionKey = key(rect);
3761
3815
  anchor = new PointerDragAnchor(point, handle.getView(), rect, camera.clone());
3762
3816
  };
3763
- const apply = (point, previous, speed, invert, recordVelocity) => {
3817
+ const remember = (point, at) => {
3818
+ samples.push({ x: point.x, y: point.y, at });
3819
+ const cutoff = at - POINTER_VELOCITY_MAX_AGE_MS;
3820
+ while (samples.length > 2 && samples[0].at < cutoff) samples.shift();
3821
+ };
3822
+ const apply = (point, previous, speed, invert, recordVelocity, releaseVelocity) => {
3764
3823
  const handle = runtime();
3765
3824
  if (!handle) return;
3766
3825
  const rect = element.getBoundingClientRect();
3767
3826
  if (anchor && key(rect) !== projectionKey) {
3768
3827
  const queued = pending;
3769
- start(displayed ?? previous);
3828
+ place(displayed ?? previous);
3770
3829
  pending = queued;
3771
3830
  }
3831
+ const options = {
3832
+ recordVelocity: releaseVelocity ? false : recordVelocity,
3833
+ source,
3834
+ ...releaseVelocity ? { velocity: releaseVelocity } : {}
3835
+ };
3772
3836
  if (anchor) {
3773
3837
  handle.applyViewDelta(anchor.solve(point, speed, invert), {
3774
- pointerOrigin: anchor.origin,
3775
- recordVelocity,
3776
- source
3838
+ ...options,
3839
+ pointerOrigin: anchor.origin
3777
3840
  });
3778
3841
  } else {
3779
3842
  handle.applyViewDelta({
3780
3843
  yaw: -(point.x - previous.x) * 360 * speed / Math.max(rect.width, 1) * (invert ? -1 : 1),
3781
3844
  pitch: (point.y - previous.y) * 180 * speed / Math.max(rect.height, 1) * (invert ? -1 : 1)
3782
- }, { recordVelocity, source });
3845
+ }, options);
3783
3846
  }
3784
3847
  };
3848
+ const valid = () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked();
3849
+ const clear = () => {
3850
+ anchor = void 0;
3851
+ revision = -1;
3852
+ pending = void 0;
3853
+ displayed = void 0;
3854
+ samples = [];
3855
+ };
3785
3856
  return {
3786
- start,
3787
- valid: () => revision === (runtime()?.getPointerDragState().revision ?? -1) && !runtime()?.isInteractionLocked(),
3788
- clear: () => {
3789
- anchor = void 0;
3790
- revision = -1;
3791
- pending = void 0;
3792
- displayed = void 0;
3857
+ start(point, at = performance.now()) {
3858
+ gestureSpeed = DEFAULT_POINTER_DRAG_SPEED;
3859
+ gestureInvert = false;
3860
+ samples = [];
3861
+ remember(point, at);
3862
+ place(point);
3793
3863
  },
3864
+ valid,
3865
+ clear,
3794
3866
  suspend: () => {
3795
3867
  anchor = void 0;
3796
3868
  pending = void 0;
3797
3869
  displayed = void 0;
3870
+ samples = [];
3798
3871
  },
3799
- move(point, previous, speed, invert) {
3872
+ move(point, previous, speed, invert, at = performance.now()) {
3800
3873
  fresh = true;
3874
+ gestureSpeed = speed;
3875
+ gestureInvert = invert;
3876
+ remember(point, at);
3801
3877
  pending = { point, previous: lastPoint ?? previous, speed, invert };
3802
3878
  },
3803
- flush(deltaSeconds = lastFrameDelta, immediate = false) {
3804
- lastFrameDelta = deltaSeconds;
3879
+ flush(deltaSeconds = lastFrameDelta, immediate = false, releaseVelocity) {
3880
+ if (deltaSeconds > 0) lastFrameDelta = deltaSeconds;
3805
3881
  if (!pending) return;
3806
3882
  const sample = pending;
3807
3883
  if (!anchor || immediate) {
3808
3884
  pending = void 0;
3809
- apply(sample.point, sample.previous, sample.speed, sample.invert, fresh);
3885
+ apply(sample.point, sample.previous, sample.speed, sample.invert, fresh, releaseVelocity);
3810
3886
  fresh = false;
3811
3887
  lastPoint = sample.point;
3812
3888
  displayed = sample.point;
3813
3889
  return;
3814
3890
  }
3891
+ const dt = deltaSeconds > 0 ? deltaSeconds : lastFrameDelta;
3815
3892
  const previousDisplay = displayed ?? sample.previous;
3816
3893
  const dx = sample.point.x - previousDisplay.x;
3817
3894
  const dy = sample.point.y - previousDisplay.y;
3818
3895
  const distance = Math.hypot(dx, dy);
3819
- const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + deltaSeconds) : 0;
3896
+ const ratio = distance > 0.01 ? POINTER_FOLLOW_TIME_SECONDS / (POINTER_FOLLOW_TIME_SECONDS + dt) : 0;
3820
3897
  const point = { x: sample.point.x - dx * ratio, y: sample.point.y - dy * ratio };
3821
3898
  apply(point, sample.previous, sample.speed, sample.invert, fresh);
3822
3899
  fresh = false;
3823
3900
  lastPoint = sample.point;
3824
3901
  displayed = point;
3825
3902
  if (ratio === 0) pending = void 0;
3903
+ },
3904
+ release() {
3905
+ const handle = runtime();
3906
+ if (!handle || !valid()) {
3907
+ clear();
3908
+ return;
3909
+ }
3910
+ const newestAt = samples[samples.length - 1]?.at ?? 0;
3911
+ const velocity = pointerReleaseVelocity(samples, Math.max(performance.now(), newestAt), (point) => {
3912
+ if (anchor) return anchor.solve(point, gestureSpeed, gestureInvert);
3913
+ const origin = samples[0] ?? point;
3914
+ const rect = element.getBoundingClientRect();
3915
+ const sign = gestureInvert ? -1 : 1;
3916
+ return {
3917
+ yaw: -(point.x - origin.x) * 360 * gestureSpeed / Math.max(rect.width, 1) * sign,
3918
+ pitch: (point.y - origin.y) * 180 * gestureSpeed / Math.max(rect.height, 1) * sign
3919
+ };
3920
+ });
3921
+ if (pending) this.flush(lastFrameDelta, true, velocity);
3922
+ else handle.applyViewDelta({}, { velocity, source });
3923
+ clear();
3826
3924
  }
3827
3925
  };
3828
3926
  }
@@ -3838,6 +3936,9 @@ var BUTTON_FLAG = {
3838
3936
  function resolveSpeed2(value, fallback) {
3839
3937
  return Number.isFinite(value) && value >= 0 ? value : fallback;
3840
3938
  }
3939
+ function pointerAt(event) {
3940
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
3941
+ }
3841
3942
  function resolveButtons(buttons) {
3842
3943
  const list = buttons && buttons.length > 0 ? buttons : ["left"];
3843
3944
  return new Set(list.map((button) => BUTTON_FLAG[button]));
@@ -3924,7 +4025,7 @@ function MouseControls({
3924
4025
  });
3925
4026
  element.setPointerCapture?.(event.pointerId);
3926
4027
  syncActive();
3927
- drag.start({ x: event.clientX, y: event.clientY });
4028
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
3928
4029
  };
3929
4030
  const onPointerMove = (event) => {
3930
4031
  if (!isMouseLike(event)) {
@@ -3945,15 +4046,15 @@ function MouseControls({
3945
4046
  };
3946
4047
  pointersRef.current.set(event.pointerId, next);
3947
4048
  const speed = resolveSpeed2(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED2);
3948
- drag.move(next, previous, speed, optionsRef.current.invert);
4049
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
3949
4050
  };
3950
4051
  const releasePointer = (event) => {
3951
4052
  if (!pointersRef.current.has(event.pointerId)) {
3952
4053
  return;
3953
4054
  }
3954
- if (event.type === "pointerup") drag.flush();
4055
+ if (event.type === "pointerup") drag.release();
4056
+ else drag.clear();
3955
4057
  pointersRef.current.delete(event.pointerId);
3956
- drag.clear();
3957
4058
  if (element.hasPointerCapture?.(event.pointerId)) {
3958
4059
  element.releasePointerCapture?.(event.pointerId);
3959
4060
  }
@@ -5619,6 +5720,9 @@ function notifyScenesHost(host) {
5619
5720
  import { useFrame as useFrame6, useThree as useThree7 } from "@react-three/fiber";
5620
5721
  import { useContext as useContext13, useEffect as useEffect11, useRef as useRef10 } from "react";
5621
5722
  var DEFAULT_ROTATE_SPEED3 = 0.35;
5723
+ function pointerAt2(event) {
5724
+ return event.timeStamp > 0 ? event.timeStamp : performance.now();
5725
+ }
5622
5726
  function resolveSpeed3(value, fallback) {
5623
5727
  return Number.isFinite(value) && value >= 0 ? value : fallback;
5624
5728
  }
@@ -5687,7 +5791,7 @@ function TouchControls({
5687
5791
  });
5688
5792
  element.setPointerCapture?.(event.pointerId);
5689
5793
  syncActive();
5690
- drag.start({ x: event.clientX, y: event.clientY });
5794
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5691
5795
  if (pointersRef.current.size >= 2) {
5692
5796
  drag.suspend();
5693
5797
  const [first, second] = Array.from(pointersRef.current.values());
@@ -5734,15 +5838,16 @@ function TouchControls({
5734
5838
  return;
5735
5839
  }
5736
5840
  const speed = resolveSpeed3(optionsRef.current.rotateSpeed, DEFAULT_ROTATE_SPEED3);
5737
- drag.move(next, previous, speed, optionsRef.current.invert);
5841
+ drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5738
5842
  };
5739
5843
  const releasePointer = (event) => {
5740
5844
  if (!pointersRef.current.has(event.pointerId)) {
5741
5845
  return;
5742
5846
  }
5743
- if (event.type === "pointerup") drag.flush();
5847
+ const pinch = pointersRef.current.size >= 2;
5848
+ if (event.type === "pointerup" && !pinch) drag.release();
5849
+ else drag.clear();
5744
5850
  pointersRef.current.delete(event.pointerId);
5745
- drag.clear();
5746
5851
  if (element.hasPointerCapture?.(event.pointerId)) {
5747
5852
  element.releasePointerCapture?.(event.pointerId);
5748
5853
  }
@@ -5751,7 +5856,7 @@ function TouchControls({
5751
5856
  }
5752
5857
  syncActive(event.type !== "pointerup");
5753
5858
  const remaining = pointersRef.current.values().next().value;
5754
- if (remaining) drag.start(remaining);
5859
+ if (remaining) drag.start(remaining, pointerAt2(event));
5755
5860
  };
5756
5861
  element.addEventListener("pointerdown", onPointerDown);
5757
5862
  element.addEventListener("pointermove", onPointerMove);