@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.cjs CHANGED
@@ -3769,12 +3769,18 @@ var PointerDragAnchor = class {
3769
3769
  this.projection = projection;
3770
3770
  this.local = new import_three2.Vector3();
3771
3771
  this.rotation = new import_three2.Euler(0, 0, 0, "YXZ");
3772
- 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(
3773
3776
  import_three2.MathUtils.degToRad(origin.pitch),
3774
3777
  import_three2.MathUtils.degToRad(-origin.yaw),
3775
3778
  0,
3776
3779
  "YXZ"
3777
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);
3778
3784
  }
3779
3785
  ray(point) {
3780
3786
  return this.local.set(
@@ -3783,6 +3789,21 @@ var PointerDragAnchor = class {
3783
3789
  0.5
3784
3790
  ).applyMatrix4(this.projection.projectionMatrixInverse).normalize();
3785
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
+ }
3786
3807
  solve(point, speed = DEFAULT_POINTER_DRAG_SPEED, invert = false) {
3787
3808
  const multiplier = (Number.isFinite(speed) && speed >= 0 ? speed : DEFAULT_POINTER_DRAG_SPEED) / DEFAULT_POINTER_DRAG_SPEED * (invert ? -1 : 1);
3788
3809
  if (multiplier === 0) return { yaw: 0, pitch: 0 };
@@ -3790,22 +3811,27 @@ var PointerDragAnchor = class {
3790
3811
  x: this.start.x + (point.x - this.start.x) * multiplier,
3791
3812
  y: this.start.y + (point.y - this.start.y) * multiplier
3792
3813
  });
3793
- const latitude = Math.asin(import_three2.MathUtils.clamp(this.anchor.y / Math.max(Math.hypot(ray.y, ray.z), 1e-12), -1, 1));
3794
- const offset = Math.atan2(ray.y, -ray.z);
3795
3814
  const originPitch = import_three2.MathUtils.degToRad(this.origin.pitch);
3796
- const candidates = [latitude - offset, Math.PI - latitude - offset, -Math.PI - latitude - offset];
3797
- let pitch = candidates[0];
3798
- let distance = Infinity;
3799
- for (const candidate of candidates) {
3800
- const bounded = import_three2.MathUtils.clamp(candidate, -Math.PI / 2, Math.PI / 2);
3801
- const nextDistance = Math.abs(candidate - bounded) * 10 + Math.abs(bounded - originPitch);
3802
- if (nextDistance < distance) {
3803
- pitch = bounded;
3804
- distance = nextDistance;
3805
- }
3806
- }
3807
- const z = Math.sin(pitch) * ray.y + Math.cos(pitch) * ray.z;
3808
- 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);
3809
3835
  return { yaw: normalizeViewYaw(yaw - this.origin.yaw), pitch: import_three2.MathUtils.radToDeg(pitch) - this.origin.pitch };
3810
3836
  }
3811
3837
  };
