@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.d.cts CHANGED
@@ -29243,7 +29243,7 @@ interface Projection {
29243
29243
  */
29244
29244
  isClipped(dirZ: number): boolean;
29245
29245
  }
29246
- type ProjectionId = "perspective" | "stereographic" | "blended";
29246
+ type ProjectionId = "perspective" | "stereographic";
29247
29247
  declare const PROJECTIONS: Record<ProjectionId, () => Projection>;
29248
29248
 
29249
29249
  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
@@ -29243,7 +29243,7 @@ interface Projection {
29243
29243
  */
29244
29244
  isClipped(dirZ: number): boolean;
29245
29245
  }
29246
- type ProjectionId = "perspective" | "stereographic" | "blended";
29246
+ type ProjectionId = "perspective" | "stereographic";
29247
29247
  declare const PROJECTIONS: Record<ProjectionId, () => Projection>;
29248
29248
 
29249
29249
  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
  });
@@ -884,10 +887,21 @@ function createEngine({
884
887
  draggedStarIndex: -1,
885
888
  draggedDist: 2e3,
886
889
  draggedGroup: null,
887
- tempArrangement: {}
890
+ tempArrangement: {},
891
+ // Touch state
892
+ touchCount: 0,
893
+ touchStartTime: 0,
894
+ touchStartX: 0,
895
+ touchStartY: 0,
896
+ touchMoved: false,
897
+ pinchStartDistance: 0,
898
+ pinchStartFov: ENGINE_CONFIG.defaultFov,
899
+ pinchCenterX: 0,
900
+ pinchCenterY: 0
888
901
  };
889
902
  const mouseNDC = new THREE5.Vector2();
890
903
  let isMouseInWindow = false;
904
+ let isTouchDevice = false;
891
905
  let edgeHoverStart = 0;
892
906
  let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
893
907
  let currentConfig;
@@ -895,7 +909,7 @@ function createEngine({
895
909
  function mix(a, b, t) {
896
910
  return a * (1 - t) + b * t;
897
911
  }
898
- let currentProjection = PROJECTIONS.blended();
912
+ let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
899
913
  function syncProjectionState() {
900
914
  if (currentProjection instanceof BlendedProjection) {
901
915
  currentProjection.setFov(state.fov);
@@ -1933,9 +1947,13 @@ function createEngine({
1933
1947
  let lastAppliedLat = void 0;
1934
1948
  let lastBackdropCount = void 0;
1935
1949
  function setProjection(id) {
1936
- const factory = PROJECTIONS[id];
1937
- if (!factory) return;
1938
- currentProjection = factory();
1950
+ if (id === "blended") {
1951
+ currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
1952
+ } else {
1953
+ const factory = PROJECTIONS[id];
1954
+ if (!factory) return;
1955
+ currentProjection = factory();
1956
+ }
1939
1957
  updateUniforms();
1940
1958
  }
1941
1959
  function setConfig(cfg) {
@@ -2050,7 +2068,8 @@ function createEngine({
2050
2068
  const w = rect.width;
2051
2069
  const h = rect.height;
2052
2070
  let closestLabel = null;
2053
- let minLabelDist = 40;
2071
+ const LABEL_THRESHOLD = isTouchDevice ? 48 : 40;
2072
+ let minLabelDist = LABEL_THRESHOLD;
2054
2073
  for (const item of dynamicLabels) {
2055
2074
  if (!item.obj.visible) continue;
2056
2075
  if (isNodeFiltered(item.node)) continue;
@@ -2363,6 +2382,144 @@ function createEngine({
2363
2382
  state.targetLat = state.lat;
2364
2383
  state.targetLon = state.lon;
2365
2384
  }
2385
+ function getTouchDistance(t1, t2) {
2386
+ const dx = t1.clientX - t2.clientX;
2387
+ const dy = t1.clientY - t2.clientY;
2388
+ return Math.sqrt(dx * dx + dy * dy);
2389
+ }
2390
+ function getTouchCenter(t1, t2) {
2391
+ return {
2392
+ x: (t1.clientX + t2.clientX) / 2,
2393
+ y: (t1.clientY + t2.clientY) / 2
2394
+ };
2395
+ }
2396
+ function onTouchStart(e) {
2397
+ e.preventDefault();
2398
+ isTouchDevice = true;
2399
+ const touches = e.touches;
2400
+ state.touchCount = touches.length;
2401
+ if (touches.length === 1) {
2402
+ const touch = touches[0];
2403
+ state.touchStartTime = performance.now();
2404
+ state.touchStartX = touch.clientX;
2405
+ state.touchStartY = touch.clientY;
2406
+ state.touchMoved = false;
2407
+ state.lastMouseX = touch.clientX;
2408
+ state.lastMouseY = touch.clientY;
2409
+ flyToActive = false;
2410
+ state.dragMode = "camera";
2411
+ state.isDragging = true;
2412
+ state.velocityX = 0;
2413
+ state.velocityY = 0;
2414
+ } else if (touches.length === 2) {
2415
+ const t0 = touches[0];
2416
+ const t1 = touches[1];
2417
+ state.pinchStartDistance = getTouchDistance(t0, t1);
2418
+ state.pinchStartFov = state.fov;
2419
+ const center = getTouchCenter(t0, t1);
2420
+ state.pinchCenterX = center.x;
2421
+ state.pinchCenterY = center.y;
2422
+ state.lastMouseX = center.x;
2423
+ state.lastMouseY = center.y;
2424
+ state.touchMoved = true;
2425
+ }
2426
+ }
2427
+ function onTouchMove(e) {
2428
+ e.preventDefault();
2429
+ const touches = e.touches;
2430
+ if (touches.length === 1 && state.dragMode === "camera") {
2431
+ const touch = touches[0];
2432
+ const deltaX = touch.clientX - state.lastMouseX;
2433
+ const deltaY = touch.clientY - state.lastMouseY;
2434
+ state.lastMouseX = touch.clientX;
2435
+ state.lastMouseY = touch.clientY;
2436
+ const totalDx = touch.clientX - state.touchStartX;
2437
+ const totalDy = touch.clientY - state.touchStartY;
2438
+ if (Math.sqrt(totalDx * totalDx + totalDy * totalDy) > ENGINE_CONFIG.tapMaxDistance) {
2439
+ state.touchMoved = true;
2440
+ }
2441
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2442
+ const rotLock = Math.max(0, Math.min(1, (state.fov - 100) / (ENGINE_CONFIG.maxFov - 100)));
2443
+ const latFactor = 1 - rotLock * rotLock;
2444
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2445
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2446
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2447
+ state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
2448
+ state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
2449
+ state.lon = state.targetLon;
2450
+ state.lat = state.targetLat;
2451
+ } else if (touches.length === 2) {
2452
+ const t0 = touches[0];
2453
+ const t1 = touches[1];
2454
+ const newDistance = getTouchDistance(t0, t1);
2455
+ const scale = newDistance / state.pinchStartDistance;
2456
+ state.fov = state.pinchStartFov / scale;
2457
+ state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
2458
+ handlers.onFovChange?.(state.fov);
2459
+ const center = getTouchCenter(t0, t1);
2460
+ const deltaX = center.x - state.lastMouseX;
2461
+ const deltaY = center.y - state.lastMouseY;
2462
+ state.lastMouseX = center.x;
2463
+ state.lastMouseY = center.y;
2464
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
2465
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2466
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
2467
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
2468
+ state.lon = state.targetLon;
2469
+ state.lat = state.targetLat;
2470
+ }
2471
+ }
2472
+ function onTouchEnd(e) {
2473
+ e.preventDefault();
2474
+ const remainingTouches = e.touches.length;
2475
+ if (remainingTouches === 0) {
2476
+ const duration = performance.now() - state.touchStartTime;
2477
+ const wasTap = !state.touchMoved && duration < ENGINE_CONFIG.tapMaxDuration;
2478
+ if (wasTap) {
2479
+ const rect = renderer.domElement.getBoundingClientRect();
2480
+ const mX = state.touchStartX - rect.left;
2481
+ const mY = state.touchStartY - rect.top;
2482
+ mouseNDC.x = mX / rect.width * 2 - 1;
2483
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
2484
+ const syntheticEvent = {
2485
+ clientX: state.touchStartX,
2486
+ clientY: state.touchStartY
2487
+ };
2488
+ 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);
2494
+ } else {
2495
+ setFocusedBook(null);
2496
+ }
2497
+ }
2498
+ state.isDragging = false;
2499
+ state.dragMode = "none";
2500
+ state.touchCount = 0;
2501
+ } else if (remainingTouches === 1) {
2502
+ const touch = e.touches[0];
2503
+ state.lastMouseX = touch.clientX;
2504
+ state.lastMouseY = touch.clientY;
2505
+ state.touchCount = 1;
2506
+ state.dragMode = "camera";
2507
+ state.isDragging = true;
2508
+ state.velocityX = 0;
2509
+ state.velocityY = 0;
2510
+ }
2511
+ }
2512
+ function onTouchCancel(e) {
2513
+ e.preventDefault();
2514
+ state.isDragging = false;
2515
+ state.dragMode = "none";
2516
+ state.touchCount = 0;
2517
+ state.velocityX = 0;
2518
+ state.velocityY = 0;
2519
+ }
2520
+ function onGesturePrevent(e) {
2521
+ e.preventDefault();
2522
+ }
2366
2523
  function resize() {
2367
2524
  const w = container.clientWidth || 1;
2368
2525
  const h = container.clientHeight || 1;
@@ -2385,6 +2542,13 @@ function createEngine({
2385
2542
  });
2386
2543
  el.addEventListener("mouseleave", onWindowBlur);
2387
2544
  window.addEventListener("blur", onWindowBlur);
2545
+ el.addEventListener("touchstart", onTouchStart, { passive: false });
2546
+ el.addEventListener("touchmove", onTouchMove, { passive: false });
2547
+ el.addEventListener("touchend", onTouchEnd, { passive: false });
2548
+ el.addEventListener("touchcancel", onTouchCancel, { passive: false });
2549
+ el.addEventListener("gesturestart", onGesturePrevent, { passive: false });
2550
+ el.addEventListener("gesturechange", onGesturePrevent, { passive: false });
2551
+ el.addEventListener("gestureend", onGesturePrevent, { passive: false });
2388
2552
  raf = requestAnimationFrame(tick);
2389
2553
  }
2390
2554
  function tick() {
@@ -2426,7 +2590,7 @@ function createEngine({
2426
2590
  }
2427
2591
  let panX = 0;
2428
2592
  let panY = 0;
2429
- if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
2593
+ if (!state.isDragging && isMouseInWindow && !currentConfig?.editable && !isTouchDevice) {
2430
2594
  const t = ENGINE_CONFIG.edgePanThreshold;
2431
2595
  const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
2432
2596
  const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
@@ -2479,8 +2643,9 @@ function createEngine({
2479
2643
  } else if (!state.isDragging && !flyToActive) {
2480
2644
  state.lon += state.velocityX;
2481
2645
  state.lat += state.velocityY;
2482
- state.velocityX *= ENGINE_CONFIG.inertiaDamping;
2483
- state.velocityY *= ENGINE_CONFIG.inertiaDamping;
2646
+ const damping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
2647
+ state.velocityX *= damping;
2648
+ state.velocityY *= damping;
2484
2649
  if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
2485
2650
  if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
2486
2651
  }
@@ -2652,6 +2817,13 @@ function createEngine({
2652
2817
  el.removeEventListener("wheel", onWheel);
2653
2818
  el.removeEventListener("mouseleave", onWindowBlur);
2654
2819
  window.removeEventListener("blur", onWindowBlur);
2820
+ el.removeEventListener("touchstart", onTouchStart);
2821
+ el.removeEventListener("touchmove", onTouchMove);
2822
+ el.removeEventListener("touchend", onTouchEnd);
2823
+ el.removeEventListener("touchcancel", onTouchCancel);
2824
+ el.removeEventListener("gesturestart", onGesturePrevent);
2825
+ el.removeEventListener("gesturechange", onGesturePrevent);
2826
+ el.removeEventListener("gestureend", onGesturePrevent);
2655
2827
  }
2656
2828
  function dispose() {
2657
2829
  stop();
@@ -2706,7 +2878,7 @@ var init_createEngine = __esm({
2706
2878
  init_projections();
2707
2879
  init_fader();
2708
2880
  ENGINE_CONFIG = {
2709
- minFov: 10,
2881
+ minFov: 1,
2710
2882
  maxFov: 135,
2711
2883
  defaultFov: 50,
2712
2884
  dragSpeed: 125e-5,
@@ -2718,7 +2890,14 @@ var init_createEngine = __esm({
2718
2890
  horizonLockStrength: 0.05,
2719
2891
  edgePanThreshold: 0.15,
2720
2892
  edgePanMaxSpeed: 0.02,
2721
- edgePanDelay: 250
2893
+ edgePanDelay: 250,
2894
+ // Touch-specific
2895
+ touchInertiaDamping: 0.85,
2896
+ // Snappier than mouse (0.92)
2897
+ tapMaxDuration: 300,
2898
+ // ms
2899
+ tapMaxDistance: 10
2900
+ // px
2722
2901
  };
2723
2902
  ORDER_REVEAL_CONFIG = {
2724
2903
  globalDim: 0.85,