@project-skymap/library 0.6.0 → 0.7.0

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
@@ -746,10 +746,14 @@ var init_projections = __esm({
746
746
  maxFov = 165;
747
747
  glslProjectionType = 2;
748
748
  /** FOV thresholds for blend transition (degrees) */
749
- blendStart = 40;
750
- blendEnd = 100;
749
+ blendStart;
750
+ blendEnd;
751
751
  /** Current blend factor, updated via setFov() */
752
752
  blend = 0;
753
+ constructor(blendStart = 40, blendEnd = 100) {
754
+ this.blendStart = blendStart;
755
+ this.blendEnd = blendEnd;
756
+ }
753
757
  /** Call this each frame / when FOV changes so forward/inverse stay in sync */
754
758
  setFov(fovDeg) {
755
759
  if (fovDeg <= this.blendStart) {
@@ -802,8 +806,7 @@ var init_projections = __esm({
802
806
  };
803
807
  exports.PROJECTIONS = {
804
808
  perspective: () => new PerspectiveProjection(),
805
- stereographic: () => new StereographicProjection(),
806
- blended: () => new BlendedProjection()
809
+ stereographic: () => new StereographicProjection()
807
810
  };
808
811
  }
809
812
  });
@@ -906,10 +909,21 @@ function createEngine({
906
909
  draggedStarIndex: -1,
907
910
  draggedDist: 2e3,
908
911
  draggedGroup: null,
909
- tempArrangement: {}
912
+ tempArrangement: {},
913
+ // Touch state
914
+ touchCount: 0,
915
+ touchStartTime: 0,
916
+ touchStartX: 0,
917
+ touchStartY: 0,
918
+ touchMoved: false,
919
+ pinchStartDistance: 0,
920
+ pinchStartFov: ENGINE_CONFIG.defaultFov,
921
+ pinchCenterX: 0,
922
+ pinchCenterY: 0
910
923
  };
911
924
  const mouseNDC = new THREE5__namespace.Vector2();
912
925
  let isMouseInWindow = false;
926
+ let isTouchDevice = false;
913
927
  let edgeHoverStart = 0;
914
928
  let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
915
929
  let currentConfig;
@@ -917,7 +931,7 @@ function createEngine({
917
931
  function mix(a, b, t) {
918
932
  return a * (1 - t) + b * t;
919
933
  }
920
- let currentProjection = exports.PROJECTIONS.blended();
934
+ let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
921
935
  function syncProjectionState() {
922
936
  if (currentProjection instanceof BlendedProjection) {
923
937
  currentProjection.setFov(state.fov);
@@ -1955,9 +1969,13 @@ function createEngine({
1955
1969
  let lastAppliedLat = void 0;
1956
1970
  let lastBackdropCount = void 0;
1957
1971
  function setProjection(id) {
1958
- const factory = exports.PROJECTIONS[id];
1959
- if (!factory) return;
1960
- currentProjection = factory();
1972
+ if (id === "blended") {
1973
+ currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
1974
+ } else {
1975
+ const factory = exports.PROJECTIONS[id];
1976
+ if (!factory) return;
1977
+ currentProjection = factory();
1978
+ }
1961
1979
  updateUniforms();
1962
1980
  }
1963
1981
  function setConfig(cfg) {
@@ -2072,7 +2090,8 @@ function createEngine({
2072
2090
  const w = rect.width;
2073
2091
  const h = rect.height;
2074
2092
  let closestLabel = null;
2075
- let minLabelDist = 40;
2093
+ const LABEL_THRESHOLD = isTouchDevice ? 48 : 40;
2094
+ let minLabelDist = LABEL_THRESHOLD;
2076
2095
  for (const item of dynamicLabels) {
2077
2096
  if (!item.obj.visible) continue;
2078
2097
  if (isNodeFiltered(item.node)) continue;
@@ -2385,6 +2404,144 @@ function createEngine({
2385
2404
  state.targetLat = state.lat;
2386
2405
  state.targetLon = state.lon;
2387
2406
  }
2407
+ function getTouchDistance(t1, t2) {
2408
+ const dx = t1.clientX - t2.clientX;
2409
+ const dy = t1.clientY - t2.clientY;
2410
+ return Math.sqrt(dx * dx + dy * dy);
2411
+ }
2412
+ function getTouchCenter(t1, t2) {
2413
+ return {
2414
+ x: (t1.clientX + t2.clientX) / 2,
2415
+ y: (t1.clientY + t2.clientY) / 2
2416
+ };
2417
+ }
2418
+ function onTouchStart(e) {
2419
+ e.preventDefault();
2420
+ isTouchDevice = true;
2421
+ const touches = e.touches;
2422
+ state.touchCount = touches.length;
2423
+ if (touches.length === 1) {
2424
+ const touch = touches[0];
2425
+ state.touchStartTime = performance.now();
2426
+ state.touchStartX = touch.clientX;
2427
+ state.touchStartY = touch.clientY;
2428
+ state.touchMoved = false;
2429
+ state.lastMouseX = touch.clientX;
2430
+ state.lastMouseY = touch.clientY;
2431
+ flyToActive = false;
2432
+ state.dragMode = "camera";
2433
+ state.isDragging = true;
2434
+ state.velocityX = 0;
2435
+ state.velocityY = 0;
2436
+ } else if (touches.length === 2) {
2437
+ const t0 = touches[0];
2438
+ const t1 = touches[1];
2439
+ state.pinchStartDistance = getTouchDistance(t0, t1);
2440
+ state.pinchStartFov = state.fov;
2441
+ const center = getTouchCenter(t0, t1);
2442
+ state.pinchCenterX = center.x;
2443
+ state.pinchCenterY = center.y;
2444
+ state.lastMouseX = center.x;
2445
+ state.lastMouseY = center.y;
2446
+ state.touchMoved = true;
2447
+ }
2448
+ }
2449
+ function onTouchMove(e) {
2450
+ e.preventDefault();
2451
+ const touches = e.touches;
2452
+ if (touches.length === 1 && state.dragMode === "camera") {
2453
+ const touch = touches[0];
2454
+ const deltaX = touch.clientX - state.lastMouseX;
2455
+ const deltaY = touch.clientY - state.lastMouseY;
2456
+ state.lastMouseX = touch.clientX;
2457
+ state.lastMouseY = touch.clientY;
2458
+ const totalDx = touch.clientX - state.touchStartX;
2459
+ const totalDy = touch.clientY - state.touchStartY;
2460
+ if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
2461
+ state.touchMoved = true;
2462
+ }
2463
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2464
+ const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
2465
+ const latFactor = 1 - rotLock * rotLock;
2466
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2467
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2468
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2469
+ state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2470
+ state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2471
+ state.lon = state.targetLon;
2472
+ state.lat = state.targetLat;
2473
+ } else if (touches.length === 2) {
2474
+ const t0 = touches[0];
2475
+ const t1 = touches[1];
2476
+ const newDistance = getTouchDistance(t0, t1);
2477
+ const scale = newDistance / state.pinchStartDistance;
2478
+ state.fov = state.pinchStartFov / scale;
2479
+ state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
2480
+ handlers.onFovChange?.(state.fov);
2481
+ const center = getTouchCenter(t0, t1);
2482
+ const deltaX = center.x - state.lastMouseX;
2483
+ const deltaY = center.y - state.lastMouseY;
2484
+ state.lastMouseX = center.x;
2485
+ state.lastMouseY = center.y;
2486
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2487
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2488
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2489
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2490
+ state.lon = state.targetLon;
2491
+ state.lat = state.targetLat;
2492
+ }
2493
+ }
2494
+ function onTouchEnd(e) {
2495
+ e.preventDefault();
2496
+ const remainingTouches = e.touches.length;
2497
+ if (remainingTouches === 0) {
2498
+ const duration = performance.now() - state.touchStartTime;
2499
+ const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
2500
+ if (wasTap) {
2501
+ const rect = renderer.domElement.getBoundingClientRect();
2502
+ const mX = state.touchStartX - rect.left;
2503
+ const mY = state.touchStartY - rect.top;
2504
+ mouseNDC.x = mX / rect.width * 2 - 1;
2505
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2506
+ const syntheticEvent = {
2507
+ clientX: state.touchStartX,
2508
+ clientY: state.touchStartY
2509
+ };
2510
+ 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);
2516
+ } else {
2517
+ setFocusedBook(null);
2518
+ }
2519
+ }
2520
+ state.isDragging = false;
2521
+ state.dragMode = "none";
2522
+ state.touchCount = 0;
2523
+ } else if (remainingTouches === 1) {
2524
+ const touch = e.touches[0];
2525
+ state.lastMouseX = touch.clientX;
2526
+ state.lastMouseY = touch.clientY;
2527
+ state.touchCount = 1;
2528
+ state.dragMode = "camera";
2529
+ state.isDragging = true;
2530
+ state.velocityX = 0;
2531
+ state.velocityY = 0;
2532
+ }
2533
+ }
2534
+ function onTouchCancel(e) {
2535
+ e.preventDefault();
2536
+ state.isDragging = false;
2537
+ state.dragMode = "none";
2538
+ state.touchCount = 0;
2539
+ state.velocityX = 0;
2540
+ state.velocityY = 0;
2541
+ }
2542
+ function onGesturePrevent(e) {
2543
+ e.preventDefault();
2544
+ }
2388
2545
  function resize() {
2389
2546
  const w = container.clientWidth || 1;
2390
2547
  const h = container.clientHeight || 1;
@@ -2407,6 +2564,13 @@ function createEngine({
2407
2564
  });
2408
2565
  el.addEventListener("mouseleave", onWindowBlur);
2409
2566
  window.addEventListener("blur", onWindowBlur);
2567
+ el.addEventListener("touchstart", onTouchStart, { passive: false });
2568
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
2569
+ el.addEventListener("touchend", onTouchEnd, { passive: false });
2570
+ el.addEventListener("touchcancel", onTouchCancel, { passive: false });
2571
+ el.addEventListener("gesturestart", onGesturePrevent, { passive: false });
2572
+ el.addEventListener("gesturechange", onGesturePrevent, { passive: false });
2573
+ el.addEventListener("gestureend", onGesturePrevent, { passive: false });
2410
2574
  raf = requestAnimationFrame(tick);
2411
2575
  }
2412
2576
  function tick() {
@@ -2448,7 +2612,7 @@ function createEngine({
2448
2612
  }
2449
2613
  let panX = 0;
2450
2614
  let panY = 0;
2451
- if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
2615
+ if (!state.isDragging && isMouseInWindow && !currentConfig?.editable && !isTouchDevice) {
2452
2616
  const t = ENGINE_CONFIG.edgePanThreshold;
2453
2617
  const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
2454
2618
  const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
@@ -2501,8 +2665,9 @@ function createEngine({
2501
2665
  } else if (!state.isDragging && !flyToActive) {
2502
2666
  state.lon += state.velocityX;
2503
2667
  state.lat += state.velocityY;
2504
- state.velocityX *= ENGINE_CONFIG.inertiaDamping;
2505
- state.velocityY *= ENGINE_CONFIG.inertiaDamping;
2668
+ const damping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
2669
+ state.velocityX *= damping;
2670
+ state.velocityY *= damping;
2506
2671
  if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
2507
2672
  if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
2508
2673
  }
@@ -2674,6 +2839,13 @@ function createEngine({
2674
2839
  el.removeEventListener("wheel", onWheel);
2675
2840
  el.removeEventListener("mouseleave", onWindowBlur);
2676
2841
  window.removeEventListener("blur", onWindowBlur);
2842
+ el.removeEventListener("touchstart", onTouchStart);
2843
+ el.removeEventListener("touchmove", onTouchMove);
2844
+ el.removeEventListener("touchend", onTouchEnd);
2845
+ el.removeEventListener("touchcancel", onTouchCancel);
2846
+ el.removeEventListener("gesturestart", onGesturePrevent);
2847
+ el.removeEventListener("gesturechange", onGesturePrevent);
2848
+ el.removeEventListener("gestureend", onGesturePrevent);
2677
2849
  }
2678
2850
  function dispose() {
2679
2851
  stop();
@@ -2728,7 +2900,7 @@ var init_createEngine = __esm({
2728
2900
  init_projections();
2729
2901
  init_fader();
2730
2902
  ENGINE_CONFIG = {
2731
- minFov: 10,
2903
+ minFov: 1,
2732
2904
  maxFov: 135,
2733
2905
  defaultFov: 50,
2734
2906
  dragSpeed: 125e-5,
@@ -2740,7 +2912,14 @@ var init_createEngine = __esm({
2740
2912
  horizonLockStrength: 0.05,
2741
2913
  edgePanThreshold: 0.15,
2742
2914
  edgePanMaxSpeed: 0.02,
2743
- edgePanDelay: 250
2915
+ edgePanDelay: 250,
2916
+ // Touch-specific
2917
+ touchInertiaDamping: 0.85,
2918
+ // Snappier than mouse (0.92)
2919
+ tapMaxDuration: 300,
2920
+ // ms
2921
+ tapMaxDistance: 10
2922
+ // px
2744
2923
  };
2745
2924
  ORDER_REVEAL_CONFIG = {
2746
2925
  globalDim: 0.85,