@@ -4001,7 +4027,10 @@ function MouseControls({
4001
4027
  const element = gl.domElement;
4002
4028
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "mouse");
4003
4029
  const allowedButtons = resolveButtons(buttons);
4030
+ const pending = /* @__PURE__ */ new Set();
4031
+ let disposed = false;
4004
4032
  const clearPointers = () => {
4033
+ pending.clear();
4005
4034
  for (const pointerId of pointersRef.current.keys()) {
4006
4035
  if (element.hasPointerCapture?.(pointerId)) {
4007
4036
  element.releasePointerCapture?.(pointerId);
@@ -4027,6 +4056,25 @@ function MouseControls({
4027
4056
  { cancelled }
4028
4057
  );
4029
4058
  };
4059
+ const beginPointer = (event) => {
4060
+ if (disposed || controlsRef.current?.isInteractionLocked()) {
4061
+ return;
4062
+ }
4063
+ if (pointersRef.current.has(event.pointerId)) {
4064
+ return;
4065
+ }
4066
+ pointersRef.current.set(event.pointerId, {
4067
+ x: event.clientX,
4068
+ y: event.clientY,
4069
+ at: performance.now()
4070
+ });
4071
+ try {
4072
+ element.setPointerCapture?.(event.pointerId);
4073
+ } catch {
4074
+ }
4075
+ syncActive();
4076
+ drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
4077
+ };
4030
4078
  const onPointerDown = (event) => {
4031
4079
  if (!isMouseLike(event)) {
4032
4080
  return;
@@ -4038,14 +4086,14 @@ function MouseControls({
4038
4086
  return;
4039
4087
  }
4040
4088
  event.preventDefault();
4041
- pointersRef.current.set(event.pointerId, {
4042
- x: event.clientX,
4043
- y: event.clientY,
4044
- at: performance.now()
4089
+ const pointerId = event.pointerId;
4090
+ pending.add(pointerId);
4091
+ queueMicrotask(() => {
4092
+ if (!pending.delete(pointerId)) {
4093
+ return;
4094
+ }
4095
+ beginPointer(event);
4045
4096
  });
4046
- element.setPointerCapture?.(event.pointerId);
4047
- syncActive();
4048
- drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
4049
4097
  };
4050
4098
  const onPointerMove = (event) => {
4051
4099
  if (!isMouseLike(event)) {
@@ -4069,6 +4117,7 @@ function MouseControls({
4069
4117
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
4070
4118
  };
4071
4119
  const releasePointer = (event) => {
4120
+ pending.delete(event.pointerId);
4072
4121
  if (!pointersRef.current.has(event.pointerId)) {
4073
4122
  return;
4074
4123
  }
@@ -4110,6 +4159,7 @@ function MouseControls({
4110
4159
  element.addEventListener("lostpointercapture", releasePointer);
4111
4160
  element.addEventListener("wheel", onWheel, { passive: false });
4112
4161
  return () => {
4162
+ disposed = true;
4113
4163
  validateDragRef.current = void 0;
4114
4164
  element.removeEventListener("pointerdown", onPointerDown);
4115
4165
  element.removeEventListener("pointermove", onPointerMove);
@@ -5604,6 +5654,12 @@ function vector3ToPanoPosition(vector) {
5604
5654
  )
5605
5655
  });
5606
5656
  }
5657
+ function translateHotspotPosition(origin, from, to) {
5658
+ return normalizePanoPosition({
5659
+ yaw: origin.yaw + normalizePanoYaw(to.yaw - from.yaw),
5660
+ pitch: origin.pitch + (to.pitch - from.pitch)
5661
+ });
5662
+ }
5607
5663
 
5608
5664
  // src/panorama-radius.ts
5609
5665
  var DEFAULT_PANORAMA_RADIUS = 1e3;
@@ -5747,7 +5803,10 @@ function TouchControls({
5747
5803
  (0, import_react18.useEffect)(() => {
5748
5804
  const element = gl.domElement;
5749
5805
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "touch");
5806
+ const pending = /* @__PURE__ */ new Set();
5807
+ let disposed = false;
5750
5808
  const clearPointers = () => {
5809
+ pending.clear();
5751
5810
  for (const pointerId of pointersRef.current.keys()) {
5752
5811
  if (element.hasPointerCapture?.(pointerId)) {
5753
5812
  element.releasePointerCapture?.(pointerId);
@@ -5773,20 +5832,22 @@ function TouchControls({
5773
5832
  { cancelled }
5774
5833
  );
5775
5834
  };
5776
- const onPointerDown = (event) => {
5777
- if (event.pointerType !== "touch") {
5835
+ const beginPointer = (event) => {
5836
+ if (disposed || controlsRef.current?.isInteractionLocked()) {
5778
5837
  return;
5779
5838
  }
5780
- if (controlsRef.current?.isInteractionLocked()) {
5839
+ if (pointersRef.current.has(event.pointerId)) {
5781
5840
  return;
5782
5841
  }
5783
- event.preventDefault();
5784
5842
  pointersRef.current.set(event.pointerId, {
5785
5843
  x: event.clientX,
5786
5844
  y: event.clientY,
5787
5845
  at: performance.now()
5788
5846
  });
5789
- element.setPointerCapture?.(event.pointerId);
5847
+ try {
5848
+ element.setPointerCapture?.(event.pointerId);
5849
+ } catch {
5850
+ }
5790
5851
  syncActive();
5791
5852
  drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5792
5853
  if (pointersRef.current.size >= 2) {
@@ -5795,6 +5856,23 @@ function TouchControls({
5795
5856
  pinchDistanceRef.current = pointerDistance(first, second);
5796
5857
  }
5797
5858
  };
5859
+ const onPointerDown = (event) => {
5860
+ if (event.pointerType !== "touch") {
5861
+ return;
5862
+ }
5863
+ if (controlsRef.current?.isInteractionLocked()) {
5864
+ return;
5865
+ }
5866
+ event.preventDefault();
5867
+ const pointerId = event.pointerId;
5868
+ pending.add(pointerId);
5869
+ queueMicrotask(() => {
5870
+ if (!pending.delete(pointerId)) {
5871
+ return;
5872
+ }
5873
+ beginPointer(event);
5874
+ });
5875
+ };
5798
5876
  const onPointerMove = (event) => {
5799
5877
  if (event.pointerType !== "touch") {
5800
5878
  return;
@@ -5838,6 +5916,7 @@ function TouchControls({
5838
5916
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5839
5917
  };
5840
5918
  const releasePointer = (event) => {
5919
+ pending.delete(event.pointerId);
5841
5920
  if (!pointersRef.current.has(event.pointerId)) {
5842
5921
  return;
5843
5922
  }
@@ -5861,6 +5940,7 @@ function TouchControls({
5861
5940
  element.addEventListener("pointercancel", releasePointer);
5862
5941
  element.addEventListener("lostpointercapture", releasePointer);
5863
5942
  return () => {
5943
+ disposed = true;
5864
5944
  validateDragRef.current = void 0;
5865
5945
  element.removeEventListener("pointerdown", onPointerDown);
5866
5946
  element.removeEventListener("pointermove", onPointerMove);
@@ -11410,6 +11490,93 @@ var import_fiber21 = require("@react-three/fiber");
11410
11490
  var import_react42 = require("react");
11411
11491
  var import_three24 = require("three");
11412
11492
 
11493
+ // src/hotspot/drag-session.ts
11494
+ var CLICK_SUPPRESS_MS = 1e3;
11495
+ function trackPointerDrag(source, element, pointerId, onMove, onEnd) {
11496
+ let stopped = false;
11497
+ const releaseCapture = () => {
11498
+ if (!element.hasPointerCapture?.(pointerId)) {
11499
+ return;
11500
+ }
11501
+ try {
11502
+ element.releasePointerCapture?.(pointerId);
11503
+ } catch {
11504
+ }
11505
+ };
11506
+ const stop = () => {
11507
+ if (stopped) {
11508
+ return;
11509
+ }
11510
+ stopped = true;
11511
+ source.removeEventListener("pointermove", onPointerMove, true);
11512
+ source.removeEventListener("pointerup", onPointerUp, true);
11513
+ source.removeEventListener("pointercancel", onPointerCancel, true);
11514
+ element.removeEventListener("lostpointercapture", onLostCapture);
11515
+ releaseCapture();
11516
+ };
11517
+ const finish = (event) => {
11518
+ if (stopped || event.pointerId !== pointerId) {
11519
+ return;
11520
+ }
11521
+ stop();
11522
+ onEnd(event);
11523
+ };
11524
+ const onPointerMove = (event) => {
11525
+ if (stopped || event.pointerId !== pointerId) {
11526
+ return;
11527
+ }
11528
+ try {
11529
+ event.preventDefault?.();
11530
+ } catch {
11531
+ }
11532
+ onMove(event);
11533
+ };
11534
+ const onPointerUp = (event) => {
11535
+ finish(event);
11536
+ };
11537
+ const onPointerCancel = (event) => {
11538
+ finish(event);
11539
+ };
11540
+ const onLostCapture = (event) => {
11541
+ if ((event.buttons ?? 0) > 0) {
11542
+ return;
11543
+ }
11544
+ finish(event);
11545
+ };
11546
+ try {
11547
+ element.setPointerCapture?.(pointerId);
11548
+ } catch {
11549
+ }
11550
+ source.addEventListener("pointermove", onPointerMove, true);
11551
+ source.addEventListener("pointerup", onPointerUp, true);
11552
+ source.addEventListener("pointercancel", onPointerCancel, true);
11553
+ element.addEventListener("lostpointercapture", onLostCapture);
11554
+ return stop;
11555
+ }
11556
+ function suppressFollowingClick(target) {
11557
+ let cleaned = false;
11558
+ const cleanup = () => {
11559
+ if (cleaned) {
11560
+ return;
11561
+ }
11562
+ cleaned = true;
11563
+ target.removeEventListener("click", swallowClick, true);
11564
+ target.removeEventListener("pointerdown", cancel, true);
11565
+ };
11566
+ const swallowClick = (event) => {
11567
+ event.preventDefault();
11568
+ event.stopPropagation();
11569
+ event.stopImmediatePropagation();
11570
+ cleanup();
11571
+ };
11572
+ const cancel = () => {
11573
+ cleanup();
11574
+ };
11575
+ target.addEventListener("click", swallowClick, true);
11576
+ target.addEventListener("pointerdown", cancel, true);
11577
+ setTimeout(cleanup, CLICK_SUPPRESS_MS);
11578
+ }
11579
+
11413
11580
  // src/hotspot/hotspot-tooltip.tsx
11414
11581
  var import_react40 = require("react");
11415
11582
 
@@ -11669,6 +11836,21 @@ function fixedScaleFactor(cameraFov, referenceFov) {
11669
11836
  const safeReferenceFov = import_three22.MathUtils.clamp(referenceFov, 1, 179);
11670
11837
  return Math.tan(import_three22.MathUtils.degToRad(cameraFov) / 2) / Math.tan(import_three22.MathUtils.degToRad(safeReferenceFov) / 2);
11671
11838
  }
11839
+ function selectionFrameScale(worldScaleX, worldScaleY, cameraSpaceDepth, fovDegrees, viewportHeight, outsetPixels) {
11840
+ const pixels = Number.isFinite(outsetPixels) && outsetPixels > 0 ? outsetPixels : 0;
11841
+ const depth = Number.isFinite(cameraSpaceDepth) ? Math.max(cameraSpaceDepth, 1e-4) : 1e-4;
11842
+ const height = Number.isFinite(viewportHeight) ? Math.max(viewportHeight, 1) : 1;
11843
+ const fov = Number.isFinite(fovDegrees) ? import_three22.MathUtils.clamp(fovDegrees, 1, 179) : 75;
11844
+ const pixelsPerWorldUnit = height / (2 * Math.tan(import_three22.MathUtils.degToRad(fov) * 0.5) * depth);
11845
+ const axis = (worldScale) => {
11846
+ const pixelsPerLocalUnit = pixelsPerWorldUnit * Math.abs(worldScale);
11847
+ if (pixelsPerLocalUnit < 1e-4) {
11848
+ return 1;
11849
+ }
11850
+ return 1 + 2 * pixels / pixelsPerLocalUnit;
11851
+ };
11852
+ return [axis(worldScaleX), axis(worldScaleY)];
11853
+ }
11672
11854
 
11673
11855
  // src/hotspot/hotspot-stroke-line.tsx
11674
11856
  var import_drei = require("@react-three/drei");
@@ -11751,6 +11933,11 @@ var DEFAULT_REFERENCE_FOV = 75;
11751
11933
  var MIN_ANGULAR_SIZE = 0.01;
11752
11934
  var MAX_ANGULAR_SIZE = 179;
11753
11935
  var DRAG_EPSILON_DEGREES = 1e-3;
11936
+ var SELECTION_BORDER_WIDTH = 3;
11937
+ var SELECTION_BORDER_GAP = 3;
11938
+ var SELECTION_BORDER_OUTSET = SELECTION_BORDER_GAP + SELECTION_BORDER_WIDTH / 2;
11939
+ var SELECTION_CAMERA_POSITION = new import_three24.Vector3();
11940
+ var SELECTION_WORLD_SCALE = new import_three24.Vector3();
11754
11941
  var SELECTION_BORDER_POINTS = [
11755
11942
  [-0.5, -0.5, 1e-3],
11756
11943
  [0.5, -0.5, 1e-3],
@@ -11838,6 +12025,34 @@ function angularDistance(a, b) {
11838
12025
  const bVector = panoPositionToVector3(b);
11839
12026
  return import_three24.MathUtils.radToDeg(aVector.angleTo(bVector));
11840
12027
  }
12028
+ var DRAG_POINTER = new import_three24.Vector2();
12029
+ var DRAG_RAYCASTER = new import_three24.Raycaster();
12030
+ function aimDragRaycaster(event, element, camera) {
12031
+ const rect = element.getBoundingClientRect();
12032
+ if (!(rect.width > 0) || !(rect.height > 0)) {
12033
+ return false;
12034
+ }
12035
+ DRAG_POINTER.set(
12036
+ (event.clientX - rect.left) / rect.width * 2 - 1,
12037
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
12038
+ );
12039
+ camera.updateMatrixWorld();
12040
+ DRAG_RAYCASTER.setFromCamera(DRAG_POINTER, camera);
12041
+ return true;
12042
+ }
12043
+ function pointerToPanoPosition(event, element, camera) {
12044
+ if (!aimDragRaycaster(event, element, camera)) {
12045
+ return null;
12046
+ }
12047
+ return vector3ToPanoPosition(DRAG_RAYCASTER.ray.direction);
12048
+ }
12049
+ function pointerHitsHotspot(event, element, camera, group) {
12050
+ if (!group || !aimDragRaycaster(event, element, camera)) {
12051
+ return false;
12052
+ }
12053
+ group.updateMatrixWorld(true);
12054
+ return DRAG_RAYCASTER.intersectObject(group, true).length > 0;
12055
+ }
11841
12056
  function isTooltipOpen(trigger, hovered, focused, pinned) {
11842
12057
  switch (trigger) {
11843
12058
  case "always":
@@ -12033,6 +12248,24 @@ function updateTooltipAnchor(anchor, group, placement, useAngularScale, camera,
12033
12248
  }
12034
12249
  setTooltipAnchorOnLocalBox(anchor, placement, -0.5, 0.5, -0.5, 0.5);
12035
12250
  }
12251
+ function placeSelectionFrame(frame, group, camera, viewportHeight) {
12252
+ if (!frame) {
12253
+ return;
12254
+ }
12255
+ group.getWorldScale(SELECTION_WORLD_SCALE);
12256
+ SELECTION_CAMERA_POSITION.setFromMatrixPosition(group.matrixWorld).applyMatrix4(
12257
+ camera.matrixWorldInverse
12258
+ );
12259
+ const [scaleX, scaleY] = selectionFrameScale(
12260
+ SELECTION_WORLD_SCALE.x,
12261
+ SELECTION_WORLD_SCALE.y,
12262
+ -SELECTION_CAMERA_POSITION.z,
12263
+ camera instanceof import_three24.PerspectiveCamera ? camera.fov : DEFAULT_REFERENCE_FOV,
12264
+ viewportHeight,
12265
+ SELECTION_BORDER_OUTSET
12266
+ );
12267
+ frame.scale.set(scaleX, scaleY, 1);
12268
+ }
12036
12269
  function HotspotAnchor({
12037
12270
  id,
12038
12271
  position,
@@ -12072,10 +12305,13 @@ function HotspotAnchor({
12072
12305
  const hotspotTargetRegistry = (0, import_react42.useContext)(HotspotTargetRegistryContext);
12073
12306
  const eventBus = (0, import_react42.useContext)(PanoEventBusContext);
12074
12307
  const cursorApi = usePanoCursor();
12075
- const { camera, size } = (0, import_fiber21.useThree)();
12308
+ const { camera, gl, size } = (0, import_fiber21.useThree)();
12076
12309
  const groupRef = (0, import_react42.useRef)(null);
12310
+ const selectionFrameRef = (0, import_react42.useRef)(null);
12077
12311
  const tooltipAnchorRef = (0, import_react42.useRef)(null);
12078
12312
  const dragStateRef = (0, import_react42.useRef)(null);
12313
+ const updateDragRef = (0, import_react42.useRef)(() => void 0);
12314
+ const endDragRef = (0, import_react42.useRef)(() => void 0);
12079
12315
  const suppressNextClickRef = (0, import_react42.useRef)(false);
12080
12316
  const [hovered, setHovered] = (0, import_react42.useState)(false);
12081
12317
  const [dragging, setDragging] = (0, import_react42.useState)(false);
@@ -12145,7 +12381,9 @@ function HotspotAnchor({
12145
12381
  const cursorClaimId = hotspotCursorClaimId(id);
12146
12382
  (0, import_react42.useEffect)(
12147
12383
  () => () => {
12148
- dragStateRef.current?.releaseInteractionLock();
12384
+ const dragState = dragStateRef.current;
12385
+ dragState?.stopTracking();
12386
+ dragState?.releaseInteractionLock();
12149
12387
  dragStateRef.current = null;
12150
12388
  },
12151
12389
  []
@@ -12202,6 +12440,7 @@ function HotspotAnchor({
12202
12440
  if (!dragState) {
12203
12441
  return;
12204
12442
  }
12443
+ dragState.stopTracking();
12205
12444
  dragState.releaseInteractionLock();
12206
12445
  dragStateRef.current = null;
12207
12446
  setDragging(false);
@@ -12224,6 +12463,7 @@ function HotspotAnchor({
12224
12463
  worldHeight,
12225
12464
  resolvedDistance
12226
12465
  );
12466
+ placeSelectionFrame(selectionFrameRef.current, group, camera, size.height);
12227
12467
  if (tooltipOpen && tooltipAnchorRef.current) {
12228
12468
  updateTooltipAnchor(
12229
12469
  tooltipAnchorRef.current,
@@ -12242,6 +12482,7 @@ function HotspotAnchor({
12242
12482
  rotation,
12243
12483
  scaleMode,
12244
12484
  size,
12485
+ selected,
12245
12486
  surfaceQuaternion3,
12246
12487
  tooltipOpen,
12247
12488
  tooltipPlacement,
@@ -12267,6 +12508,12 @@ function HotspotAnchor({
12267
12508
  worldHeight,
12268
12509
  resolvedDistance
12269
12510
  );
12511
+ placeSelectionFrame(
12512
+ selectionFrameRef.current,
12513
+ group,
12514
+ state.camera,
12515
+ state.size.height
12516
+ );
12270
12517
  if (tooltipOpen && tooltipAnchorRef.current) {
12271
12518
  updateTooltipAnchor(
12272
12519
  tooltipAnchorRef.current,
@@ -12282,31 +12529,78 @@ function HotspotAnchor({
12282
12529
  event.stopPropagation();
12283
12530
  event.nativeEvent.stopImmediatePropagation?.();
12284
12531
  };
12285
- const releasePointer = (event, emitDragEnd) => {
12532
+ const dragPosition = (dragState, event) => {
12533
+ const pointerPosition = pointerToPanoPosition(event, gl.domElement, camera);
12534
+ if (!pointerPosition) {
12535
+ return dragState.lastPosition;
12536
+ }
12537
+ return translateHotspotPosition(
12538
+ dragState.startPosition,
12539
+ dragState.pointerOrigin,
12540
+ pointerPosition
12541
+ );
12542
+ };
12543
+ updateDragRef.current = (nativeEvent) => {
12286
12544
  const dragState = dragStateRef.current;
12287
- if (!dragState || dragState.pointerId !== event.pointerId) {
12545
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12288
12546
  return;
12289
12547
  }
12290
- stopPointerEvent(event);
12291
- const target = event.target;
12292
- if (target.hasPointerCapture?.(event.pointerId)) {
12293
- target.releasePointerCapture?.(event.pointerId);
12294
- }
12295
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12296
- if (emitDragEnd) {
12297
- onDragEnd?.({
12298
- ...makeInteractionEvent(id, nextPosition, event),
12299
- source: "pointer",
12300
- startPosition: dragState.startPosition
12301
- });
12548
+ const nextPosition = dragPosition(dragState, nativeEvent);
12549
+ dragState.lastPosition = nextPosition;
12550
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12551
+ onPositionChange?.({
12552
+ id,
12553
+ position: nextPosition,
12554
+ source: "pointer",
12555
+ nativeEvent,
12556
+ startPosition: dragState.startPosition
12557
+ });
12558
+ };
12559
+ endDragRef.current = (nativeEvent, emitDragEnd) => {
12560
+ const dragState = dragStateRef.current;
12561
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12562
+ return;
12302
12563
  }
12303
- suppressNextClickRef.current = dragState.moved;
12564
+ const nextPosition = dragPosition(dragState, nativeEvent);
12565
+ dragState.lastPosition = nextPosition;
12566
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12567
+ const moved = dragState.moved;
12568
+ const startPosition = dragState.startPosition;
12569
+ dragState.stopTracking();
12304
12570
  dragState.releaseInteractionLock();
12305
12571
  dragStateRef.current = null;
12572
+ suppressNextClickRef.current = moved;
12306
12573
  setDragging(false);
12307
- if (emitDragEnd) {
12308
- setHovered(true);
12574
+ if (!emitDragEnd) {
12575
+ return;
12576
+ }
12577
+ const overHotspot = pointerHitsHotspot(
12578
+ nativeEvent,
12579
+ gl.domElement,
12580
+ camera,
12581
+ groupRef.current
12582
+ );
12583
+ eventHovered.current = overHotspot;
12584
+ setHovered(overHotspot);
12585
+ if (moved && typeof window !== "undefined") {
12586
+ suppressFollowingClick(window);
12309
12587
  }
12588
+ if (moved) {
12589
+ onPositionChange?.({
12590
+ id,
12591
+ position: nextPosition,
12592
+ source: "pointer",
12593
+ nativeEvent,
12594
+ startPosition
12595
+ });
12596
+ }
12597
+ onDragEnd?.({
12598
+ id,
12599
+ position: nextPosition,
12600
+ source: "pointer",
12601
+ nativeEvent,
12602
+ startPosition
12603
+ });
12310
12604
  };
12311
12605
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
12312
12606
  "group",
@@ -12326,23 +12620,42 @@ function HotspotAnchor({
12326
12620
  }
12327
12621
  suppressNextClickRef.current = false;
12328
12622
  } : void 0,
12329
- onLostPointerCapture: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12330
- onPointerCancel: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12623
+ onLostPointerCapture: acceptsPointer ? (event) => {
12624
+ endDragRef.current(event.nativeEvent, true);
12625
+ } : void 0,
12626
+ onPointerCancel: acceptsPointer ? (event) => {
12627
+ endDragRef.current(event.nativeEvent, true);
12628
+ } : void 0,
12331
12629
  onPointerDown: acceptsPointer ? (event) => {
12332
- stopPointerEvent(event);
12630
+ const native = event.nativeEvent;
12333
12631
  if (!draggable || dragStateRef.current) {
12632
+ stopPointerEvent(event);
12334
12633
  return;
12335
12634
  }
12635
+ const pointerId = native.pointerId;
12636
+ stopPointerEvent(event);
12336
12637
  const releaseInteractionLock = controlsRef?.current?.acquireInteractionLock() ?? (() => void 0);
12337
- const target = event.target;
12338
- target.setPointerCapture?.(event.pointerId);
12638
+ const pointerOrigin = pointerToPanoPosition(native, gl.domElement, camera) ?? vector3ToPanoPosition(event.ray.direction);
12339
12639
  const startPosition = { ...normalizedPosition };
12640
+ const tracksOutsideMesh = typeof window !== "undefined";
12641
+ const stopTracking = tracksOutsideMesh ? trackPointerDrag(
12642
+ window,
12643
+ gl.domElement,
12644
+ pointerId,
12645
+ (pointerEvent) => updateDragRef.current(pointerEvent),
12646
+ (pointerEvent) => endDragRef.current(pointerEvent, true)
12647
+ ) : () => void 0;
12340
12648
  dragStateRef.current = {
12341
- pointerId: event.pointerId,
12649
+ pointerId,
12342
12650
  startPosition,
12651
+ pointerOrigin,
12652
+ lastPosition: startPosition,
12343
12653
  releaseInteractionLock,
12344
- moved: false
12654
+ moved: false,
12655
+ tracksOutsideMesh,
12656
+ stopTracking
12345
12657
  };
12658
+ eventHovered.current = false;
12346
12659
  setHovered(false);
12347
12660
  setDragging(true);
12348
12661
  onDragStart?.({
@@ -12353,17 +12666,13 @@ function HotspotAnchor({
12353
12666
  } : void 0,
12354
12667
  onPointerMove: acceptsPointer ? (event) => {
12355
12668
  const dragState = dragStateRef.current;
12356
- if (!dragState || dragState.pointerId !== event.pointerId) {
12669
+ if (!dragState || dragState.pointerId !== event.nativeEvent.pointerId) {
12357
12670
  return;
12358
12671
  }
12359
12672
  stopPointerEvent(event);
12360
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12361
- dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12362
- onPositionChange?.({
12363
- ...makeInteractionEvent(id, nextPosition, event),
12364
- source: "pointer",
12365
- startPosition: dragState.startPosition
12366
- });
12673
+ if (!dragState.tracksOutsideMesh) {
12674
+ updateDragRef.current(event.nativeEvent);
12675
+ }
12367
12676
  } : void 0,
12368
12677
  onPointerOut: acceptsPointer ? (event) => {
12369
12678
  if (!dragStateRef.current) {
@@ -12392,7 +12701,10 @@ function HotspotAnchor({
12392
12701
  );
12393
12702
  }
12394
12703
  } : void 0,
12395
- onPointerUp: acceptsPointer ? (event) => releasePointer(event, true) : void 0,
12704
+ onPointerUp: acceptsPointer ? (event) => {
12705
+ stopPointerEvent(event);
12706
+ endDragRef.current(event.nativeEvent, true);
12707
+ } : void 0,
12396
12708
  children: [
12397
12709
  focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12398
12710
  "lineSegments",
@@ -12405,16 +12717,16 @@ function HotspotAnchor({
12405
12717
  }
12406
12718
  ) : null,
12407
12719
  children,
12408
- selected && useAngularScale ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12720
+ selected && useAngularScale ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("group", { ref: selectionFrameRef, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12409
12721
  HotspotStrokeLine,
12410
12722
  {
12411
12723
  points: SELECTION_BORDER_POINTS,
12412
12724
  color: "#ffb300",
12413
- lineWidth: 3,
12725
+ lineWidth: SELECTION_BORDER_WIDTH,
12414
12726
  opacity: 1,
12415
12727
  renderOrder: renderOrder + 1
12416
12728
  }
12417
- ) : null,
12729
+ ) }) : null,
12418
12730
  tooltipOpen && tooltipContent ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("group", { ref: tooltipAnchorRef, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12419
12731
  HotspotTooltip,
12420
12732
  {
@@ -13917,11 +14229,15 @@ function createTextTexture(text, width, height, style, rasterLong) {
13917
14229
  if (!context) {
13918
14230
  throw new Error("Canvas 2D is unavailable for this text hotspot.");
13919
14231
  }
13920
- drawTextHotspot(context, canvasWidth, canvasHeight, text, {
13921
- ...style,
13922
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * scale,
13923
- strokeWidth: Number.isFinite(style.strokeWidth) ? style.strokeWidth * scale : style.strokeWidth
13924
- });
14232
+ context.setTransform(
14233
+ canvasWidth / layout.canvasWidth,
14234
+ 0,
14235
+ 0,
14236
+ canvasHeight / layout.canvasHeight,
14237
+ 0,
14238
+ 0
14239
+ );
14240
+ drawTextHotspot(context, layout.canvasWidth, layout.canvasHeight, text, style);
13925
14241
  const texture = new import_three32.CanvasTexture(canvas);
13926
14242
  texture.colorSpace = import_three32.SRGBColorSpace;
13927
14243
  texture.wrapS = import_three32.ClampToEdgeWrapping;
@@ -13981,13 +14297,7 @@ function useTextTexture(text, width, height, style, scale, scaleMode, referenceF
13981
14297
  let nextTexture = null;
13982
14298
  const run = async () => {
13983
14299
  try {
13984
- const layout = textureSize(width, height);
13985
- const layoutLong = Math.max(layout.canvasWidth, layout.canvasHeight);
13986
- const drawScale = rasterLong / layoutLong;
13987
- const { font } = resolveTextFont({
13988
- ...style,
13989
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * drawScale
13990
- });
14300
+ const { font } = resolveTextFont(style);
13991
14301
  try {
13992
14302
  await document.fonts?.load(font);
13993
14303
  } catch {