@ericchen1990/pano-view 3.6.3 → 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/README.zh-CN.md CHANGED
@@ -60,15 +60,15 @@
60
60
  | 更新 | 生成并发布文件,再管理入口引用和缓存 | 无需发布静态文件;生效时间仍取决于后端和客户端缓存 |
61
61
  | 权限与个性化 | 公开文件适合公开内容;私有分发需要托管/CDN 访问控制 | 后端可按用户、组织鉴权并返回不同配置 |
62
62
  | 可用性与维护 | 依赖静态托管/CDN;维护发布及缓存更新流程 | 依赖 API 及其数据库/缓存服务;维护查询、鉴权及缓存失效 |
63
- | 适用场景 | 公开、已发布的导览;更新较少;读取流量大 | 编辑预览、私有内容、频繁更新和个性化 |
63
+ | 适用场景 | 公开、已发布的漫游;更新较少;读取流量大 | 编辑预览、私有内容、频繁更新和个性化 |
64
64
 
65
- **公开、已发布的导览优先使用 JSON + CDN;编辑预览、私有和个性化内容优先使用数据库 + API。** 同时需要两者时,以数据库作为编辑来源,发布时生成 JSON 快照。这种混合方式需要应用自己的发布流程;包不会自动生成或同步快照。
65
+ **公开、已发布的漫游优先使用 JSON + CDN;编辑预览、私有和个性化内容优先使用数据库 + API。** 同时需要两者时,以数据库作为编辑来源,发布时生成 JSON 快照。这种混合方式需要应用自己的发布流程;包不会自动生成或同步快照。
66
66
 
67
67
  两条路径均不会自动轮询或订阅配置变化。更新已打开的页面时,宿主应用需要重新加载配置,并将新数据传给查看器。未命中缓存的 API 读取可以返回当前数据库值,但不会让已有查看器自动更新。
68
68
 
69
69
  ### 数据库配置接入
70
70
 
71
- 后端先查询导览并完成鉴权,再将需要公开给查看器的配置字段映射为项目。下例的 `readAuthorizedTour` 由宿主应用实现,负责数据库读取和访问检查,不是包提供的 API。它返回的场景记录包含转换时保存的精简 `viewerConfig`、资源目录、视角和热点定义:
71
+ 后端先查询漫游并完成鉴权,再将需要公开给查看器的配置字段映射为项目。下例的 `readAuthorizedTour` 由宿主应用实现,负责数据库读取和访问检查,不是包提供的 API。它返回的场景记录包含转换时保存的精简 `viewerConfig`、资源目录、视角和热点定义:
72
72
 
73
73
  ```ts
74
74
  import { loadPanoProject } from "@ericchen1990/pano-view/data";
@@ -1826,7 +1826,7 @@ import { VideoHotspot } from "@ericchen1990/pano-view";
1826
1826
 
1827
1827
  `playing` 与 `VideoHotspot` 一样受控。`loop` 默认 `false`,`muted` 默认 `false`,`volume` 默认 `1`,`pauseWhenHidden` 默认 `true`(标签页隐藏时暂停,`playing` 仍为 `true` 时,在标签页恢复可见后继续播放)。首次用户手势会解锁音频;若自动播放被阻止,会报告 `"blocked"`,并在 `playing` 仍为 true 时于下一次指针或按键后重试 `play()`。
1828
1828
 
1829
- 默认标记在 **停止**(扬声器)与 **播放**(扬声器 + 声波)之间切换。可用 `icon` 自定义停止图标、`playingIcon` 自定义播放图标。只设 `icon` 时两种状态共用该图。`marker={false}` 隐藏标记,仍保留定点声源。整个导览共用或按场景切换的非空间背景音频请使用 [`BackgroundAudio`](#backgroundaudio)。
1829
+ 默认标记在 **停止**(扬声器)与 **播放**(扬声器 + 声波)之间切换。可用 `icon` 自定义停止图标、`playingIcon` 自定义播放图标。只设 `icon` 时两种状态共用该图。`marker={false}` 隐藏标记,仍保留定点声源。整个漫游共用或按场景切换的非空间背景音频请使用 [`BackgroundAudio`](#backgroundaudio)。
1830
1830
 
1831
1831
  ```tsx
1832
1832
  import { AudioHotspot } from "@ericchen1990/pano-view";
