@ericchen1990/pano-view 3.6.3 → 3.6.5

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,94 @@ 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
+ var CAPTURE = { capture: true };
11557
+ function suppressFollowingClick(target) {
11558
+ let cleaned = false;
11559
+ const cleanup = () => {
11560
+ if (cleaned) {
11561
+ return;
11562
+ }
11563
+ cleaned = true;
11564
+ target.removeEventListener("click", swallowClick, CAPTURE);
11565
+ target.removeEventListener("pointerdown", cancel, CAPTURE);
11566
+ };
11567
+ const swallowClick = (event) => {
11568
+ event.preventDefault();
11569
+ event.stopPropagation();
11570
+ event.stopImmediatePropagation();
11571
+ cleanup();
11572
+ };
11573
+ const cancel = () => {
11574
+ cleanup();
11575
+ };
11576
+ target.addEventListener("click", swallowClick, CAPTURE);
11577
+ target.addEventListener("pointerdown", cancel, CAPTURE);
11578
+ setTimeout(cleanup, CLICK_SUPPRESS_MS);
11579
+ }
11580
+
11439
11581
  // src/hotspot/hotspot-tooltip.tsx
11440
11582
  var import_react40 = require("react");
11441
11583
 
@@ -11695,6 +11837,21 @@ function fixedScaleFactor(cameraFov, referenceFov) {
11695
11837
  const safeReferenceFov = import_three22.MathUtils.clamp(referenceFov, 1, 179);
11696
11838
  return Math.tan(import_three22.MathUtils.degToRad(cameraFov) / 2) / Math.tan(import_three22.MathUtils.degToRad(safeReferenceFov) / 2);
11697
11839
  }
11840
+ function selectionFrameScale(worldScaleX, worldScaleY, cameraSpaceDepth, fovDegrees, viewportHeight, outsetPixels) {
11841
+ const pixels = Number.isFinite(outsetPixels) && outsetPixels > 0 ? outsetPixels : 0;
11842
+ const depth = Number.isFinite(cameraSpaceDepth) ? Math.max(cameraSpaceDepth, 1e-4) : 1e-4;
11843
+ const height = Number.isFinite(viewportHeight) ? Math.max(viewportHeight, 1) : 1;
11844
+ const fov = Number.isFinite(fovDegrees) ? import_three22.MathUtils.clamp(fovDegrees, 1, 179) : 75;
11845
+ const pixelsPerWorldUnit = height / (2 * Math.tan(import_three22.MathUtils.degToRad(fov) * 0.5) * depth);
11846
+ const axis = (worldScale) => {
11847
+ const pixelsPerLocalUnit = pixelsPerWorldUnit * Math.abs(worldScale);
11848
+ if (pixelsPerLocalUnit < 1e-4) {
11849
+ return 1;
11850
+ }
11851
+ return 1 + 2 * pixels / pixelsPerLocalUnit;
11852
+ };
11853
+ return [axis(worldScaleX), axis(worldScaleY)];
11854
+ }
11698
11855
 
11699
11856
  // src/hotspot/hotspot-stroke-line.tsx
11700
11857
  var import_drei = require("@react-three/drei");
@@ -11777,6 +11934,11 @@ var DEFAULT_REFERENCE_FOV = 75;
11777
11934
  var MIN_ANGULAR_SIZE = 0.01;
11778
11935
  var MAX_ANGULAR_SIZE = 179;
11779
11936
  var DRAG_EPSILON_DEGREES = 1e-3;
