@project-skymap/library 0.6.0 → 0.7.1

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;
@@ -29243,7 +29244,7 @@ interface Projection {
29243
29244
  */
29244
29245
  isClipped(dirZ: number): boolean;
29245
29246
  }
29246
- type ProjectionId = "perspective" | "stereographic" | "blended";
29247
+ type ProjectionId = "perspective" | "stereographic";
29247
29248
  declare const PROJECTIONS: Record<ProjectionId, () => Projection>;
29248
29249
 
29249
29250
  export { type BibleJSON, type ConstellationConfig, type GenerateOptions, type HierarchyFilter, PROJECTIONS, type Projection, type ProjectionId, type SceneLink, type SceneModel, type SceneNode, type StarArrangement, StarMap, type StarMapConfig, type StarMapHandle, type StarMapProps, bibleToSceneModel, defaultGenerateOptions, defaultStars, generateArrangement };
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;
@@ -29243,7 +29244,7 @@ interface Projection {
29243
29244
  */
29244
29245
  isClipped(dirZ: number): boolean;
29245
29246
  }
29246
- type ProjectionId = "perspective" | "stereographic" | "blended";
29247
+ type ProjectionId = "perspective" | "stereographic";
29247
29248
  declare const PROJECTIONS: Record<ProjectionId, () => Projection>;
29248
29249
 
29249
29250
  export { type BibleJSON, type ConstellationConfig, type GenerateOptions, type HierarchyFilter, PROJECTIONS, type Projection, type ProjectionId, type SceneLink, type SceneModel, type SceneNode, type StarArrangement, StarMap, type StarMapConfig, type StarMapHandle, type StarMapProps, bibleToSceneModel, defaultGenerateOptions, defaultStars, generateArrangement };
