@project-skymap/library 0.7.0 → 0.7.2

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
@@ -844,12 +844,19 @@ var createEngine_exports = {};
844
844
  __export(createEngine_exports, {
845
845
  createEngine: () => createEngine
846
846
  });
847
+ function triggerHaptic(style = "light") {
848
+ if (typeof navigator !== "undefined" && "vibrate" in navigator) {
849
+ const durations = { light: 10, medium: 25, heavy: 50 };
850
+ navigator.vibrate(durations[style]);
851
+ }
852
+ }
847
853
  function createEngine({
848
854
  container,
849
855
  onSelect,
850
856
  onHover,
851
857
  onArrangementChange,
852
- onFovChange
858
+ onFovChange,
859
+ onLongPress
853
860
  }) {
854
861
  let hoveredBookId = null;
855
862
  let focusedBookId = null;
@@ -919,13 +926,20 @@ function createEngine({
919
926
  pinchStartDistance: 0,
920
927
  pinchStartFov: ENGINE_CONFIG.defaultFov,
921
928
  pinchCenterX: 0,
922
- pinchCenterY: 0
929
+ pinchCenterY: 0,
930
+ // Double-tap detection
931
+ lastTapTime: 0,
932
+ lastTapX: 0,
933
+ lastTapY: 0,
934
+ // Long-press detection
935
+ longPressTimer: null,
936
+ longPressTriggered: false
923
937
  };
924
938
  const mouseNDC = new THREE5__namespace.Vector2();
925
939
  let isMouseInWindow = false;
926
940
  let isTouchDevice = false;
927
941
  let edgeHoverStart = 0;
928
- let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
942
+ let handlers = { onSelect, onHover, onArrangementChange, onFovChange, onLongPress };
929
943
  let currentConfig;
930
944
  const constellationLayer = new ConstellationArtworkLayer(scene);
931
945
  function mix(a, b, t) {
@@ -2418,6 +2432,11 @@ function createEngine({
2418
2432
  function onTouchStart(e) {
2419
2433
  e.preventDefault();
2420
2434
  isTouchDevice = true;
2435
+ if (state.longPressTimer) {
2436
+ clearTimeout(state.longPressTimer);
2437
+ state.longPressTimer = null;
2438
+ }
2439
+ state.longPressTriggered = false;
2421
2440
  const touches = e.touches;
2422
2441
  state.touchCount = touches.length;
2423
2442
  if (touches.length === 1) {
@@ -2433,6 +2452,23 @@ function createEngine({
2433
2452
  state.isDragging = true;
2434
2453
  state.velocityX = 0;
2435
2454
  state.velocityY = 0;
2455
+ state.longPressTimer = setTimeout(() => {
2456
+ if (!state.touchMoved && state.touchCount === 1) {
2457
+ state.longPressTriggered = true;
2458
+ const rect = renderer.domElement.getBoundingClientRect();
2459
+ const mX = state.touchStartX - rect.left;
2460
+ const mY = state.touchStartY - rect.top;
2461
+ mouseNDC.x = mX / rect.width * 2 - 1;
2462
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2463
+ const syntheticEvent = {
2464
+ clientX: state.touchStartX,
2465
+ clientY: state.touchStartY
2466
+ };
2467
+ const hit = pick(syntheticEvent);
2468
+ triggerHaptic("heavy");
2469
+ handlers.onLongPress?.(hit?.node ?? null, state.touchStartX, state.touchStartY);
2470
+ }
2471
+ }, ENGINE_CONFIG.longPressDelay);
2436
2472
  } else if (touches.length === 2) {
2437
2473
  const t0 = touches[0];
2438
2474
  const t1 = touches[1];
@@ -2459,6 +2495,10 @@ function createEngine({
2459
2495
  const totalDy = touch.clientY - state.touchStartY;
2460
2496
  if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
2461
2497
  state.touchMoved = true;
2498
+ if (state.longPressTimer) {
2499
+ clearTimeout(state.longPressTimer);
2500
+ state.longPressTimer = null;
2501
+ }
2462
2502
  }
2463
2503
  const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2464
2504
  const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
@@ -2493,11 +2533,21 @@ function createEngine({
2493
2533
  }
2494
2534
  function onTouchEnd(e) {
2495
2535
  e.preventDefault();
2536
+ if (state.longPressTimer) {
2537
+ clearTimeout(state.longPressTimer);
2538
+ state.longPressTimer = null;
2539
+ }
2496
2540
  const remainingTouches = e.touches.length;
2497
2541
  if (remainingTouches === 0) {
2498
- const duration = performance.now() - state.touchStartTime;
2499
- const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
2542
+ const now = performance.now();
2543
+ const duration = now - state.touchStartTime;
2544
+ const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration && !state.longPressTriggered;
2500
2545
  if (wasTap) {
2546
+ const timeSinceLastTap = now - state.lastTapTime;
2547
+ const distFromLastTap = Math.sqrt(
2548
+ Math.pow(state.touchStartX - state.lastTapX, 2) + Math.pow(state.touchStartY - state.lastTapY, 2)
2549
+ );
2550
+ const isDoubleTap = timeSinceLastTap < ENGINE_CONFIG.doubleTapMaxDelay && distFromLastTap < ENGINE_CONFIG.doubleTapMaxDistance;
2501
2551
  const rect = renderer.domElement.getBoundingClientRect();
2502
2552
  const mX = state.touchStartX - rect.left;
2503
2553
  const mY = state.touchStartY - rect.top;
@@ -2508,13 +2558,28 @@ function createEngine({
2508
2558
  clientY: state.touchStartY
2509
2559
  };
2510
2560
  const hit = pick(syntheticEvent);
2511
- if (hit) {
2512
- handlers.onSelect?.(hit.node);
2513
- constellationLayer.setFocused(hit.node.id);
2514
- if (hit.node.level === 2) setFocusedBook(hit.node.id);
2515
- else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
2561
+ if (isDoubleTap) {
2562
+ if (hit) {
2563
+ triggerHaptic("medium");
2564
+ flyTo(hit.node.id, ENGINE_CONFIG.minFov);
2565
+ handlers.onSelect?.(hit.node);
2566
+ }
2567
+ state.lastTapTime = 0;
2568
+ state.lastTapX = 0;
2569
+ state.lastTapY = 0;
2516
2570
  } else {
2517
- setFocusedBook(null);
2571
+ if (hit) {
2572
+ triggerHaptic("light");
2573
+ handlers.onSelect?.(hit.node);
2574
+ constellationLayer.setFocused(hit.node.id);
2575
+ if (hit.node.level === 2) setFocusedBook(hit.node.id);
2576
+ else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
2577
+ } else {
2578
+ setFocusedBook(null);
2579
+ }
2580
+ state.lastTapTime = now;
2581
+ state.lastTapX = state.touchStartX;
2582
+ state.lastTapY = state.touchStartY;
2518
2583
  }
2519
2584
  }
2520
2585
  state.isDragging = false;
@@ -2533,6 +2598,10 @@ function createEngine({
2533
2598
  }
2534
2599
  function onTouchCancel(e) {
2535
2600
  e.preventDefault();
2601
+ if (state.longPressTimer) {
2602
+ clearTimeout(state.longPressTimer);
2603
+ state.longPressTimer = null;
2604
+ }
2536
2605
  state.isDragging = false;
2537
2606
  state.dragMode = "none";
2538
2607
  state.touchCount = 0;
@@ -2918,8 +2987,14 @@ var init_createEngine = __esm({
2918
2987
  // Snappier than mouse (0.92)
2919
2988
  tapMaxDuration: 300,
2920
2989
  // ms
2921
- tapMaxDistance: 10
2990
+ tapMaxDistance: 10,
2922
2991
  // px
2992
+ doubleTapMaxDelay: 300,
2993
+ // ms between taps
2994
+ doubleTapMaxDistance: 30,
2995
+ // px between tap locations
2996
+ longPressDelay: 500
2997
+ // ms to trigger long-press
2923
2998
  };
2924
2999
  ORDER_REVEAL_CONFIG = {
2925
3000
  globalDim: 0.85,
@@ -2930,7 +3005,7 @@ var init_createEngine = __esm({
2930
3005
  }
2931
3006
  });
2932
3007
  var StarMap = react.forwardRef(
2933
- ({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
3008
+ ({ config, className, onSelect, onHover, onArrangementChange, onFovChange, onLongPress }, ref) => {
2934
3009
  const containerRef = react.useRef(null);
2935
3010
  const engineRef = react.useRef(null);
2936
3011
  react.useImperativeHandle(ref, () => ({
@@ -2953,7 +3028,8 @@ var StarMap = react.forwardRef(
2953
3028
  onSelect,
2954
3029
  onHover,
2955
3030
  onArrangementChange,
2956
- onFovChange
3031
+ onFovChange,
3032
+ onLongPress
2957
3033
  });
2958
3034
  engineRef.current.setConfig(config);
2959
3035
  engineRef.current.start();
@@ -2969,8 +3045,8 @@ var StarMap = react.forwardRef(
2969
3045
  engineRef.current?.setConfig?.(config);
2970
3046
  }, [config]);
2971
3047
  react.useEffect(() => {
2972
- engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
2973
- }, [onSelect, onHover, onArrangementChange, onFovChange]);
3048
+ engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange, onLongPress });
3049
+ }, [onSelect, onHover, onArrangementChange, onFovChange, onLongPress]);
2974
3050
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
2975
3051
  }
2976
3052
  );