@ericchen1990/pano-view 3.6.2 → 3.6.4

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
@@ -3723,12 +3723,18 @@ var PointerDragAnchor = class {
3723
3723
  this.projection = projection;
3724
3724
  this.local = new Vector3();
3725
3725
  this.rotation = new Euler2(0, 0, 0, "YXZ");
3726
- 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(
3727
3730
  MathUtils2.degToRad(origin.pitch),
3728
3731
  MathUtils2.degToRad(-origin.yaw),
3729
3732
  0,
3730
3733
  "YXZ"
3731
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);
3732
3738
  }
3733
3739
  ray(point) {
3734
3740
  return this.local.set(
@@ -3737,6 +3743,21 @@ var PointerDragAnchor = class {
3737
3743
  0.5
3738
3744
  ).applyMatrix4(this.projection.projectionMatrixInverse).normalize();
3739
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
+ }
3740
3761
  solve(point, speed = DEFAULT_POINTER_DRAG_SPEED, invert = false) {
3741
3762
  const multiplier = (Number.isFinite(speed) && speed >= 0 ? speed : DEFAULT_POINTER_DRAG_SPEED) / DEFAULT_POINTER_DRAG_SPEED * (invert ? -1 : 1);
3742
3763
  if (multiplier === 0) return { yaw: 0, pitch: 0 };
@@ -3744,22 +3765,27 @@ var PointerDragAnchor = class {
3744
3765
  x: this.start.x + (point.x - this.start.x) * multiplier,
3745
3766
  y: this.start.y + (point.y - this.start.y) * multiplier
3746
3767
  });
3747
- const latitude = Math.asin(MathUtils2.clamp(this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12), -1, 1));
3748
- const offset = Math.atan2(ray.y, -ray.z);
3749
3768
  const originPitch = MathUtils2.degToRad(this.origin.pitch);
3750
- const candidates = [latitude - offset, Math.PI - latitude - offset, -Math.PI - latitude - offset];
3751
- let pitch = candidates[0];
3752
- let distance = Infinity;
3753
- for (const candidate of candidates) {
3754
- const bounded = MathUtils2.clamp(candidate, -Math.PI / 2, Math.PI / 2);
3755
- const nextDistance = Math.abs(candidate - bounded) * 10 + Math.abs(bounded - originPitch);
3756
- if (nextDistance < distance) {
3757
- pitch = bounded;
3758
- distance = nextDistance;
3759
- }
3760
- }
3761
- const z = Math.sin(pitch) * ray.y + Math.cos(pitch) * ray.z;
3762
- 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);
3763
3789
  return { yaw: normalizeViewYaw(yaw - this.origin.yaw), pitch: MathUtils2.radToDeg(pitch) - this.origin.pitch };
3764
3790
  }
3765
3791
  };