11937
+ var SELECTION_BORDER_WIDTH = 3;
11938
+ var SELECTION_BORDER_GAP = 3;
11939
+ var SELECTION_BORDER_OUTSET = SELECTION_BORDER_GAP + SELECTION_BORDER_WIDTH / 2;
11940
+ var SELECTION_CAMERA_POSITION = new import_three24.Vector3();
11941
+ var SELECTION_WORLD_SCALE = new import_three24.Vector3();
11780
11942
  var SELECTION_BORDER_POINTS = [
11781
11943
  [-0.5, -0.5, 1e-3],
11782
11944
  [0.5, -0.5, 1e-3],
@@ -11864,6 +12026,34 @@ function angularDistance(a, b) {
11864
12026
  const bVector = panoPositionToVector3(b);
11865
12027
  return import_three24.MathUtils.radToDeg(aVector.angleTo(bVector));
11866
12028
  }
12029
+ var DRAG_POINTER = new import_three24.Vector2();
12030
+ var DRAG_RAYCASTER = new import_three24.Raycaster();
12031
+ function aimDragRaycaster(event, element, camera) {
12032
+ const rect = element.getBoundingClientRect();
12033
+ if (!(rect.width > 0) || !(rect.height > 0)) {
12034
+ return false;
12035
+ }
12036
+ DRAG_POINTER.set(
12037
+ (event.clientX - rect.left) / rect.width * 2 - 1,
12038
+ -((event.clientY - rect.top) / rect.height) * 2 + 1
12039
+ );
12040
+ camera.updateMatrixWorld();
12041
+ DRAG_RAYCASTER.setFromCamera(DRAG_POINTER, camera);
12042
+ return true;
12043
+ }
12044
+ function pointerToPanoPosition(event, element, camera) {
12045
+ if (!aimDragRaycaster(event, element, camera)) {
12046
+ return null;
12047
+ }
12048
+ return vector3ToPanoPosition(DRAG_RAYCASTER.ray.direction);
12049
+ }
12050
+ function pointerHitsHotspot(event, element, camera, group) {
12051
+ if (!group || !aimDragRaycaster(event, element, camera)) {
12052
+ return false;
12053
+ }
12054
+ group.updateMatrixWorld(true);
12055
+ return DRAG_RAYCASTER.intersectObject(group, true).length > 0;
12056
+ }
11867
12057
  function isTooltipOpen(trigger, hovered, focused, pinned) {
11868
12058
  switch (trigger) {
11869
12059
  case "always":
@@ -12059,6 +12249,24 @@ function updateTooltipAnchor(anchor, group, placement, useAngularScale, camera,
12059
12249
  }
12060
12250
  setTooltipAnchorOnLocalBox(anchor, placement, -0.5, 0.5, -0.5, 0.5);
12061
12251
  }
12252
+ function placeSelectionFrame(frame, group, camera, viewportHeight) {
12253
+ if (!frame) {
12254
+ return;
12255
+ }
12256
+ group.getWorldScale(SELECTION_WORLD_SCALE);
12257
+ SELECTION_CAMERA_POSITION.setFromMatrixPosition(group.matrixWorld).applyMatrix4(
12258
+ camera.matrixWorldInverse
12259
+ );
12260
+ const [scaleX, scaleY] = selectionFrameScale(
12261
+ SELECTION_WORLD_SCALE.x,
12262
+ SELECTION_WORLD_SCALE.y,
12263
+ -SELECTION_CAMERA_POSITION.z,
12264
+ camera instanceof import_three24.PerspectiveCamera ? camera.fov : DEFAULT_REFERENCE_FOV,
12265
+ viewportHeight,
12266
+ SELECTION_BORDER_OUTSET
12267
+ );
12268
+ frame.scale.set(scaleX, scaleY, 1);
12269
+ }
12062
12270
  function HotspotAnchor({
12063
12271
  id,
12064
12272
  position,
@@ -12098,10 +12306,13 @@ function HotspotAnchor({
12098
12306
  const hotspotTargetRegistry = (0, import_react42.useContext)(HotspotTargetRegistryContext);
12099
12307
  const eventBus = (0, import_react42.useContext)(PanoEventBusContext);
12100
12308
  const cursorApi = usePanoCursor();
12101
- const { camera, size } = (0, import_fiber21.useThree)();
12309
+ const { camera, gl, size } = (0, import_fiber21.useThree)();
12102
12310
  const groupRef = (0, import_react42.useRef)(null);
12311
+ const selectionFrameRef = (0, import_react42.useRef)(null);
12103
12312
  const tooltipAnchorRef = (0, import_react42.useRef)(null);
12104
12313
  const dragStateRef = (0, import_react42.useRef)(null);
12314
+ const updateDragRef = (0, import_react42.useRef)(() => void 0);
12315
+ const endDragRef = (0, import_react42.useRef)(() => void 0);
12105
12316
  const suppressNextClickRef = (0, import_react42.useRef)(false);
12106
12317
  const [hovered, setHovered] = (0, import_react42.useState)(false);
12107
12318
  const [dragging, setDragging] = (0, import_react42.useState)(false);
@@ -12171,7 +12382,9 @@ function HotspotAnchor({
12171
12382
  const cursorClaimId = hotspotCursorClaimId(id);
12172
12383
  (0, import_react42.useEffect)(
12173
12384
  () => () => {
12174
- dragStateRef.current?.releaseInteractionLock();
12385
+ const dragState = dragStateRef.current;
12386
+ dragState?.stopTracking();
12387
+ dragState?.releaseInteractionLock();
12175
12388
  dragStateRef.current = null;
12176
12389
  },
12177
12390
  []
@@ -12228,6 +12441,7 @@ function HotspotAnchor({
12228
12441
  if (!dragState) {
12229
12442
  return;
12230
12443
  }
12444
+ dragState.stopTracking();
12231
12445
  dragState.releaseInteractionLock();
12232
12446
  dragStateRef.current = null;
12233
12447
  setDragging(false);
@@ -12250,6 +12464,7 @@ function HotspotAnchor({
12250
12464
  worldHeight,
12251
12465
  resolvedDistance
12252
12466
  );
12467
+ placeSelectionFrame(selectionFrameRef.current, group, camera, size.height);
12253
12468
  if (tooltipOpen && tooltipAnchorRef.current) {
12254
12469
  updateTooltipAnchor(
12255
12470
  tooltipAnchorRef.current,
@@ -12268,6 +12483,7 @@ function HotspotAnchor({
12268
12483
  rotation,
12269
12484
  scaleMode,
12270
12485
  size,
12486
+ selected,
12271
12487
  surfaceQuaternion3,
12272
12488
  tooltipOpen,
12273
12489
  tooltipPlacement,
@@ -12293,6 +12509,12 @@ function HotspotAnchor({
12293
12509
  worldHeight,
12294
12510
  resolvedDistance
12295
12511
  );
12512
+ placeSelectionFrame(
12513
+ selectionFrameRef.current,
12514
+ group,
12515
+ state.camera,
12516
+ state.size.height
12517
+ );
12296
12518
  if (tooltipOpen && tooltipAnchorRef.current) {
12297
12519
  updateTooltipAnchor(
12298
12520
  tooltipAnchorRef.current,
@@ -12308,31 +12530,78 @@ function HotspotAnchor({
12308
12530
  event.stopPropagation();
12309
12531
  event.nativeEvent.stopImmediatePropagation?.();
12310
12532
  };
12311
- const releasePointer = (event, emitDragEnd) => {
12533
+ const dragPosition = (dragState, event) => {
12534
+ const pointerPosition = pointerToPanoPosition(event, gl.domElement, camera);
12535
+ if (!pointerPosition) {
12536
+ return dragState.lastPosition;
12537
+ }
12538
+ return translateHotspotPosition(
12539
+ dragState.startPosition,
12540
+ dragState.pointerOrigin,
12541
+ pointerPosition
12542
+ );
12543
+ };
12544
+ updateDragRef.current = (nativeEvent) => {
12312
12545
  const dragState = dragStateRef.current;
12313
- if (!dragState || dragState.pointerId !== event.pointerId) {
12546
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12314
12547
  return;
12315
12548
  }
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
- });
12549
+ const nextPosition = dragPosition(dragState, nativeEvent);
12550
+ dragState.lastPosition = nextPosition;
12551
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12552
+ onPositionChange?.({
12553
+ id,
12554
+ position: nextPosition,
12555
+ source: "pointer",
12556
+ nativeEvent,
12557
+ startPosition: dragState.startPosition
12558
+ });
12559
+ };
12560
+ endDragRef.current = (nativeEvent, emitDragEnd) => {
12561
+ const dragState = dragStateRef.current;
12562
+ if (!dragState || dragState.pointerId !== nativeEvent.pointerId) {
12563
+ return;
12328
12564
  }
12329
- suppressNextClickRef.current = dragState.moved;
12565
+ const nextPosition = dragPosition(dragState, nativeEvent);
12566
+ dragState.lastPosition = nextPosition;
12567
+ dragState.moved || (dragState.moved = angularDistance(dragState.startPosition, nextPosition) > DRAG_EPSILON_DEGREES);
12568
+ const moved = dragState.moved;
12569
+ const startPosition = dragState.startPosition;
12570
+ dragState.stopTracking();
12330
12571
  dragState.releaseInteractionLock();
12331
12572
  dragStateRef.current = null;
12573
+ suppressNextClickRef.current = moved;
12332
12574
  setDragging(false);
12333
- if (emitDragEnd) {
12334
- setHovered(true);
12575
+ if (!emitDragEnd) {
12576
+ return;
12577
+ }
12578
+ const overHotspot = pointerHitsHotspot(
12579
+ nativeEvent,
12580
+ gl.domElement,
12581
+ camera,
12582
+ groupRef.current
12583
+ );
12584
+ eventHovered.current = overHotspot;
12585
+ setHovered(overHotspot);
12586
+ if (moved && typeof window !== "undefined") {
12587
+ suppressFollowingClick(window);
12588
+ }
12589
+ if (moved) {
12590
+ onPositionChange?.({
12591
+ id,
12592
+ position: nextPosition,
12593
+ source: "pointer",
12594
+ nativeEvent,
12595
+ startPosition
12596
+ });
12335
12597
  }
12598
+ onDragEnd?.({
12599
+ id,
12600
+ position: nextPosition,
12601
+ source: "pointer",
12602
+ nativeEvent,
12603
+ startPosition
12604
+ });
12336
12605
  };
12337
12606
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
12338
12607
  "group",
@@ -12352,23 +12621,42 @@ function HotspotAnchor({
12352
12621
  }
12353
12622
  suppressNextClickRef.current = false;
12354
12623
  } : void 0,
12355
- onLostPointerCapture: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12356
- onPointerCancel: acceptsPointer ? (event) => releasePointer(event, false) : void 0,
12624
+ onLostPointerCapture: acceptsPointer ? (event) => {
12625
+ endDragRef.current(event.nativeEvent, true);
12626
+ } : void 0,
12627
+ onPointerCancel: acceptsPointer ? (event) => {
12628
+ endDragRef.current(event.nativeEvent, true);
12629
+ } : void 0,
12357
12630
  onPointerDown: acceptsPointer ? (event) => {
12358
- stopPointerEvent(event);
12631
+ const native = event.nativeEvent;
12359
12632
  if (!draggable || dragStateRef.current) {
12633
+ stopPointerEvent(event);
12360
12634
  return;
12361
12635
  }
12636
+ const pointerId = native.pointerId;
12637
+ stopPointerEvent(event);
12362
12638
  const releaseInteractionLock = controlsRef?.current?.acquireInteractionLock() ?? (() => void 0);
12363
- const target = event.target;
12364
- target.setPointerCapture?.(event.pointerId);
12639
+ const pointerOrigin = pointerToPanoPosition(native, gl.domElement, camera) ?? vector3ToPanoPosition(event.ray.direction);
12365
12640
  const startPosition = { ...normalizedPosition };
12641
+ const tracksOutsideMesh = typeof window !== "undefined";
12642
+ const stopTracking = tracksOutsideMesh ? trackPointerDrag(
12643
+ window,
12644
+ gl.domElement,
12645
+ pointerId,
12646
+ (pointerEvent) => updateDragRef.current(pointerEvent),
12647
+ (pointerEvent) => endDragRef.current(pointerEvent, true)
12648
+ ) : () => void 0;
12366
12649
  dragStateRef.current = {
12367
- pointerId: event.pointerId,
12650
+ pointerId,
12368
12651
  startPosition,
12652
+ pointerOrigin,
12653
+ lastPosition: startPosition,
12369
12654
  releaseInteractionLock,
12370
- moved: false
12655
+ moved: false,
12656
+ tracksOutsideMesh,
12657
+ stopTracking
12371
12658
  };
12659
+ eventHovered.current = false;
12372
12660
  setHovered(false);
12373
12661
  setDragging(true);
12374
12662
  onDragStart?.({
@@ -12379,17 +12667,13 @@ function HotspotAnchor({
12379
12667
  } : void 0,
12380
12668
  onPointerMove: acceptsPointer ? (event) => {
12381
12669
  const dragState = dragStateRef.current;
12382
- if (!dragState || dragState.pointerId !== event.pointerId) {
12670
+ if (!dragState || dragState.pointerId !== event.nativeEvent.pointerId) {
12383
12671
  return;
12384
12672
  }
12385
12673
  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
- });
12674
+ if (!dragState.tracksOutsideMesh) {
12675
+ updateDragRef.current(event.nativeEvent);
12676
+ }
12393
12677
  } : void 0,
12394
12678
  onPointerOut: acceptsPointer ? (event) => {
12395
12679
  if (!dragStateRef.current) {
@@ -12418,7 +12702,10 @@ function HotspotAnchor({
12418
12702
  );
12419
12703
  }
12420
12704
  } : void 0,
12421
- onPointerUp: acceptsPointer ? (event) => releasePointer(event, true) : void 0,
12705
+ onPointerUp: acceptsPointer ? (event) => {
12706
+ stopPointerEvent(event);
12707
+ endDragRef.current(event.nativeEvent, true);
12708
+ } : void 0,
12422
12709
  children: [
12423
12710
  focused ? focusContent ?? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12424
12711
  "lineSegments",
@@ -12431,16 +12718,16 @@ function HotspotAnchor({
12431
12718
  }
12432
12719
  ) : null,
12433
12720
  children,
12434
- selected && useAngularScale ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12721
+ selected && useAngularScale ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("group", { ref: selectionFrameRef, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12435
12722
  HotspotStrokeLine,
12436
12723
  {
12437
12724
  points: SELECTION_BORDER_POINTS,
12438
12725
  color: "#ffb300",
12439
- lineWidth: 3,
12726
+ lineWidth: SELECTION_BORDER_WIDTH,
12440
12727
  opacity: 1,
12441
12728
  renderOrder: renderOrder + 1
12442
12729
  }
12443
- ) : null,
12730
+ ) }) : null,
12444
12731
  tooltipOpen && tooltipContent ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("group", { ref: tooltipAnchorRef, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
12445
12732
  HotspotTooltip,
12446
12733
  {
@@ -13943,11 +14230,15 @@ function createTextTexture(text, width, height, style, rasterLong) {
13943
14230
  if (!context) {
13944
14231
  throw new Error("Canvas 2D is unavailable for this text hotspot.");
13945
14232
  }
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
- });
14233
+ context.setTransform(
14234
+ canvasWidth / layout.canvasWidth,
14235
+ 0,
14236
+ 0,
14237
+ canvasHeight / layout.canvasHeight,
14238
+ 0,
14239
+ 0
14240
+ );
14241
+ drawTextHotspot(context, layout.canvasWidth, layout.canvasHeight, text, style);
13951
14242
  const texture = new import_three32.CanvasTexture(canvas);
13952
14243
  texture.colorSpace = import_three32.SRGBColorSpace;
13953
14244
  texture.wrapS = import_three32.ClampToEdgeWrapping;
@@ -14007,13 +14298,7 @@ function useTextTexture(text, width, height, style, scale, scaleMode, referenceF
14007
14298
  let nextTexture = null;
14008
14299
  const run = async () => {
14009
14300
  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
- });
14301
+ const { font } = resolveTextFont(style);
14017
14302
  try {
14018
14303
  await document.fonts?.load(font);
14019
14304
  } catch {