@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.d.cts CHANGED
@@ -103,6 +103,7 @@ type StarMapProps = {
103
103
  onHover?: (node?: SceneNode) => void;
104
104
  onArrangementChange?: (arrangement: StarArrangement) => void;
105
105
  onFovChange?: (fov: number) => void;
106
+ onLongPress?: (node: SceneNode | null, x: number, y: number) => void;
106
107
  };
107
108
  type StarMapHandle = {
108
109
  getFullArrangement: () => StarArrangement | undefined;
package/dist/index.d.ts CHANGED
@@ -103,6 +103,7 @@ type StarMapProps = {
103
103
  onHover?: (node?: SceneNode) => void;
104
104
  onArrangementChange?: (arrangement: StarArrangement) => void;
105
105
  onFovChange?: (fov: number) => void;
106
+ onLongPress?: (node: SceneNode | null, x: number, y: number) => void;
106
107
  };
107
108
  type StarMapHandle = {
108
109
  getFullArrangement: () => StarArrangement | undefined;
package/dist/index.js CHANGED
@@ -822,12 +822,19 @@ var createEngine_exports = {};
822
822
  __export(createEngine_exports, {
823
823
  createEngine: () => createEngine
824
824
  });
825
+ function triggerHaptic(style = "light") {
826
+ if (typeof navigator !== "undefined" && "vibrate" in navigator) {
827
+ const durations = { light: 10, medium: 25, heavy: 50 };
828
+ navigator.vibrate(durations[style]);
829
+ }
830
+ }
825
831
  function createEngine({
826
832
  container,
827
833
  onSelect,
828
834
  onHover,
829
835
  onArrangementChange,
830
- onFovChange
836
+ onFovChange,
837
+ onLongPress
831
838
  }) {
832
839
  let hoveredBookId = null;
833
840
  let focusedBookId = null;
@@ -897,13 +904,20 @@ function createEngine({
897
904
  pinchStartDistance: 0,
898
905
  pinchStartFov: ENGINE_CONFIG.defaultFov,
899
906
  pinchCenterX: 0,
900
- pinchCenterY: 0
907
+ pinchCenterY: 0,
908
+ // Double-tap detection
909
+ lastTapTime: 0,
910
+ lastTapX: 0,
911
+ lastTapY: 0,
912
+ // Long-press detection
913
+ longPressTimer: null,
914
+ longPressTriggered: false
901
915
  };
902
916
  const mouseNDC = new THREE5.Vector2();
903
917
  let isMouseInWindow = false;
904
918
  let isTouchDevice = false;
905
919
  let edgeHoverStart = 0;
906
- let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
920
+ let handlers = { onSelect, onHover, onArrangementChange, onFovChange, onLongPress };
907
921
  let currentConfig;
908
922
  const constellationLayer = new ConstellationArtworkLayer(scene);
909
923
  function mix(a, b, t) {
@@ -2396,6 +2410,11 @@ function createEngine({
2396
2410
  function onTouchStart(e) {
2397
2411
  e.preventDefault();
2398
2412
  isTouchDevice = true;
2413
+ if (state.longPressTimer) {
2414
+ clearTimeout(state.longPressTimer);
2415
+ state.longPressTimer = null;
2416
+ }
2417
+ state.longPressTriggered = false;
2399
2418
  const touches = e.touches;
2400
2419
  state.touchCount = touches.length;
2401
2420
  if (touches.length === 1) {
@@ -2411,6 +2430,23 @@ function createEngine({
2411
2430
  state.isDragging = true;
2412
2431
  state.velocityX = 0;
2413
2432
  state.velocityY = 0;
2433
+ state.longPressTimer = setTimeout(() => {
2434
+ if (!state.touchMoved && state.touchCount === 1) {
2435
+ state.longPressTriggered = true;
2436
+ const rect = renderer.domElement.getBoundingClientRect();
2437
+ const mX = state.touchStartX - rect.left;
2438
+ const mY = state.touchStartY - rect.top;
2439
+ mouseNDC.x = mX / rect.width * 2 - 1;
2440
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2441
+ const syntheticEvent = {
2442
+ clientX: state.touchStartX,
2443
+ clientY: state.touchStartY
2444
+ };
2445
+ const hit = pick(syntheticEvent);
2446
+ triggerHaptic("heavy");
2447
+ handlers.onLongPress?.(hit?.node ?? null, state.touchStartX, state.touchStartY);
2448
+ }
2449
+ }, ENGINE_CONFIG.longPressDelay);
2414
2450
  } else if (touches.length === 2) {
2415
2451
  const t0 = touches[0];
2416
2452
  const t1 = touches[1];
@@ -2437,6 +2473,10 @@ function createEngine({
2437
2473
  const totalDy = touch.clientY - state.touchStartY;
2438
2474
  if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
2439
2475
  state.touchMoved = true;
2476
+ if (state.longPressTimer) {
2477
+ clearTimeout(state.longPressTimer);
2478
+ state.longPressTimer = null;
2479
+ }
2440
2480
  }
2441
2481
  const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2442
2482
  const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
@@ -2471,11 +2511,21 @@ function createEngine({
2471
2511
  }
2472
2512
  function onTouchEnd(e) {
2473
2513
  e.preventDefault();
2514
+ if (state.longPressTimer) {
2515
+ clearTimeout(state.longPressTimer);
2516
+ state.longPressTimer = null;
2517
+ }
2474
2518
  const remainingTouches = e.touches.length;
2475
2519
  if (remainingTouches === 0) {
2476
- const duration = performance.now() - state.touchStartTime;
2477
- const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
2520
+ const now = performance.now();
2521
+ const duration = now - state.touchStartTime;
2522
+ const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration && !state.longPressTriggered;
2478
2523
  if (wasTap) {
2524
+ const timeSinceLastTap = now - state.lastTapTime;
2525
+ const distFromLastTap = Math.sqrt(
2526
+ Math.pow(state.touchStartX - state.lastTapX, 2) + Math.pow(state.touchStartY - state.lastTapY, 2)
2527
+ );
2528
+ const isDoubleTap = timeSinceLastTap < ENGINE_CONFIG.doubleTapMaxDelay && distFromLastTap < ENGINE_CONFIG.doubleTapMaxDistance;
2479
2529
  const rect = renderer.domElement.getBoundingClientRect();
2480
2530
  const mX = state.touchStartX - rect.left;
2481
2531
  const mY = state.touchStartY - rect.top;
@@ -2486,13 +2536,28 @@ function createEngine({
2486
2536
  clientY: state.touchStartY
2487
2537
  };
2488
2538
  const hit = pick(syntheticEvent);
2489
- if (hit) {
2490
- handlers.onSelect?.(hit.node);
2491
- constellationLayer.setFocused(hit.node.id);
2492
- if (hit.node.level === 2) setFocusedBook(hit.node.id);
2493
- else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
2539
+ if (isDoubleTap) {
2540
+ if (hit) {
2541
+ triggerHaptic("medium");
2542
+ flyTo(hit.node.id, ENGINE_CONFIG.minFov);
2543
+ handlers.onSelect?.(hit.node);
2544
+ }
2545
+ state.lastTapTime = 0;
2546
+ state.lastTapX = 0;
2547
+ state.lastTapY = 0;
2494
2548
  } else {
2495
- setFocusedBook(null);
2549
+ if (hit) {
2550
+ triggerHaptic("light");
2551
+ handlers.onSelect?.(hit.node);
2552
+ constellationLayer.setFocused(hit.node.id);
2553
+ if (hit.node.level === 2) setFocusedBook(hit.node.id);
2554
+ else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
2555
+ } else {
2556
+ setFocusedBook(null);
2557
+ }
2558
+ state.lastTapTime = now;
2559
+ state.lastTapX = state.touchStartX;
2560
+ state.lastTapY = state.touchStartY;
2496
2561
  }
2497
2562
  }
2498
2563
  state.isDragging = false;
@@ -2511,6 +2576,10 @@ function createEngine({
2511
2576
  }
2512
2577
  function onTouchCancel(e) {
2513
2578
  e.preventDefault();
2579
+ if (state.longPressTimer) {
2580
+ clearTimeout(state.longPressTimer);
2581
+ state.longPressTimer = null;
2582
+ }
2514
2583
  state.isDragging = false;
2515
2584
  state.dragMode = "none";
2516
2585
  state.touchCount = 0;
@@ -2896,8 +2965,14 @@ var init_createEngine = __esm({
2896
2965
  // Snappier than mouse (0.92)
2897
2966
  tapMaxDuration: 300,
2898
2967
  // ms
2899
- tapMaxDistance: 10
2968
+ tapMaxDistance: 10,
2900
2969
  // px
2970
+ doubleTapMaxDelay: 300,
2971
+ // ms between taps
2972
+ doubleTapMaxDistance: 30,
2973
+ // px between tap locations
2974
+ longPressDelay: 500
2975
+ // ms to trigger long-press
2901
2976
  };
2902
2977
  ORDER_REVEAL_CONFIG = {
2903
2978
  globalDim: 0.85,
@@ -2908,7 +2983,7 @@ var init_createEngine = __esm({
2908
2983
  }
2909
2984
  });
2910
2985
  var StarMap = forwardRef(
2911
- ({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
2986
+ ({ config, className, onSelect, onHover, onArrangementChange, onFovChange, onLongPress }, ref) => {
2912
2987
  const containerRef = useRef(null);
2913
2988
  const engineRef = useRef(null);
2914
2989
  useImperativeHandle(ref, () => ({
@@ -2931,7 +3006,8 @@ var StarMap = forwardRef(
2931
3006
  onSelect,
2932
3007
  onHover,
2933
3008
  onArrangementChange,
2934
- onFovChange
3009
+ onFovChange,
3010
+ onLongPress
2935
3011
  });
2936
3012
  engineRef.current.setConfig(config);
2937
3013
  engineRef.current.start();
@@ -2947,8 +3023,8 @@ var StarMap = forwardRef(
2947
3023
  engineRef.current?.setConfig?.(config);
2948
3024
  }, [config]);
2949
3025
  useEffect(() => {
2950
- engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
2951
- }, [onSelect, onHover, onArrangementChange, onFovChange]);
3026
+ engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange, onLongPress });
3027
+ }, [onSelect, onHover, onArrangementChange, onFovChange, onLongPress]);
2952
3028
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
2953
3029
  }
2954
3030
  );