@@ -3955,7 +3981,10 @@ function MouseControls({
3955
3981
  const element = gl.domElement;
3956
3982
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "mouse");
3957
3983
  const allowedButtons = resolveButtons(buttons);
3984
+ const pending = /* @__PURE__ */ new Set();
3985
+ let disposed = false;
3958
3986
  const clearPointers = () => {
3987
+ pending.clear();
3959
3988
  for (const pointerId of pointersRef.current.keys()) {
3960
3989
  if (element.hasPointerCapture?.(pointerId)) {
3961
3990
  element.releasePointerCapture?.(pointerId);
@@ -3981,6 +4010,25 @@ function MouseControls({
3981
4010
  { cancelled }
3982
4011
  );
3983
4012
  };
4013
+ const beginPointer = (event) => {
4014
+ if (disposed || controlsRef.current?.isInteractionLocked()) {
4015
+ return;
4016
+ }
4017
+ if (pointersRef.current.has(event.pointerId)) {
4018
+ return;
4019
+ }
4020
+ pointersRef.current.set(event.pointerId, {
4021
+ x: event.clientX,
4022
+ y: event.clientY,
4023
+ at: performance.now()
4024
+ });
4025
+ try {
4026
+ element.setPointerCapture?.(event.pointerId);
4027
+ } catch {
4028
+ }
4029
+ syncActive();
4030
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
4031
+ };
3984
4032
  const onPointerDown = (event) => {
3985
4033
  if (!isMouseLike(event)) {
3986
4034
  return;
@@ -3992,14 +4040,14 @@ function MouseControls({
3992
4040
  return;
3993
4041
  }
3994
4042
  event.preventDefault();
3995
- pointersRef.current.set(event.pointerId, {
3996
- x: event.clientX,
3997
- y: event.clientY,
3998
- at: performance.now()
4043
+ const pointerId = event.pointerId;
4044
+ pending.add(pointerId);
4045
+ queueMicrotask(() => {
4046
+ if (!pending.delete(pointerId)) {
4047
+ return;
4048
+ }
4049
+ beginPointer(event);
3999
4050
  });
4000
- element.setPointerCapture?.(event.pointerId);
4001
- syncActive();
4002
- drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
4003
4051
  };
4004
4052
  const onPointerMove = (event) => {
4005
4053
  if (!isMouseLike(event)) {
@@ -4023,6 +4071,7 @@ function MouseControls({
4023
4071
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
4024
4072
  };
4025
4073
  const releasePointer = (event) => {
4074
+ pending.delete(event.pointerId);
4026
4075
  if (!pointersRef.current.has(event.pointerId)) {
4027
4076
  return;
4028
4077
  }
@@ -4064,6 +4113,7 @@ function MouseControls({
4064
4113
  element.addEventListener("lostpointercapture", releasePointer);
4065
4114
  element.addEventListener("wheel", onWheel, { passive: false });
4066
4115
  return () => {
4116
+ disposed = true;
4067
4117
  validateDragRef.current = void 0;
4068
4118
  element.removeEventListener("pointerdown", onPointerDown);
4069
4119
  element.removeEventListener("pointermove", onPointerMove);
@@ -5581,6 +5631,12 @@ function vector3ToPanoPosition(vector) {
5581
5631
  )
5582
5632
  });
5583
5633
  }
5634
+ function translateHotspotPosition(origin, from, to) {
5635
+ return normalizePanoPosition({
5636
+ yaw: origin.yaw + normalizePanoYaw(to.yaw - from.yaw),
5637
+ pitch: origin.pitch + (to.pitch - from.pitch)
5638
+ });
5639
+ }
5584
5640
 
5585
5641
  // src/panorama-radius.ts
5586
5642
  var DEFAULT_PANORAMA_RADIUS = 1e3;
@@ -5724,7 +5780,10 @@ function TouchControls({
5724
5780
  useEffect11(() => {
5725
5781
  const element = gl.domElement;
5726
5782
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "touch");
5783
+ const pending = /* @__PURE__ */ new Set();
5784
+ let disposed = false;
5727
5785
  const clearPointers = () => {
5786
+ pending.clear();
5728
5787
  for (const pointerId of pointersRef.current.keys()) {
5729
5788
  if (element.hasPointerCapture?.(pointerId)) {
5730
5789
  element.releasePointerCapture?.(pointerId);
@@ -5750,20 +5809,22 @@ function TouchControls({
5750
5809
  { cancelled }
5751
5810
  );
5752
5811
  };
5753
- const onPointerDown = (event) => {
5754
- if (event.pointerType !== "touch") {
5812
+ const beginPointer = (event) => {
5813
+ if (disposed || controlsRef.current?.isInteractionLocked()) {
5755
5814
  return;
5756
5815
  }
5757
- if (controlsRef.current?.isInteractionLocked()) {
5816
+ if (pointersRef.current.has(event.pointerId)) {
5758
5817
  return;
5759
5818
  }
5760
- event.preventDefault();
5761
5819
  pointersRef.current.set(event.pointerId, {
5762
5820
  x: event.clientX,
5763
5821
  y: event.clientY,
5764
5822
  at: performance.now()
5765
5823
  });
5766
- element.setPointerCapture?.(event.pointerId);
5824
+ try {
5825
+ element.setPointerCapture?.(event.pointerId);
5826
+ } catch {
5827
+ }
5767
5828
  syncActive();
5768
5829
  drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5769
5830
  if (pointersRef.current.size >= 2) {
@@ -5772,6 +5833,23 @@ function TouchControls({
5772
5833
  pinchDistanceRef.current = pointerDistance(first, second);
5773
5834
  }
5774
5835
  };
5836
+ const onPointerDown = (event) => {
5837
+ if (event.pointerType !== "touch") {
5838
+ return;
5839
+ }
5840
+ if (controlsRef.current?.isInteractionLocked()) {
5841
+ return;
5842
+ }
5843
+ event.preventDefault();
5844
+ const pointerId = event.pointerId;
5845
+ pending.add(pointerId);
5846
+ queueMicrotask(() => {
5847
+ if (!pending.delete(pointerId)) {
5848
+ return;
5849
+ }
5850
+ beginPointer(event);
5851
+ });
5852
+ };
5775
5853
  const onPointerMove = (event) => {
5776
5854
  if (event.pointerType !== "touch") {
5777
5855
  return;
@@ -5815,6 +5893,7 @@ function TouchControls({
5815
5893
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5816
5894
  };
5817
5895
  const releasePointer = (event) => {
5896
+ pending.delete(event.pointerId);
5818
5897
  if (!pointersRef.current.has(event.pointerId)) {
5819
5898
  return;
5820
5899
  }
@@ -5838,6 +5917,7 @@ function TouchControls({
5838
5917
  element.addEventListener("pointercancel", releasePointer);
5839
5918
  element.addEventListener("lostpointercapture", releasePointer);
5840
5919
  return () => {
5920
+ disposed = true;
5841
5921
  validateDragRef.current = void 0;
5842
5922
  element.removeEventListener("pointerdown", onPointerDown);
5843
5923
  element.removeEventListener("pointermove", onPointerMove);
@@ -11479,13 +11559,103 @@ import {
11479
11559
  EdgesGeometry,
11480
11560
  MathUtils as MathUtils8,
11481
11561
  Matrix4 as Matrix44,
11562
+ PerspectiveCamera as PerspectiveCamera8,
11482
11563
  Plane,
11483
11564
  PlaneGeometry as PlaneGeometry4,
11484
11565
  Quaternion as Quaternion3,
11485
11566
  Ray,
11567
+ Raycaster as Raycaster2,
11568
+ Vector2 as Vector24,
11486
11569
  Vector3 as Vector316
11487
11570
  } from "three";
11488
11571
 
11572
+ // src/hotspot/drag-session.ts
11573
+ var CLICK_SUPPRESS_MS = 1e3;
11574
+ function trackPointerDrag(source, element, pointerId, onMove, onEnd) {
11575
+ let stopped = false;
11576
+ const releaseCapture = () => {
11577
+ if (!element.hasPointerCapture?.(pointerId)) {
11578
+ return;
11579
+ }
11580
+ try {
11581
+ element.releasePointerCapture?.(pointerId);
11582
+ } catch {
11583
+ }
11584
+ };
11585
+ const stop = () => {
11586
+ if (stopped) {
11587
+ return;
11588
+ }
11589
+ stopped = true;
11590
+ source.removeEventListener("pointermove", onPointerMove, true);
11591
+ source.removeEventListener("pointerup", onPointerUp, true);
11592
+ source.removeEventListener("pointercancel", onPointerCancel, true);
11593
+ element.removeEventListener("lostpointercapture", onLostCapture);
11594
+ releaseCapture();
11595
+ };
11596
+ const finish = (event) => {
11597
+ if (stopped || event.pointerId !== pointerId) {
11598
+ return;
11599
+ }
11600
+ stop();
11601
+ onEnd(event);
11602
+ };
11603
+ const onPointerMove = (event) => {
11604
+ if (stopped || event.pointerId !== pointerId) {
11605
+ return;
11606
+ }
11607
+ try {
11608
+ event.preventDefault?.();
11609
+ } catch {
11610
+ }
11611
+ onMove(event);
11612
+ };
11613
+ const onPointerUp = (event) => {
11614
+ finish(event);
11615
+ };
11616
+ const onPointerCancel = (event) => {
11617
+ finish(event);
11618
+ };
11619
+ const onLostCapture = (event) => {
11620
+ if ((event.buttons ?? 0) > 0) {
11621
+ return;
11622
+ }
11623
+ finish(event);
11624
+ };
11625
+ try {
11626
+ element.setPointerCapture?.(pointerId);
11627
+ } catch {
11628
+ }
11629
+ source.addEventListener("pointermove", onPointerMove, true);
11630
+ source.addEventListener("pointerup", onPointerUp, true);
11631
+ source.addEventListener("pointercancel", onPointerCancel, true);
11632
+ element.addEventListener("lostpointercapture", onLostCapture);
11633
+ return stop;
11634
+ }
11635
+ function suppressFollowingClick(target) {
11636
+ let cleaned = false;
11637
+ const cleanup = () => {
11638
+ if (cleaned) {
11639
+ return;
11640
+ }
11641
+ cleaned = true;
11642
+ target.removeEventListener("click", swallowClick, true);
11643
+ target.removeEventListener("pointerdown", cancel, true);
11644
+ };
11645
+ const swallowClick = (event) => {
11646
+ event.preventDefault();
11647
+ event.stopPropagation();
11648
+ event.stopImmediatePropagation();
11649
+ cleanup();
11650
+ };
11651
+ const cancel = () => {
11652
+ cleanup();
11653
+ };
11654
+ target.addEventListener("click", swallowClick, true);
11655
+ target.addEventListener("pointerdown", cancel, true);
11656
+ setTimeout(cleanup, CLICK_SUPPRESS_MS);
11657
+ }
11658
+
11489
11659
  // src/hotspot/hotspot-tooltip.tsx
11490
11660
  import { useEffect as useEffect23, useMemo as useMemo18, useState as useState12 } from "react";
11491
11661
 
@@ -11754,6 +11924,21 @@ function fixedScaleFactor(cameraFov, referenceFov) {
11754
11924
  const safeReferenceFov = MathUtils6.clamp(referenceFov, 1, 179);
11755
11925
  return Math.tan(MathUtils6.degToRad(cameraFov) / 2) / Math.tan(MathUtils6.degToRad(safeReferenceFov) / 2);
11756
11926
  }
11927
+ function selectionFrameScale(worldScaleX, worldScaleY, cameraSpaceDepth, fovDegrees, viewportHeight, outsetPixels) {
11928
+ const pixels = Number.isFinite(outsetPixels) && outsetPixels > 0 ? outsetPixels : 0;
11929
+ const depth = Number.isFinite(cameraSpaceDepth) ? Math.max(cameraSpaceDepth, 1e-4) : 1e-4;
11930
+ const height = Number.isFinite(viewportHeight) ? Math.max(viewportHeight, 1) : 1;
11931
+ const fov = Number.isFinite(fovDegrees) ? MathUtils6.clamp(fovDegrees, 1, 179) : 75;
11932
+ const pixelsPerWorldUnit = height / (2 * Math.tan(MathUtils6.degToRad(fov) * 0.5) * depth);
11933
+ const axis = (worldScale) => {
11934
+ const pixelsPerLocalUnit = pixelsPerWorldUnit * Math.abs(worldScale);
11935
+ if (pixelsPerLocalUnit < 1e-4) {
11936
+ return 1;
11937
+ }
11938
+ return 1 + 2 * pixels / pixelsPerLocalUnit;
11939
+ };
11940
+ return [axis(worldScaleX), axis(worldScaleY)];
11941
+ }
11757
11942
 
11758
11943
  // src/hotspot/hotspot-stroke-line.tsx
11759
11944
  import { Line } from "@react-three/drei";
@@ -11840,6 +12025,11 @@ var DEFAULT_REFERENCE_FOV = 75;
11840
12025
  var MIN_ANGULAR_SIZE = 0.01;
11841
12026
  var MAX_ANGULAR_SIZE = 179;
11842
12027
  var DRAG_EPSILON_DEGREES = 1e-3;
12028
+ var SELECTION_BORDER_WIDTH = 3;
12029
+ var SELECTION_BORDER_GAP = 3;
12030
+ var SELECTION_BORDER_OUTSET = SELECTION_BORDER_GAP + SELECTION_BORDER_WIDTH / 2;
12031
+ var SELECTION_CAMERA_POSITION = new Vector316();
12032
+ var SELECTION_WORLD_SCALE = new Vector316();
11843
12033
  var SELECTION_BORDER_POINTS = [
11844
12034
  [-0.5, -0.5, 1e-3],
11845
12035
  [0.5, -0.5, 1e-3],
@@ -11927,6 +12117,34 @@ function angularDistance(a, b) {
11927
12117
  const bVector = panoPositionToVector3(b);
11928
12118
  return MathUtils8.radToDeg(aVector.angleTo(bVector));
11929
12119
  }
12120
+ var DRAG_POINTER = new Vector24();
12121
+ var DRAG_RAYCASTER = new Raycaster2();
12122
+ function aimDragRaycaster(event, element, camera) {
12123
+ const rect = element.getBoundingClientRect();
12124
+ if (!(rect.width > 0) || !(rect.height > 0)) {
12125
+ return false;
12126
+ }
12127
+ DRAG_POINTER.set(
12128
+ (event.clientX - rect.left) / rect.width * 2 - 1,
12129
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
12130
+ );
12131
+ camera.updateMatrixWorld();
12132
+ DRAG_RAYCASTER.setFromCamera(DRAG_POINTER, camera);
12133
+ return true;
12134
+ }
12135
+ function pointerToPanoPosition(event, element, camera) {
12136
+ if (!aimDragRaycaster(event, element, camera)) {
12137
+ return null;
12138
+ }
12139
+ return vector3ToPanoPosition(DRAG_RAYCASTER.ray.direction);
12140
+ }
12141
+ function pointerHitsHotspot(event, element, camera, group) {
12142
+ if (!group || !aimDragRaycaster(event, element, camera)) {
12143
+ return false;
12144
+ }
12145
+ group.updateMatrixWorld(true);
12146
+ return DRAG_RAYCASTER.intersectObject(group, true).length > 0;
12147
+ }
11930
12148
  function isTooltipOpen(trigger, hovered, focused, pinned) {
11931
12149
  switch (trigger) {
11932
12150
  case "always":
@@ -12122,6 +12340,24 @@ function updateTooltipAnchor(anchor, group, placement, useAngularScale, camera,
12122
12340
  }
12123
12341
  setTooltipAnchorOnLocalBox(anchor, placement, -0.5, 0.5, -0.5, 0.5);
12124
12342
  }
12343
+ function placeSelectionFrame(frame, group, camera, viewportHeight) {
12344
+ if (!frame) {
12345
+ return;
12346
+ }
12347
+ group.getWorldScale(SELECTION_WORLD_SCALE);
12348
+ SELECTION_CAMERA_POSITION.setFromMatrixPosition(group.matrixWorld).applyMatrix4(
12349
+ camera.matrixWorldInverse
12350
+ );
12351
+ const [scaleX, scaleY] = selectionFrameScale(
12352
+ SELECTION_WORLD_SCALE.x,
12353
+ SELECTION_WORLD_SCALE.y,
12354
+ -SELECTION_CAMERA_POSITION.z,
12355
+ camera instanceof PerspectiveCamera8 ? camera.fov : DEFAULT_REFERENCE_FOV,
12356
+ viewportHeight,
12357
+ SELECTION_BORDER_OUTSET
12358
+ );
12359
+ frame.scale.set(scaleX, scaleY, 1);
12360
+ }
12125
12361
  function HotspotAnchor({
12126
12362
  id,
12127
12363
  position,
@@ -12161,10 +12397,13 @@ function HotspotAnchor({
12161
12397
  const hotspotTargetRegistry = useContext21(HotspotTargetRegistryContext);
12162
12398
  const eventBus = useContext21(PanoEventBusContext);
12163
12399
  const cursorApi = usePanoCursor();
12164
- const { camera, size } = useThree19();
12400
+ const { camera, gl, size } = useThree19();
12165
12401
  const groupRef = useRef25(null);
12402
+ const selectionFrameRef = useRef25(null);
12166
12403
  const tooltipAnchorRef = useRef25(null);
12167
12404
  const dragStateRef = useRef25(null);
12405
+ const updateDragRef = useRef25(() => void 0);
12406
+ const endDragRef = useRef25(() => void 0);
12168
12407
  const suppressNextClickRef = useRef25(false);
12169
12408
  const [hovered, setHovered] = useState13(false);
12170
12409
  const [dragging, setDragging] = useState13(false);
@@ -12234,7 +12473,9 @@ function HotspotAnchor({
12234
12473
  const cursorClaimId = hotspotCursorClaimId(id);
12235
12474
  useEffect24(
12236
12475
  () => () => {
12237
- dragStateRef.current?.releaseInteractionLock();
12476
+ const dragState = dragStateRef.current;
12477
+ dragState?.stopTracking();
12478
+ dragState?.releaseInteractionLock();
12238
12479
  dragStateRef.current = null;
12239
12480
  },
12240
12481
  []
@@ -12291,6 +12532,7 @@ function HotspotAnchor({
12291
12532
  if (!dragState) {
12292
12533
  return;
12293
12534
  }
12535
+ dragState.stopTracking();
12294
12536
  dragState.releaseInteractionLock();
12295
12537
  dragStateRef.current = null;
12296
12538
  setDragging(false);
@@ -12313,6 +12555,7 @@ function HotspotAnchor({
12313
12555
  worldHeight,
12314
12556
  resolvedDistance
12315
12557
  );
12558
+ placeSelectionFrame(selectionFrameRef.current, group, camera, size.height);
12316
12559
  if (tooltipOpen && tooltipAnchorRef.current) {
12317
12560
  updateTooltipAnchor(
12318
12561
  tooltipAnchorRef.current,
@@ -12331,6 +12574,7 @@ function HotspotAnchor({
12331
12574
  rotation,
12332
12575
  scaleMode,
12333
12576
  size,
12577
+ selected,
12334
12578
  surfaceQuaternion3,
12335
12579
  tooltipOpen,
12336
12580
  tooltipPlacement,
@@ -12356,6 +12600,12 @@ function HotspotAnchor({
12356
12600
  worldHeight,
12357
12601
  resolvedDistance
12358
12602
  );
12603
+ placeSelectionFrame(
12604
+ selectionFrameRef.current,
12605
+ group,
12606
+ state.camera,
12607
+ state.size.height
12608
+ );
12359
12609
  if (tooltipOpen && tooltipAnchorRef.current) {
12360
12610
  updateTooltipAnchor(
12361
12611
  tooltipAnchorRef.current,
@@ -12371,31 +12621,78 @@ function HotspotAnchor({
12371
12621
  event.stopPropagation();
12372
12622
  event.nativeEvent.stopImmediatePropagation?.();
12373
12623
  };
12374
- const releasePointer = (event, emitDragEnd) => {
12624
+ const dragPosition = (dragState, event) => {
12625
+ const pointerPosition = pointerToPanoPosition(event, gl.domElement, camera);
12626
+ if (!pointerPosition) {
12627
+ return dragState.lastPosition;
12628
+ }
12629
+ return translateHotspotPosition(
12630
+ dragState.startPosition,
12631
+ dragState.pointerOrigin,
12632
+ pointerPosition
12633
+ );
12634
+ };
12635
+ updateDragRef.current = (nativeEvent) => {
12375
12636
  const dragState = dragStateRef.current;
12376
- if (!dragState || dragState.pointerId !== event.pointerId) {
12637
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12377
12638
  return;
12378
12639
  }
12379
- stopPointerEvent(event);
12380
- const target = event.target;
12381
- if (target.hasPointerCapture?.(event.pointerId)) {
12382
- target.releasePointerCapture?.(event.pointerId);
12383
- }
12384
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12385
- if (emitDragEnd) {
12386
- onDragEnd?.({
12387
- ...makeInteractionEvent(id, nextPosition, event),
12388
- source: "pointer",
12389
- startPosition: dragState.startPosition
12390
- });
12640
+ const nextPosition = dragPosition(dragState, nativeEvent);
12641
+ dragState.lastPosition = nextPosition;
12642
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12643
+ onPositionChange?.({
12644
+ id,
12645
+ position: nextPosition,
12646
+ source: "pointer",
12647
+ nativeEvent,
12648
+ startPosition: dragState.startPosition
12649
+ });
12650
+ };
12651
+ endDragRef.current = (nativeEvent, emitDragEnd) => {
12652
+ const dragState = dragStateRef.current;
12653
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12654
+ return;
12391
12655
  }
12392
- suppressNextClickRef.current = dragState.moved;
12656
+ const nextPosition = dragPosition(dragState, nativeEvent);
12657
+ dragState.lastPosition = nextPosition;
12658
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12659
+ const moved = dragState.moved;
12660
+ const startPosition = dragState.startPosition;
12661
+ dragState.stopTracking();
12393
12662
  dragState.releaseInteractionLock();
12394
12663
  dragStateRef.current = null;
12664
+ suppressNextClickRef.current = moved;
12395
12665
  setDragging(false);
12396
- if (emitDragEnd) {
12397
- setHovered(true);
12666
+ if (!emitDragEnd) {
12667
+ return;
12668
+ }
12669
+ const overHotspot = pointerHitsHotspot(
12670
+ nativeEvent,
12671
+ gl.domElement,
12672
+ camera,
12673
+ groupRef.current
12674
+ );
12675
+ eventHovered.current = overHotspot;
12676
+ setHovered(overHotspot);
12677
+ if (moved && typeof window !== "undefined") {
12678
+ suppressFollowingClick(window);
12398
12679
  }
12680
+ if (moved) {
12681
+ onPositionChange?.({
12682
+ id,
12683
+ position: nextPosition,
12684
+ source: "pointer",
12685
+ nativeEvent,
12686
+ startPosition
12687
+ });
12688
+ }
12689
+ onDragEnd?.({
12690
+ id,
12691
+ position: nextPosition,
12692
+ source: "pointer",
12693
+ nativeEvent,
12694
+ startPosition
12695
+ });
12399
12696
  };
12400
12697
  return /* @__PURE__ */ jsxs15(
12401
12698
  "group",
@@ -12415,23 +12712,42 @@ function HotspotAnchor({
12415
12712
  }
12416
12713
  suppressNextClickRef.current = false;
12417
12714
  } : void 0,
12418
- onLostPointerCapture: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12419
- onPointerCancel: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12715
+ onLostPointerCapture: acceptsPointer ? (event) => {
12716
+ endDragRef.current(event.nativeEvent, true);
12717
+ } : void 0,
12718
+ onPointerCancel: acceptsPointer ? (event) => {
12719
+ endDragRef.current(event.nativeEvent, true);
12720
+ } : void 0,
12420
12721
  onPointerDown: acceptsPointer ? (event) => {
12421
- stopPointerEvent(event);
12722
+ const native = event.nativeEvent;
12422
12723
  if (!draggable || dragStateRef.current) {
12724
+ stopPointerEvent(event);
12423
12725
  return;
12424
12726
  }
12727
+ const pointerId = native.pointerId;
12728
+ stopPointerEvent(event);
12425
12729
  const releaseInteractionLock = controlsRef?.current?.acquireInteractionLock() ?? (() => void 0);
12426
- const target = event.target;
12427
- target.setPointerCapture?.(event.pointerId);
12730
+ const pointerOrigin = pointerToPanoPosition(native, gl.domElement, camera) ?? vector3ToPanoPosition(event.ray.direction);
12428
12731
  const startPosition = { ...normalizedPosition };
12732
+ const tracksOutsideMesh = typeof window !== "undefined";
12733
+ const stopTracking = tracksOutsideMesh ? trackPointerDrag(
12734
+ window,
12735
+ gl.domElement,
12736
+ pointerId,
12737
+ (pointerEvent) => updateDragRef.current(pointerEvent),
12738
+ (pointerEvent) => endDragRef.current(pointerEvent, true)
12739
+ ) : () => void 0;
12429
12740
  dragStateRef.current = {
12430
- pointerId: event.pointerId,
12741
+ pointerId,
12431
12742
  startPosition,
12743
+ pointerOrigin,
12744
+ lastPosition: startPosition,
12432
12745
  releaseInteractionLock,
12433
- moved: false
12746
+ moved: false,
12747
+ tracksOutsideMesh,
12748
+ stopTracking
12434
12749
  };
12750
+ eventHovered.current = false;
12435
12751
  setHovered(false);
12436
12752
  setDragging(true);
12437
12753
  onDragStart?.({
@@ -12442,17 +12758,13 @@ function HotspotAnchor({
12442
12758
  } : void 0,
12443
12759
  onPointerMove: acceptsPointer ? (event) => {
12444
12760
  const dragState = dragStateRef.current;
12445
- if (!dragState || dragState.pointerId !== event.pointerId) {
12761
+ if (!dragState || dragState.pointerId !== event.nativeEvent.pointerId) {
12446
12762
  return;
12447
12763
  }
12448
12764
  stopPointerEvent(event);
12449
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12450
- dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12451
- onPositionChange?.({
12452
- ...makeInteractionEvent(id, nextPosition, event),
12453
- source: "pointer",
12454
- startPosition: dragState.startPosition
12455
- });
12765
+ if (!dragState.tracksOutsideMesh) {
12766
+ updateDragRef.current(event.nativeEvent);
12767
+ }
12456
12768
  } : void 0,
12457
12769
  onPointerOut: acceptsPointer ? (event) => {
12458
12770
  if (!dragStateRef.current) {
@@ -12481,7 +12793,10 @@ function HotspotAnchor({
12481
12793
  );
12482
12794
  }
12483
12795
  } : void 0,
12484
- onPointerUp: acceptsPointer ? (event) => releasePointer(event, true) : void 0,
12796
+ onPointerUp: acceptsPointer ? (event) => {
12797
+ stopPointerEvent(event);
12798
+ endDragRef.current(event.nativeEvent, true);
12799
+ } : void 0,
12485
12800
  children: [
12486
12801
  focused ? focusContent ?? /* @__PURE__ */ jsx26(
12487
12802
  "lineSegments",
@@ -12494,16 +12809,16 @@ function HotspotAnchor({
12494
12809
  }
12495
12810
  ) : null,
12496
12811
  children,
12497
- selected && useAngularScale ? /* @__PURE__ */ jsx26(
12812
+ selected && useAngularScale ? /* @__PURE__ */ jsx26("group", { ref: selectionFrameRef, children: /* @__PURE__ */ jsx26(
12498
12813
  HotspotStrokeLine,
12499
12814
  {
12500
12815
  points: SELECTION_BORDER_POINTS,
12501
12816
  color: "#ffb300",
12502
- lineWidth: 3,
12817
+ lineWidth: SELECTION_BORDER_WIDTH,
12503
12818
  opacity: 1,
12504
12819
  renderOrder: renderOrder + 1
12505
12820
  }
12506
- ) : null,
12821
+ ) }) : null,
12507
12822
  tooltipOpen && tooltipContent ? /* @__PURE__ */ jsx26("group", { ref: tooltipAnchorRef, children: /* @__PURE__ */ jsx26(
12508
12823
  HotspotTooltip,
12509
12824
  {
@@ -12523,11 +12838,11 @@ import {
12523
12838
  DoubleSide as DoubleSide3,
12524
12839
  Mesh as Mesh4,
12525
12840
  MeshBasicMaterial,
12526
- Vector2 as Vector24
12841
+ Vector2 as Vector25
12527
12842
  } from "three";
12528
12843
  import { jsx as jsx27, jsxs as jsxs16 } from "react/jsx-runtime";
12529
12844
  var HOTSPOT_ALPHA_CUTOFF = 0.05;
12530
- var SAMPLE_UV = new Vector24();
12845
+ var SAMPLE_UV = new Vector25();
12531
12846
  var RAYCAST_HITS = [];
12532
12847
  var IMAGE_DATA_CACHE = /* @__PURE__ */ new WeakMap();
12533
12848
  function clampOpacity(opacity) {
@@ -13790,7 +14105,7 @@ import {
13790
14105
  ClampToEdgeWrapping as ClampToEdgeWrapping8,
13791
14106
  LinearFilter as LinearFilter10,
13792
14107
  MathUtils as MathUtils10,
13793
- PerspectiveCamera as PerspectiveCamera8,
14108
+ PerspectiveCamera as PerspectiveCamera9,
13794
14109
  SRGBColorSpace as SRGBColorSpace9
13795
14110
  } from "three";
13796
14111
  import { jsx as jsx33 } from "react/jsx-runtime";
@@ -14044,11 +14359,15 @@ function createTextTexture(text, width, height, style, rasterLong) {
14044
14359
  if (!context) {
14045
14360
  throw new Error("Canvas 2D is unavailable for this text hotspot.");
14046
14361
  }
14047
- drawTextHotspot(context, canvasWidth, canvasHeight, text, {
14048
- ...style,
14049
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * scale,
14050
- strokeWidth: Number.isFinite(style.strokeWidth) ? style.strokeWidth * scale : style.strokeWidth
14051
- });
14362
+ context.setTransform(
14363
+ canvasWidth / layout.canvasWidth,
14364
+ 0,
14365
+ 0,
14366
+ canvasHeight / layout.canvasHeight,
14367
+ 0,
14368
+ 0
14369
+ );
14370
+ drawTextHotspot(context, layout.canvasWidth, layout.canvasHeight, text, style);
14052
14371
  const texture = new CanvasTexture3(canvas);
14053
14372
  texture.colorSpace = SRGBColorSpace9;
14054
14373
  texture.wrapS = ClampToEdgeWrapping8;
@@ -14066,7 +14385,7 @@ function useRasterLongSide(width, height, scale, scaleMode, referenceFov) {
14066
14385
  const measure = () => {
14067
14386
  const pixelRatio = getPixelRatio();
14068
14387
  const viewportHeight = size.height * (Number.isFinite(pixelRatio) && pixelRatio > 0 ? pixelRatio : 1);
14069
- const cameraFov = camera instanceof PerspectiveCamera8 ? camera.fov : DEFAULT_REFERENCE_FOV2;
14388
+ const cameraFov = camera instanceof PerspectiveCamera9 ? camera.fov : DEFAULT_REFERENCE_FOV2;
14070
14389
  const screenLong = Math.max(
14071
14390
  projectedDevicePixels(
14072
14391
  width * resolveScale2(scale),
@@ -14108,13 +14427,7 @@ function useTextTexture(text, width, height, style, scale, scaleMode, referenceF
14108
14427
  let nextTexture = null;
14109
14428
  const run = async () => {
14110
14429
  try {
14111
- const layout = textureSize(width, height);
14112
- const layoutLong = Math.max(layout.canvasWidth, layout.canvasHeight);
14113
- const drawScale = rasterLong / layoutLong;
14114
- const { font } = resolveTextFont({
14115
- ...style,
14116
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * drawScale
14117
- });
14430
+ const { font } = resolveTextFont(style);
14118
14431
  try {
14119
14432
  await document.fonts?.load(font);
14120
14433
  } catch {
@@ -14343,7 +14656,7 @@ import {
14343
14656
  Matrix4 as Matrix45,
14344
14657
  Quaternion as Quaternion4,
14345
14658
  ShapeUtils,
14346
- Vector2 as Vector25,
14659
+ Vector2 as Vector26,
14347
14660
  Vector3 as Vector318
14348
14661
  } from "three";
14349
14662
  import { jsx as jsx35, jsxs as jsxs18 } from "react/jsx-runtime";
@@ -14381,7 +14694,7 @@ function intersects(a, b, c, d) {
14381
14694
  }
14382
14695
  function hasSelfIntersection(vertices) {
14383
14696
  const points = vertices.map(
14384
- (vertex) => new Vector25(vertex.unwrappedYaw, vertex.pitch)
14697
+ (vertex) => new Vector26(vertex.unwrappedYaw, vertex.pitch)
14385
14698
  );
14386
14699
  for (let index = 0; index < points.length; index += 1) {
14387
14700
  const nextIndex = (index + 1) % points.length;
@@ -14530,7 +14843,7 @@ function addSubdividedTriangle(positions, indices, a, b, c, worldToLocal, steps)
14530
14843
  }
14531
14844
  function makeFillGeometry(vertices, center) {
14532
14845
  const unwrapped = unwrapPolygonVertices(vertices);
14533
- const contour = unwrapped.map((vertex) => new Vector25(vertex.unwrappedYaw, vertex.pitch));
14846
+ const contour = unwrapped.map((vertex) => new Vector26(vertex.unwrappedYaw, vertex.pitch));
14534
14847
  const triangles = ShapeUtils.triangulateShape(contour, []);
14535
14848
  const positions = [];
14536
14849
  const indices = [];
@@ -16028,12 +16341,12 @@ import { useFrame as useFrame21, useThree as useThree26 } from "@react-three/fib
16028
16341
  import { useRef as useRef36 } from "react";
16029
16342
  import {
16030
16343
  DoubleSide as DoubleSide6,
16031
- Raycaster as Raycaster3,
16032
- Vector2 as Vector26,
16344
+ Raycaster as Raycaster4,
16345
+ Vector2 as Vector27,
16033
16346
  Vector3 as Vector321
16034
16347
  } from "three";
16035
16348
  import { jsx as jsx38, jsxs as jsxs19 } from "react/jsx-runtime";
16036
- var CENTER_NDC = new Vector26(0, 0);
16349
+ var CENTER_NDC = new Vector27(0, 0);
16037
16350
  var HUD_DISTANCE = 2;
16038
16351
  var RETICLE_SCALE = 1;
16039
16352
  var RETICLE_RENDER_ORDER = 1e5;
@@ -16119,7 +16432,7 @@ function WebVRReticle({
16119
16432
  const groupRef = useRef36(null);
16120
16433
  const ringMeshRef = useRef36(null);
16121
16434
  const directionRef = useRef36(new Vector321());
16122
- const raycasterRef = useRef36(new Raycaster3());
16435
+ const raycasterRef = useRef36(new Raycaster4());
16123
16436
  const hoveredRef = useRef36(null);
16124
16437
  const dwellRef = useRef36(0);
16125
16438
  const clickedRef = useRef36(false);
@@ -16289,12 +16602,12 @@ import { useContext as useContext26, useEffect as useEffect36, useMemo as useMem
16289
16602
  import {
16290
16603
  Mesh as Mesh6,
16291
16604
  OrthographicCamera,
16292
- PerspectiveCamera as PerspectiveCamera9,
16605
+ PerspectiveCamera as PerspectiveCamera10,
16293
16606
  PlaneGeometry as PlaneGeometry5,
16294
16607
  Scene,
16295
16608
  ShaderMaterial as ShaderMaterial3,
16296
16609
  StereoCamera as StereoCamera2,
16297
- Vector2 as Vector27,
16610
+ Vector2 as Vector28,
16298
16611
  Vector4 as Vector42,
16299
16612
  WebGLRenderTarget as WebGLRenderTarget2
16300
16613
  } from "three";
@@ -16332,7 +16645,7 @@ function createDistortionResources() {
16332
16645
  depthWrite: false,
16333
16646
  fragmentShader,
16334
16647
  uniforms: {
16335
- distortion: { value: new Vector27() },
16648
+ distortion: { value: new Vector28() },
16336
16649
  sourceTexture: { value: target.texture }
16337
16650
  },
16338
16651
  vertexShader
@@ -16380,7 +16693,7 @@ function WebVRStereoRenderer({
16380
16693
  const stereoRef = useRef37(stereo);
16381
16694
  stereoRef.current = stereo;
16382
16695
  const resources = useMemo25(createDistortionResources, []);
16383
- const drawingSizeRef = useRef37(new Vector27());
16696
+ const drawingSizeRef = useRef37(new Vector28());
16384
16697
  const viewportRef = useRef37(new Vector42());
16385
16698
  const scissorRef = useRef37(new Vector42());
16386
16699
  const originalFovRef = useRef37(null);
@@ -16400,7 +16713,7 @@ function WebVRStereoRenderer({
16400
16713
  useEffect36(() => {
16401
16714
  const originalRender = gl.render.bind(gl);
16402
16715
  gl.render = (sceneToRender, cameraToRender) => {
16403
- if (!stereoActiveRef.current || !(cameraToRender instanceof PerspectiveCamera9)) {
16716
+ if (!stereoActiveRef.current || !(cameraToRender instanceof PerspectiveCamera10)) {
16404
16717
  originalRender(sceneToRender, cameraToRender);
16405
16718
  return;
16406
16719
  }
@@ -16475,7 +16788,7 @@ function WebVRStereoRenderer({
16475
16788
  if (runtime) {
16476
16789
  runtime.stereoView.active = fallback;
16477
16790
  }
16478
- if (fallback && camera instanceof PerspectiveCamera9) {
16791
+ if (fallback && camera instanceof PerspectiveCamera10) {
16479
16792
  syncStereoCameras(
16480
16793
  stereoRef.current,
16481
16794
  camera,
@@ -16486,7 +16799,7 @@ function WebVRStereoRenderer({
16486
16799
  );
16487
16800
  return;
16488
16801
  }
16489
- if (originalFovRef.current !== null && camera instanceof PerspectiveCamera9) {
16802
+ if (originalFovRef.current !== null && camera instanceof PerspectiveCamera10) {
16490
16803
  camera.fov = originalFovRef.current;
16491
16804
  camera.updateProjectionMatrix();
16492
16805
  originalFovRef.current = null;