package/dist/index.js CHANGED
@@ -724,10 +724,14 @@ var init_projections = __esm({
724
724
  maxFov = 165;
725
725
  glslProjectionType = 2;
726
726
  /** FOV thresholds for blend transition (degrees) */
727
- blendStart = 40;
728
- blendEnd = 100;
727
+ blendStart;
728
+ blendEnd;
729
729
  /** Current blend factor, updated via setFov() */
730
730
  blend = 0;
731
+ constructor(blendStart = 40, blendEnd = 100) {
732
+ this.blendStart = blendStart;
733
+ this.blendEnd = blendEnd;
734
+ }
731
735
  /** Call this each frame / when FOV changes so forward/inverse stay in sync */
732
736
  setFov(fovDeg) {
733
737
  if (fovDeg <= this.blendStart) {
@@ -780,8 +784,7 @@ var init_projections = __esm({
780
784
  };
781
785
  PROJECTIONS = {
782
786
  perspective: () => new PerspectiveProjection(),
783
- stereographic: () => new StereographicProjection(),
784
- blended: () => new BlendedProjection()
787
+ stereographic: () => new StereographicProjection()
785
788
  };
786
789
  }
787
790
  });
@@ -824,7 +827,8 @@ function createEngine({
824
827
  onSelect,
825
828
  onHover,
826
829
  onArrangementChange,
827
- onFovChange
830
+ onFovChange,
831
+ onLongPress
828
832
  }) {
829
833
  let hoveredBookId = null;
830
834
  let focusedBookId = null;
@@ -884,18 +888,36 @@ function createEngine({
884
888
  draggedStarIndex: -1,
885
889
  draggedDist: 2e3,
886
890
  draggedGroup: null,
887
- tempArrangement: {}
891
+ tempArrangement: {},
892
+ // Touch state
893
+ touchCount: 0,
894
+ touchStartTime: 0,
895
+ touchStartX: 0,
896
+ touchStartY: 0,
897
+ touchMoved: false,
898
+ pinchStartDistance: 0,
899
+ pinchStartFov: ENGINE_CONFIG.defaultFov,
900
+ pinchCenterX: 0,
901
+ pinchCenterY: 0,
902
+ // Double-tap detection
903
+ lastTapTime: 0,
904
+ lastTapX: 0,
905
+ lastTapY: 0,
906
+ // Long-press detection
907
+ longPressTimer: null,
908
+ longPressTriggered: false
888
909
  };
889
910
  const mouseNDC = new THREE5.Vector2();
890
911
  let isMouseInWindow = false;
912
+ let isTouchDevice = false;
891
913
  let edgeHoverStart = 0;
892
- let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
914
+ let handlers = { onSelect, onHover, onArrangementChange, onFovChange, onLongPress };
893
915
  let currentConfig;
894
916
  const constellationLayer = new ConstellationArtworkLayer(scene);
895
917
  function mix(a, b, t) {
896
918
  return a * (1 - t) + b * t;
897
919
  }
898
- let currentProjection = PROJECTIONS.blended();
920
+ let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
899
921
  function syncProjectionState() {
900
922
  if (currentProjection instanceof BlendedProjection) {
901
923
  currentProjection.setFov(state.fov);
@@ -1933,9 +1955,13 @@ function createEngine({
1933
1955
  let lastAppliedLat = void 0;
1934
1956
  let lastBackdropCount = void 0;
1935
1957
  function setProjection(id) {
1936
- const factory = PROJECTIONS[id];
1937
- if (!factory) return;
1938
- currentProjection = factory();
1958
+ if (id === "blended") {
1959
+ currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
1960
+ } else {
1961
+ const factory = PROJECTIONS[id];
1962
+ if (!factory) return;
1963
+ currentProjection = factory();
1964
+ }
1939
1965
  updateUniforms();
1940
1966
  }
1941
1967
  function setConfig(cfg) {
@@ -2050,7 +2076,8 @@ function createEngine({
2050
2076
  const w = rect.width;
2051
2077
  const h = rect.height;
2052
2078
  let closestLabel = null;
2053
- let minLabelDist = 40;
2079
+ const LABEL_THRESHOLD = isTouchDevice ? 48 : 40;
2080
+ let minLabelDist = LABEL_THRESHOLD;
2054
2081
  for (const item of dynamicLabels) {
2055
2082
  if (!item.obj.visible) continue;
2056
2083
  if (isNodeFiltered(item.node)) continue;
@@ -2363,6 +2390,196 @@ function createEngine({
2363
2390
  state.targetLat = state.lat;
2364
2391
  state.targetLon = state.lon;
2365
2392
  }
2393
+ function getTouchDistance(t1, t2) {
2394
+ const dx = t1.clientX - t2.clientX;
2395
+ const dy = t1.clientY - t2.clientY;
2396
+ return Math.sqrt(dx * dx + dy * dy);
2397
+ }
2398
+ function getTouchCenter(t1, t2) {
2399
+ return {
2400
+ x: (t1.clientX + t2.clientX) / 2,
2401
+ y: (t1.clientY + t2.clientY) / 2
2402
+ };
2403
+ }
2404
+ function onTouchStart(e) {
2405
+ e.preventDefault();
2406
+ isTouchDevice = true;
2407
+ if (state.longPressTimer) {
2408
+ clearTimeout(state.longPressTimer);
2409
+ state.longPressTimer = null;
2410
+ }
2411
+ state.longPressTriggered = false;
2412
+ const touches = e.touches;
2413
+ state.touchCount = touches.length;
2414
+ if (touches.length === 1) {
2415
+ const touch = touches[0];
2416
+ state.touchStartTime = performance.now();
2417
+ state.touchStartX = touch.clientX;
2418
+ state.touchStartY = touch.clientY;
2419
+ state.touchMoved = false;
2420
+ state.lastMouseX = touch.clientX;
2421
+ state.lastMouseY = touch.clientY;
2422
+ flyToActive = false;
2423
+ state.dragMode = "camera";
2424
+ state.isDragging = true;
2425
+ state.velocityX = 0;
2426
+ state.velocityY = 0;
2427
+ state.longPressTimer = setTimeout(() => {
2428
+ if (!state.touchMoved && state.touchCount === 1) {
2429
+ state.longPressTriggered = true;
2430
+ const rect = renderer.domElement.getBoundingClientRect();
2431
+ const mX = state.touchStartX - rect.left;
2432
+ const mY = state.touchStartY - rect.top;
2433
+ mouseNDC.x = mX / rect.width * 2 - 1;
2434
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2435
+ const syntheticEvent = {
2436
+ clientX: state.touchStartX,
2437
+ clientY: state.touchStartY
2438
+ };
2439
+ const hit = pick(syntheticEvent);
2440
+ handlers.onLongPress?.(hit?.node ?? null, state.touchStartX, state.touchStartY);
2441
+ }
2442
+ }, ENGINE_CONFIG.longPressDelay);
2443
+ } else if (touches.length === 2) {
2444
+ const t0 = touches[0];
2445
+ const t1 = touches[1];
2446
+ state.pinchStartDistance = getTouchDistance(t0, t1);
2447
+ state.pinchStartFov = state.fov;
2448
+ const center = getTouchCenter(t0, t1);
2449
+ state.pinchCenterX = center.x;
2450
+ state.pinchCenterY = center.y;
2451
+ state.lastMouseX = center.x;
2452
+ state.lastMouseY = center.y;
2453
+ state.touchMoved = true;
2454
+ }
2455
+ }
2456
+ function onTouchMove(e) {
2457
+ e.preventDefault();
2458
+ const touches = e.touches;
2459
+ if (touches.length === 1 && state.dragMode === "camera") {
2460
+ const touch = touches[0];
2461
+ const deltaX = touch.clientX - state.lastMouseX;
2462
+ const deltaY = touch.clientY - state.lastMouseY;
2463
+ state.lastMouseX = touch.clientX;
2464
+ state.lastMouseY = touch.clientY;
2465
+ const totalDx = touch.clientX - state.touchStartX;
2466
+ const totalDy = touch.clientY - state.touchStartY;
2467
+ if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
2468
+ state.touchMoved = true;
2469
+ if (state.longPressTimer) {
2470
+ clearTimeout(state.longPressTimer);
2471
+ state.longPressTimer = null;
2472
+ }
2473
+ }
2474
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2475
+ const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
2476
+ const latFactor = 1 - rotLock * rotLock;
2477
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2478
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2479
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2480
+ state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2481
+ state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2482
+ state.lon = state.targetLon;
2483
+ state.lat = state.targetLat;
2484
+ } else if (touches.length === 2) {
2485
+ const t0 = touches[0];
2486
+ const t1 = touches[1];
2487
+ const newDistance = getTouchDistance(t0, t1);
2488
+ const scale = newDistance / state.pinchStartDistance;
2489
+ state.fov = state.pinchStartFov / scale;
2490
+ state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
2491
+ handlers.onFovChange?.(state.fov);
2492
+ const center = getTouchCenter(t0, t1);
2493
+ const deltaX = center.x - state.lastMouseX;
2494
+ const deltaY = center.y - state.lastMouseY;
2495
+ state.lastMouseX = center.x;
2496
+ state.lastMouseY = center.y;
2497
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2498
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2499
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2500
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2501
+ state.lon = state.targetLon;
2502
+ state.lat = state.targetLat;
2503
+ }
2504
+ }
2505
+ function onTouchEnd(e) {
2506
+ e.preventDefault();
2507
+ if (state.longPressTimer) {
2508
+ clearTimeout(state.longPressTimer);
2509
+ state.longPressTimer = null;
2510
+ }
2511
+ const remainingTouches = e.touches.length;
2512
+ if (remainingTouches === 0) {
2513
+ const now = performance.now();
2514
+ const duration = now - state.touchStartTime;
2515
+ const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration && !state.longPressTriggered;
2516
+ if (wasTap) {
2517
+ const timeSinceLastTap = now - state.lastTapTime;
2518
+ const distFromLastTap = Math.sqrt(
2519
+ Math.pow(state.touchStartX - state.lastTapX, 2) + Math.pow(state.touchStartY - state.lastTapY, 2)
2520
+ );
2521
+ const isDoubleTap = timeSinceLastTap < ENGINE_CONFIG.doubleTapMaxDelay && distFromLastTap < ENGINE_CONFIG.doubleTapMaxDistance;
2522
+ const rect = renderer.domElement.getBoundingClientRect();
2523
+ const mX = state.touchStartX - rect.left;
2524
+ const mY = state.touchStartY - rect.top;
2525
+ mouseNDC.x = mX / rect.width * 2 - 1;
2526
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2527
+ const syntheticEvent = {
2528
+ clientX: state.touchStartX,
2529
+ clientY: state.touchStartY
2530
+ };
2531
+ const hit = pick(syntheticEvent);
2532
+ if (isDoubleTap) {
2533
+ if (hit) {
2534
+ flyTo(hit.node.id, ENGINE_CONFIG.minFov);
2535
+ handlers.onSelect?.(hit.node);
2536
+ }
2537
+ state.lastTapTime = 0;
2538
+ state.lastTapX = 0;
2539
+ state.lastTapY = 0;
2540
+ } else {
2541
+ if (hit) {
2542
+ handlers.onSelect?.(hit.node);
2543
+ constellationLayer.setFocused(hit.node.id);
2544
+ if (hit.node.level === 2) setFocusedBook(hit.node.id);
2545
+ else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
2546
+ } else {
2547
+ setFocusedBook(null);
2548
+ }
2549
+ state.lastTapTime = now;
2550
+ state.lastTapX = state.touchStartX;
2551
+ state.lastTapY = state.touchStartY;
2552
+ }
2553
+ }
2554
+ state.isDragging = false;
2555
+ state.dragMode = "none";
2556
+ state.touchCount = 0;
2557
+ } else if (remainingTouches === 1) {
2558
+ const touch = e.touches[0];
2559
+ state.lastMouseX = touch.clientX;
2560
+ state.lastMouseY = touch.clientY;
2561
+ state.touchCount = 1;
2562
+ state.dragMode = "camera";
2563
+ state.isDragging = true;
2564
+ state.velocityX = 0;
2565
+ state.velocityY = 0;
2566
+ }
2567
+ }
2568
+ function onTouchCancel(e) {
2569
+ e.preventDefault();
2570
+ if (state.longPressTimer) {
2571
+ clearTimeout(state.longPressTimer);
2572
+ state.longPressTimer = null;
2573
+ }
2574
+ state.isDragging = false;
2575
+ state.dragMode = "none";
2576
+ state.touchCount = 0;
2577
+ state.velocityX = 0;
2578
+ state.velocityY = 0;
2579
+ }
2580
+ function onGesturePrevent(e) {
2581
+ e.preventDefault();
2582
+ }
2366
2583
  function resize() {
2367
2584
  const w = container.clientWidth || 1;
2368
2585
  const h = container.clientHeight || 1;
@@ -2385,6 +2602,13 @@ function createEngine({
2385
2602
  });
2386
2603
  el.addEventListener("mouseleave", onWindowBlur);
2387
2604
  window.addEventListener("blur", onWindowBlur);
2605
+ el.addEventListener("touchstart", onTouchStart, { passive: false });
2606
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
2607
+ el.addEventListener("touchend", onTouchEnd, { passive: false });
2608
+ el.addEventListener("touchcancel", onTouchCancel, { passive: false });
2609
+ el.addEventListener("gesturestart", onGesturePrevent, { passive: false });
2610
+ el.addEventListener("gesturechange", onGesturePrevent, { passive: false });
2611
+ el.addEventListener("gestureend", onGesturePrevent, { passive: false });
2388
2612
  raf = requestAnimationFrame(tick);
2389
2613
  }
2390
2614
  function tick() {
@@ -2426,7 +2650,7 @@ function createEngine({
2426
2650
  }
2427
2651
  let panX = 0;
2428
2652
  let panY = 0;
2429
- if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
2653
+ if (!state.isDragging && isMouseInWindow && !currentConfig?.editable && !isTouchDevice) {
2430
2654
  const t = ENGINE_CONFIG.edgePanThreshold;
2431
2655
  const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
2432
2656
  const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
@@ -2479,8 +2703,9 @@ function createEngine({
2479
2703
  } else if (!state.isDragging && !flyToActive) {
2480
2704
  state.lon += state.velocityX;
2481
2705
  state.lat += state.velocityY;
2482
- state.velocityX *= ENGINE_CONFIG.inertiaDamping;
2483
- state.velocityY *= ENGINE_CONFIG.inertiaDamping;
2706
+ const damping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
2707
+ state.velocityX *= damping;
2708
+ state.velocityY *= damping;
2484
2709
  if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
2485
2710
  if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
2486
2711
  }
@@ -2652,6 +2877,13 @@ function createEngine({
2652
2877
  el.removeEventListener("wheel", onWheel);
2653
2878
  el.removeEventListener("mouseleave", onWindowBlur);
2654
2879
  window.removeEventListener("blur", onWindowBlur);
2880
+ el.removeEventListener("touchstart", onTouchStart);
2881
+ el.removeEventListener("touchmove", onTouchMove);
2882
+ el.removeEventListener("touchend", onTouchEnd);
2883
+ el.removeEventListener("touchcancel", onTouchCancel);
2884
+ el.removeEventListener("gesturestart", onGesturePrevent);
2885
+ el.removeEventListener("gesturechange", onGesturePrevent);
2886
+ el.removeEventListener("gestureend", onGesturePrevent);
2655
2887
  }
2656
2888
  function dispose() {
2657
2889
  stop();
@@ -2706,7 +2938,7 @@ var init_createEngine = __esm({
2706
2938
  init_projections();
2707
2939
  init_fader();
2708
2940
  ENGINE_CONFIG = {
2709
- minFov: 10,
2941
+ minFov: 1,
2710
2942
  maxFov: 135,
2711
2943
  defaultFov: 50,
2712
2944
  dragSpeed: 125e-5,
@@ -2718,7 +2950,20 @@ var init_createEngine = __esm({
2718
2950
  horizonLockStrength: 0.05,
2719
2951
  edgePanThreshold: 0.15,
2720
2952
  edgePanMaxSpeed: 0.02,
2721
- edgePanDelay: 250
2953
+ edgePanDelay: 250,
2954
+ // Touch-specific
2955
+ touchInertiaDamping: 0.85,
2956
+ // Snappier than mouse (0.92)
2957
+ tapMaxDuration: 300,
2958
+ // ms
2959
+ tapMaxDistance: 10,
2960
+ // px
2961
+ doubleTapMaxDelay: 300,
2962
+ // ms between taps
2963
+ doubleTapMaxDistance: 30,
2964
+ // px between tap locations
2965
+ longPressDelay: 500
2966
+ // ms to trigger long-press
2722
2967
  };
2723
2968
  ORDER_REVEAL_CONFIG = {
2724
2969
  globalDim: 0.85,
@@ -2729,7 +2974,7 @@ var init_createEngine = __esm({
2729
2974
  }
2730
2975
  });
2731
2976
  var StarMap = forwardRef(
2732
- ({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
2977
+ ({ config, className, onSelect, onHover, onArrangementChange, onFovChange, onLongPress }, ref) => {
2733
2978
  const containerRef = useRef(null);
2734
2979
  const engineRef = useRef(null);
2735
2980
  useImperativeHandle(ref, () => ({
@@ -2752,7 +2997,8 @@ var StarMap = forwardRef(
2752
2997
  onSelect,
2753
2998
  onHover,
2754
2999
  onArrangementChange,
2755
- onFovChange
3000
+ onFovChange,
3001
+ onLongPress
2756
3002
  });
2757
3003
  engineRef.current.setConfig(config);
2758
3004
  engineRef.current.start();
@@ -2768,8 +3014,8 @@ var StarMap = forwardRef(
2768
3014
  engineRef.current?.setConfig?.(config);
2769
3015
  }, [config]);
2770
3016
  useEffect(() => {
2771
- engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
2772
- }, [onSelect, onHover, onArrangementChange, onFovChange]);
3017
+ engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange, onLongPress });
3018
+ }, [onSelect, onHover, onArrangementChange, onFovChange, onLongPress]);
2773
3019
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
2774
3020
  }
2775
3021
  );