package/dist/index.cjs CHANGED
@@ -4027,7 +4027,10 @@ function MouseControls({
4027
4027
  const element = gl.domElement;
4028
4028
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "mouse");
4029
4029
  const allowedButtons = resolveButtons(buttons);
4030
+ const pending = /* @__PURE__ */ new Set();
4031
+ let disposed = false;
4030
4032
  const clearPointers = () => {
4033
+ pending.clear();
4031
4034
  for (const pointerId of pointersRef.current.keys()) {
4032
4035
  if (element.hasPointerCapture?.(pointerId)) {
4033
4036
  element.releasePointerCapture?.(pointerId);
@@ -4053,6 +4056,25 @@ function MouseControls({
4053
4056
  { cancelled }
4054
4057
  );
4055
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
+ };
4056
4078
  const onPointerDown = (event) => {
4057
4079
  if (!isMouseLike(event)) {
4058
4080
  return;
@@ -4064,14 +4086,14 @@ function MouseControls({
4064
4086
  return;
4065
4087
  }
4066
4088
  event.preventDefault();
4067
- pointersRef.current.set(event.pointerId, {
4068
- x: event.clientX,
4069
- y: event.clientY,
4070
- 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);
4071
4096
  });
4072
- element.setPointerCapture?.(event.pointerId);
4073
- syncActive();
4074
- drag.start({ x: event.clientX, y: event.clientY }, pointerAt(event));
4075
4097
  };
4076
4098
  const onPointerMove = (event) => {
4077
4099
  if (!isMouseLike(event)) {
@@ -4095,6 +4117,7 @@ function MouseControls({
4095
4117
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt(event));
4096
4118
  };
4097
4119
  const releasePointer = (event) => {
4120
+ pending.delete(event.pointerId);
4098
4121
  if (!pointersRef.current.has(event.pointerId)) {
4099
4122
  return;
4100
4123
  }
@@ -4136,6 +4159,7 @@ function MouseControls({
4136
4159
  element.addEventListener("lostpointercapture", releasePointer);
4137
4160
  element.addEventListener("wheel", onWheel, { passive: false });
4138
4161
  return () => {
4162
+ disposed = true;
4139
4163
  validateDragRef.current = void 0;
4140
4164
  element.removeEventListener("pointerdown", onPointerDown);
4141
4165
  element.removeEventListener("pointermove", onPointerMove);
@@ -5630,6 +5654,12 @@ function vector3ToPanoPosition(vector) {
5630
5654
  )
5631
5655
  });
5632
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
+ }
5633
5663
 
5634
5664
  // src/panorama-radius.ts
5635
5665
  var DEFAULT_PANORAMA_RADIUS = 1e3;
@@ -5773,7 +5803,10 @@ function TouchControls({
5773
5803
  (0, import_react18.useEffect)(() => {
5774
5804
  const element = gl.domElement;
5775
5805
  const drag = createPointerDrag(camera, element, () => controlsRef.current, "touch");
5806
+ const pending = /* @__PURE__ */ new Set();
5807
+ let disposed = false;
5776
5808
  const clearPointers = () => {
5809
+ pending.clear();
5777
5810
  for (const pointerId of pointersRef.current.keys()) {
5778
5811
  if (element.hasPointerCapture?.(pointerId)) {
5779
5812
  element.releasePointerCapture?.(pointerId);
@@ -5799,20 +5832,22 @@ function TouchControls({
5799
5832
  { cancelled }
5800
5833
  );
5801
5834
  };
5802
- const onPointerDown = (event) => {
5803
- if (event.pointerType !== "touch") {
5835
+ const beginPointer = (event) => {
5836
+ if (disposed || controlsRef.current?.isInteractionLocked()) {
5804
5837
  return;
5805
5838
  }
5806
- if (controlsRef.current?.isInteractionLocked()) {
5839
+ if (pointersRef.current.has(event.pointerId)) {
5807
5840
  return;
5808
5841
  }
5809
- event.preventDefault();
5810
5842
  pointersRef.current.set(event.pointerId, {
5811
5843
  x: event.clientX,
5812
5844
  y: event.clientY,
5813
5845
  at: performance.now()
5814
5846
  });
5815
- element.setPointerCapture?.(event.pointerId);
5847
+ try {
5848
+ element.setPointerCapture?.(event.pointerId);
5849
+ } catch {
5850
+ }
5816
5851
  syncActive();
5817
5852
  drag.start({ x: event.clientX, y: event.clientY }, pointerAt2(event));
5818
5853
  if (pointersRef.current.size >= 2) {
@@ -5821,6 +5856,23 @@ function TouchControls({
5821
5856
  pinchDistanceRef.current = pointerDistance(first, second);
5822
5857
  }
5823
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
+ };
5824
5876
  const onPointerMove = (event) => {
5825
5877
  if (event.pointerType !== "touch") {
5826
5878
  return;
@@ -5864,6 +5916,7 @@ function TouchControls({
5864
5916
  drag.move(next, previous, speed, optionsRef.current.invert, pointerAt2(event));
5865
5917
  };
5866
5918
  const releasePointer = (event) => {
5919
+ pending.delete(event.pointerId);
5867
5920
  if (!pointersRef.current.has(event.pointerId)) {
5868
5921
  return;
5869
5922
  }
@@ -5887,6 +5940,7 @@ function TouchControls({
5887
5940
  element.addEventListener("pointercancel", releasePointer);
5888
5941
  element.addEventListener("lostpointercapture", releasePointer);
5889
5942
  return () => {
5943
+ disposed = true;
5890
5944
  validateDragRef.current = void 0;
5891
5945
  element.removeEventListener("pointerdown", onPointerDown);
5892
5946
  element.removeEventListener("pointermove", onPointerMove);
@@ -11436,6 +11490,93 @@ var import_fiber21 = require("@react-three/fiber");
11436
11490
  var import_react42 = require("react");
11437
11491
  var import_three24 = require("three");
11438
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
+
11439
11580
  // src/hotspot/hotspot-tooltip.tsx
11440
11581
  var import_react40 = require("react");
11441
11582
 
@@ -11695,6 +11836,21 @@ function fixedScaleFactor(cameraFov, referenceFov) {
11695
11836
  const safeReferenceFov = import_three22.MathUtils.clamp(referenceFov, 1, 179);
11696
11837
  return Math.tan(import_three22.MathUtils.degToRad(cameraFov) / 2) / Math.tan(import_three22.MathUtils.degToRad(safeReferenceFov) / 2);
11697
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
+ }
11698
11854
 
11699
11855
  // src/hotspot/hotspot-stroke-line.tsx
11700
11856
  var import_drei = require("@react-three/drei");
@@ -11777,6 +11933,11 @@ var DEFAULT_REFERENCE_FOV = 75;
11777
11933
  var MIN_ANGULAR_SIZE = 0.01;
11778
11934
  var MAX_ANGULAR_SIZE = 179;
11779
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();
11780
11941
  var SELECTION_BORDER_POINTS = [
11781
11942
  [-0.5, -0.5, 1e-3],
11782
11943
  [0.5, -0.5, 1e-3],
@@ -11864,6 +12025,34 @@ function angularDistance(a, b) {
11864
12025
  const bVector = panoPositionToVector3(b);
11865
12026
  return import_three24.MathUtils.radToDeg(aVector.angleTo(bVector));
11866
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
+ }
11867
12056
  function isTooltipOpen(trigger, hovered, focused, pinned) {
11868
12057
  switch (trigger) {
11869
12058
  case "always":
@@ -12059,6 +12248,24 @@ function updateTooltipAnchor(anchor, group, placement, useAngularScale, camera,
12059
12248
  }
12060
12249
  setTooltipAnchorOnLocalBox(anchor, placement, -0.5, 0.5, -0.5, 0.5);
12061
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
+ }
12062
12269
  function HotspotAnchor({
12063
12270
  id,
12064
12271
  position,
@@ -12098,10 +12305,13 @@ function HotspotAnchor({
12098
12305
  const hotspotTargetRegistry = (0, import_react42.useContext)(HotspotTargetRegistryContext);
12099
12306
  const eventBus = (0, import_react42.useContext)(PanoEventBusContext);
12100
12307
  const cursorApi = usePanoCursor();
12101
- const { camera, size } = (0, import_fiber21.useThree)();
12308
+ const { camera, gl, size } = (0, import_fiber21.useThree)();
12102
12309
  const groupRef = (0, import_react42.useRef)(null);
12310
+ const selectionFrameRef = (0, import_react42.useRef)(null);
12103
12311
  const tooltipAnchorRef = (0, import_react42.useRef)(null);
12104
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);
12105
12315
  const suppressNextClickRef = (0, import_react42.useRef)(false);
12106
12316
  const [hovered, setHovered] = (0, import_react42.useState)(false);
12107
12317
  const [dragging, setDragging] = (0, import_react42.useState)(false);
@@ -12171,7 +12381,9 @@ function HotspotAnchor({
12171
12381
  const cursorClaimId = hotspotCursorClaimId(id);
12172
12382
  (0, import_react42.useEffect)(
12173
12383
  () => () => {
12174
- dragStateRef.current?.releaseInteractionLock();
12384
+ const dragState = dragStateRef.current;
12385
+ dragState?.stopTracking();
12386
+ dragState?.releaseInteractionLock();
12175
12387
  dragStateRef.current = null;
12176
12388
  },
12177
12389
  []
@@ -12228,6 +12440,7 @@ function HotspotAnchor({
12228
12440
  if (!dragState) {
12229
12441
  return;
12230
12442
  }
12443
+ dragState.stopTracking();
12231
12444
  dragState.releaseInteractionLock();
12232
12445
  dragStateRef.current = null;
12233
12446
  setDragging(false);
@@ -12250,6 +12463,7 @@ function HotspotAnchor({
12250
12463
  worldHeight,
12251
12464
  resolvedDistance
12252
12465
  );
12466
+ placeSelectionFrame(selectionFrameRef.current, group, camera, size.height);
12253
12467
  if (tooltipOpen && tooltipAnchorRef.current) {
12254
12468
  updateTooltipAnchor(
12255
12469
  tooltipAnchorRef.current,
@@ -12268,6 +12482,7 @@ function HotspotAnchor({
12268
12482
  rotation,
12269
12483
  scaleMode,
12270
12484
  size,
12485
+ selected,
12271
12486
  surfaceQuaternion3,
12272
12487
  tooltipOpen,
12273
12488
  tooltipPlacement,
@@ -12293,6 +12508,12 @@ function HotspotAnchor({
12293
12508
  worldHeight,
12294
12509
  resolvedDistance
12295
12510
  );
12511
+ placeSelectionFrame(
12512
+ selectionFrameRef.current,
12513
+ group,
12514
+ state.camera,
12515
+ state.size.height
12516
+ );
12296
12517
  if (tooltipOpen && tooltipAnchorRef.current) {
12297
12518
  updateTooltipAnchor(
12298
12519
  tooltipAnchorRef.current,
@@ -12308,31 +12529,78 @@ function HotspotAnchor({
12308
12529
  event.stopPropagation();
12309
12530
  event.nativeEvent.stopImmediatePropagation?.();
12310
12531
  };
12311
- 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) => {
12312
12544
  const dragState = dragStateRef.current;
12313
- if (!dragState || dragState.pointerId !== event.pointerId) {
12545
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12314
12546
  return;
12315
12547
  }
12316
- stopPointerEvent(event);
12317
- const target = event.target;
12318
- if (target.hasPointerCapture?.(event.pointerId)) {
12319
- target.releasePointerCapture?.(event.pointerId);
12320
- }
12321
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12322
- if (emitDragEnd) {
12323
- onDragEnd?.({
12324
- ...makeInteractionEvent(id, nextPosition, event),
12325
- source: "pointer",
12326
- startPosition: dragState.startPosition
12327
- });
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;
12328
12563
  }
12329
- 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();
12330
12570
  dragState.releaseInteractionLock();
12331
12571
  dragStateRef.current = null;
12572
+ suppressNextClickRef.current = moved;
12332
12573
  setDragging(false);
12333
- if (emitDragEnd) {
12334
- 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);
12587
+ }
12588
+ if (moved) {
12589
+ onPositionChange?.({
12590
+ id,
12591
+ position: nextPosition,
12592
+ source: "pointer",
12593
+ nativeEvent,
12594
+ startPosition
12595
+ });
12335
12596
  }
12597
+ onDragEnd?.({
12598
+ id,
12599
+ position: nextPosition,
12600
+ source: "pointer",
12601
+ nativeEvent,
12602
+ startPosition
12603
+ });
12336
12604
  };
12337
12605
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
12338
12606
  "group",
@@ -12352,23 +12620,42 @@ function HotspotAnchor({
12352
12620
  }
12353
12621
  suppressNextClickRef.current = false;
12354
12622
  } : void 0,
12355
- onLostPointerCapture: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12356
- 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,
12357
12629
  onPointerDown: acceptsPointer ? (event) => {
12358
- stopPointerEvent(event);
12630
+ const native = event.nativeEvent;
12359
12631
  if (!draggable || dragStateRef.current) {
12632
+ stopPointerEvent(event);
12360
12633
  return;
12361
12634
  }
12635
+ const pointerId = native.pointerId;
12636
+ stopPointerEvent(event);
12362
12637
  const releaseInteractionLock = controlsRef?.current?.acquireInteractionLock() ?? (() => void 0);
12363
- const target = event.target;
12364
- target.setPointerCapture?.(event.pointerId);
12638
+ const pointerOrigin = pointerToPanoPosition(native, gl.domElement, camera) ?? vector3ToPanoPosition(event.ray.direction);
12365
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;
12366
12648
  dragStateRef.current = {
12367
- pointerId: event.pointerId,
12649
+ pointerId,
12368
12650
  startPosition,
12651
+ pointerOrigin,
12652
+ lastPosition: startPosition,
12369
12653
  releaseInteractionLock,
12370
- moved: false
12654
+ moved: false,
12655
+ tracksOutsideMesh,
12656
+ stopTracking
12371
12657
  };
12658
+ eventHovered.current = false;
12372
12659
  setHovered(false);
12373
12660
  setDragging(true);
12374
12661
  onDragStart?.({
@@ -12379,17 +12666,13 @@ function HotspotAnchor({
12379
12666
  } : void 0,
12380
12667
  onPointerMove: acceptsPointer ? (event) => {
12381
12668
  const dragState = dragStateRef.current;
12382
- if (!dragState || dragState.pointerId !== event.pointerId) {
12669
+ if (!dragState || dragState.pointerId !== event.nativeEvent.pointerId) {
12383
12670
  return;
12384
12671
  }
12385
12672
  stopPointerEvent(event);
12386
- const nextPosition = vector3ToPanoPosition(event.ray.direction);
12387
- dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12388
- onPositionChange?.({
12389
- ...makeInteractionEvent(id, nextPosition, event),
12390
- source: "pointer",
12391
- startPosition: dragState.startPosition
12392
- });
12673
+ if (!dragState.tracksOutsideMesh) {
12674
+ updateDragRef.current(event.nativeEvent);
12675
+ }
12393
12676
  } : void 0,
12394
12677
  onPointerOut: acceptsPointer ? (event) => {
12395
12678
  if (!dragStateRef.current) {
@@ -12418,7 +12701,10 @@ function HotspotAnchor({
12418
12701
  );
12419
12702
  }
12420
12703
  } : void 0,
12421
- onPointerUp: acceptsPointer ? (event) => releasePointer(event, true) : void 0,
12704
+ onPointerUp: acceptsPointer ? (event) => {
12705
+ stopPointerEvent(event);
12706
+ endDragRef.current(event.nativeEvent, true);
12707
+ } : void 0,
12422
12708
  children: [
12423
12709
  focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12424
12710
  "lineSegments",
@@ -12431,16 +12717,16 @@ function HotspotAnchor({
12431
12717
  }
12432
12718
  ) : null,
12433
12719
  children,
12434
- 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)(
12435
12721
  HotspotStrokeLine,
12436
12722
  {
12437
12723
  points: SELECTION_BORDER_POINTS,
12438
12724
  color: "#ffb300",
12439
- lineWidth: 3,
12725
+ lineWidth: SELECTION_BORDER_WIDTH,
12440
12726
  opacity: 1,
12441
12727
  renderOrder: renderOrder + 1
12442
12728
  }
12443
- ) : null,
12729
+ ) }) : null,
12444
12730
  tooltipOpen && tooltipContent ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("group", { ref: tooltipAnchorRef, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12445
12731
  HotspotTooltip,
12446
12732
  {
@@ -13943,11 +14229,15 @@ function createTextTexture(text, width, height, style, rasterLong) {
13943
14229
  if (!context) {
13944
14230
  throw new Error("Canvas 2D is unavailable for this text hotspot.");
13945
14231
  }
13946
- drawTextHotspot(context, canvasWidth, canvasHeight, text, {
13947
- ...style,
13948
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * scale,
13949
- strokeWidth: Number.isFinite(style.strokeWidth) ? style.strokeWidth * scale : style.strokeWidth
13950
- });
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);
13951
14241
  const texture = new import_three32.CanvasTexture(canvas);
13952
14242
  texture.colorSpace = import_three32.SRGBColorSpace;
13953
14243
  texture.wrapS = import_three32.ClampToEdgeWrapping;
@@ -14007,13 +14297,7 @@ function useTextTexture(text, width, height, style, scale, scaleMode, referenceF
14007
14297
  let nextTexture = null;
14008
14298
  const run = async () => {
14009
14299
  try {
14010
- const layout = textureSize(width, height);
14011
- const layoutLong = Math.max(layout.canvasWidth, layout.canvasHeight);
14012
- const drawScale = rasterLong / layoutLong;
14013
- const { font } = resolveTextFont({
14014
- ...style,
14015
- fontSize: resolvePositive(style.fontSize, DEFAULT_FONT_SIZE) * drawScale
14016
- });
14300
+ const { font } = resolveTextFont(style);
14017
14301
  try {
14018
14302
  await document.fonts?.load(font);
14019
14303
  } catch {