@project-skymap/library 0.2.1 → 0.4.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
@@ -446,7 +446,8 @@ function createEngine({
446
446
  draggedNodeId: null,
447
447
  draggedStarIndex: -1,
448
448
  draggedDist: 2e3,
449
- draggedGroup: null
449
+ draggedGroup: null,
450
+ tempArrangement: {}
450
451
  };
451
452
  const mouseNDC = new THREE4__namespace.Vector2();
452
453
  let isMouseInWindow = false;
@@ -693,7 +694,55 @@ function createEngine({
693
694
  const nodeById = /* @__PURE__ */ new Map();
694
695
  const starIndexToId = [];
695
696
  const dynamicLabels = [];
697
+ const hoverLabelMat = createSmartMaterial({
698
+ uniforms: {
699
+ uMap: { value: null },
700
+ uSize: { value: new THREE4__namespace.Vector2(1, 1) },
701
+ uAlpha: { value: 0 },
702
+ uAngle: { value: 0 }
703
+ },
704
+ vertexShaderBody: `
705
+ uniform vec2 uSize;
706
+ uniform float uAngle;
707
+ varying vec2 vUv;
708
+ void main() {
709
+ vUv = uv;
710
+ vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
711
+ vec4 projected = smartProject(mvPos);
712
+
713
+ float c = cos(uAngle);
714
+ float s = sin(uAngle);
715
+ mat2 rot = mat2(c, -s, s, c);
716
+ vec2 offset = rot * (position.xy * uSize);
717
+
718
+ projected.xy += offset / vec2(uAspect, 1.0);
719
+ gl_Position = projected;
720
+ }
721
+ `,
722
+ fragmentShader: `
723
+ uniform sampler2D uMap;
724
+ uniform float uAlpha;
725
+ varying vec2 vUv;
726
+ void main() {
727
+ float mask = getMaskAlpha();
728
+ if (mask < 0.01) discard;
729
+ vec4 tex = texture2D(uMap, vUv);
730
+ gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
731
+ }
732
+ `,
733
+ transparent: true,
734
+ depthWrite: false,
735
+ depthTest: false
736
+ // Always on top of stars
737
+ });
738
+ const hoverLabelMesh = new THREE4__namespace.Mesh(new THREE4__namespace.PlaneGeometry(1, 1), hoverLabelMat);
739
+ hoverLabelMesh.visible = false;
740
+ hoverLabelMesh.renderOrder = 999;
741
+ hoverLabelMesh.frustumCulled = false;
742
+ root.add(hoverLabelMesh);
743
+ let currentHoverNodeId = null;
696
744
  let constellationLines = null;
745
+ let boundaryLines = null;
697
746
  let starPoints = null;
698
747
  function clearRoot() {
699
748
  for (const child of [...root.children]) {
@@ -709,6 +758,7 @@ function createEngine({
709
758
  starIndexToId.length = 0;
710
759
  dynamicLabels.length = 0;
711
760
  constellationLines = null;
761
+ boundaryLines = null;
712
762
  starPoints = null;
713
763
  }
714
764
  function createTextTexture(text, color = "#ffffff") {
@@ -734,7 +784,22 @@ function createEngine({
734
784
  function getPosition(n) {
735
785
  if (currentConfig?.arrangement) {
736
786
  const arr = currentConfig.arrangement[n.id];
737
- if (arr) return new THREE4__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
787
+ if (arr) {
788
+ if (arr.position[2] === 0) {
789
+ const x = arr.position[0];
790
+ const y = arr.position[1];
791
+ const radius = currentConfig.layout?.radius ?? 2e3;
792
+ const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
793
+ const phi = Math.atan2(y, x);
794
+ const theta = r_norm * (Math.PI / 2);
795
+ return new THREE4__namespace.Vector3(
796
+ Math.sin(theta) * Math.cos(phi),
797
+ Math.cos(theta),
798
+ Math.sin(theta) * Math.sin(phi)
799
+ ).multiplyScalar(radius);
800
+ }
801
+ return new THREE4__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
802
+ }
738
803
  }
739
804
  return new THREE4__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
740
805
  }
@@ -750,6 +815,30 @@ function createEngine({
750
815
  scene.background = cfg.background && cfg.background !== "transparent" ? new THREE4__namespace.Color(cfg.background) : new THREE4__namespace.Color(0);
751
816
  const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
752
817
  const laidOut = computeLayoutPositions(model, layoutCfg);
818
+ const divisionPositions = /* @__PURE__ */ new Map();
819
+ if (cfg.arrangement) {
820
+ const divMap = /* @__PURE__ */ new Map();
821
+ for (const n of laidOut.nodes) {
822
+ if (n.level === 2 && n.parent) {
823
+ const list = divMap.get(n.parent) ?? [];
824
+ list.push(n);
825
+ divMap.set(n.parent, list);
826
+ }
827
+ }
828
+ for (const [divId, books] of divMap.entries()) {
829
+ const centroid = new THREE4__namespace.Vector3();
830
+ let count = 0;
831
+ for (const b of books) {
832
+ const p = getPosition(b);
833
+ centroid.add(p);
834
+ count++;
835
+ }
836
+ if (count > 0) {
837
+ centroid.divideScalar(count);
838
+ divisionPositions.set(divId, centroid);
839
+ }
840
+ }
841
+ }
753
842
  const starPositions = [];
754
843
  const starSizes = [];
755
844
  const starColors = [];
@@ -798,26 +887,36 @@ function createEngine({
798
887
  const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
799
888
  const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
800
889
  starColors.push(c.r, c.g, c.b);
801
- } else if (n.level === 2) {
802
- const color = "#ffffff";
890
+ }
891
+ if (n.level === 1 || n.level === 2 || n.level === 3) {
892
+ const color = n.level === 1 ? "#38bdf8" : "#ffffff";
803
893
  const texRes = createTextTexture(n.label, color);
804
894
  if (texRes) {
805
- const baseScale = 0.05;
895
+ let baseScale = 0.05;
896
+ if (n.level === 1) baseScale = 0.08;
897
+ else if (n.level === 3) baseScale = 0.04;
806
898
  const size = new THREE4__namespace.Vector2(baseScale * texRes.aspect, baseScale);
807
899
  const mat = createSmartMaterial({
808
900
  uniforms: {
809
901
  uMap: { value: texRes.tex },
810
902
  uSize: { value: size },
811
- uAlpha: { value: 0 }
903
+ uAlpha: { value: 0 },
904
+ uAngle: { value: 0 }
812
905
  },
813
906
  vertexShaderBody: `
814
907
  uniform vec2 uSize;
908
+ uniform float uAngle;
815
909
  varying vec2 vUv;
816
910
  void main() {
817
911
  vUv = uv;
818
912
  vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
819
913
  vec4 projected = smartProject(mvPos);
820
- vec2 offset = position.xy * uSize;
914
+
915
+ float c = cos(uAngle);
916
+ float s = sin(uAngle);
917
+ mat2 rot = mat2(c, -s, s, c);
918
+ vec2 offset = rot * (position.xy * uSize);
919
+
821
920
  projected.xy += offset / vec2(uAspect, 1.0);
822
921
  gl_Position = projected;
823
922
  }
@@ -838,7 +937,17 @@ function createEngine({
838
937
  depthTest: true
839
938
  });
840
939
  const mesh = new THREE4__namespace.Mesh(new THREE4__namespace.PlaneGeometry(1, 1), mat);
841
- const p = getPosition(n);
940
+ let p = getPosition(n);
941
+ if (n.level === 1) {
942
+ if (divisionPositions.has(n.id)) {
943
+ p.copy(divisionPositions.get(n.id));
944
+ }
945
+ const r = layoutCfg.radius * 0.95;
946
+ const angle = Math.atan2(p.z, p.x);
947
+ p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
948
+ } else if (n.level === 3) {
949
+ p.multiplyScalar(1.002);
950
+ }
842
951
  mesh.position.set(p.x, p.y, p.z);
843
952
  mesh.scale.set(size.x, size.y, 1);
844
953
  mesh.frustumCulled = false;
@@ -917,9 +1026,9 @@ function createEngine({
917
1026
  const lineGeo = new THREE4__namespace.BufferGeometry();
918
1027
  lineGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(linePoints, 3));
919
1028
  const lineMat = createSmartMaterial({
920
- uniforms: { color: { value: new THREE4__namespace.Color(4478310) } },
1029
+ uniforms: { color: { value: new THREE4__namespace.Color(11193599) } },
921
1030
  vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
922
- fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1031
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
923
1032
  transparent: true,
924
1033
  depthWrite: false,
925
1034
  blending: THREE4__namespace.AdditiveBlending
@@ -952,17 +1061,73 @@ function createEngine({
952
1061
  }
953
1062
  });
954
1063
  boundaryGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(bPoints, 3));
955
- const boundaryLines = new THREE4__namespace.LineSegments(boundaryGeo, boundaryMat);
1064
+ boundaryLines = new THREE4__namespace.LineSegments(boundaryGeo, boundaryMat);
956
1065
  boundaryLines.frustumCulled = false;
957
1066
  root.add(boundaryLines);
958
1067
  }
1068
+ if (cfg.polygons) {
1069
+ const polyPoints = [];
1070
+ const rBase = layoutCfg.radius;
1071
+ for (const pts of Object.values(cfg.polygons)) {
1072
+ if (pts.length < 2) continue;
1073
+ for (let i = 0; i < pts.length; i++) {
1074
+ const p1_2d = pts[i];
1075
+ const p2_2d = pts[(i + 1) % pts.length];
1076
+ if (!p1_2d || !p2_2d) continue;
1077
+ const project2dTo3d = (p) => {
1078
+ const x = p[0];
1079
+ const y = p[1];
1080
+ const r_norm = Math.sqrt(x * x + y * y);
1081
+ const phi = Math.atan2(y, x);
1082
+ const theta = r_norm * (Math.PI / 2);
1083
+ return new THREE4__namespace.Vector3(
1084
+ Math.sin(theta) * Math.cos(phi),
1085
+ Math.cos(theta),
1086
+ Math.sin(theta) * Math.sin(phi)
1087
+ ).multiplyScalar(rBase);
1088
+ };
1089
+ const v1 = project2dTo3d(p1_2d);
1090
+ const v2 = project2dTo3d(p2_2d);
1091
+ polyPoints.push(v1.x, v1.y, v1.z);
1092
+ polyPoints.push(v2.x, v2.y, v2.z);
1093
+ }
1094
+ }
1095
+ if (polyPoints.length > 0) {
1096
+ const polyGeo = new THREE4__namespace.BufferGeometry();
1097
+ polyGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(polyPoints, 3));
1098
+ const polyMat = createSmartMaterial({
1099
+ uniforms: { color: { value: new THREE4__namespace.Color(3718648) } },
1100
+ // Cyan-ish
1101
+ vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
1102
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1103
+ transparent: true,
1104
+ depthWrite: false,
1105
+ blending: THREE4__namespace.AdditiveBlending
1106
+ });
1107
+ const polyLines = new THREE4__namespace.LineSegments(polyGeo, polyMat);
1108
+ polyLines.frustumCulled = false;
1109
+ root.add(polyLines);
1110
+ }
1111
+ }
959
1112
  resize();
960
1113
  }
961
1114
  let lastData = void 0;
962
1115
  let lastAdapter = void 0;
963
1116
  let lastModel = void 0;
1117
+ let lastAppliedLon = void 0;
1118
+ let lastAppliedLat = void 0;
964
1119
  function setConfig(cfg) {
965
1120
  currentConfig = cfg;
1121
+ if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
1122
+ state.lon = cfg.camera.lon;
1123
+ state.targetLon = cfg.camera.lon;
1124
+ lastAppliedLon = cfg.camera.lon;
1125
+ }
1126
+ if (typeof cfg.camera?.lat === "number" && cfg.camera.lat !== lastAppliedLat) {
1127
+ state.lat = cfg.camera.lat;
1128
+ state.targetLat = cfg.camera.lat;
1129
+ lastAppliedLat = cfg.camera.lat;
1130
+ }
966
1131
  let shouldRebuild = false;
967
1132
  let model = cfg.model;
968
1133
  if (!model && cfg.data && cfg.adapter) {
@@ -993,22 +1158,21 @@ function createEngine({
993
1158
  function getFullArrangement() {
994
1159
  const arr = {};
995
1160
  if (starPoints && starPoints.geometry.attributes.position) {
996
- const positions = starPoints.geometry.attributes.position.array;
1161
+ const attr = starPoints.geometry.attributes.position;
997
1162
  for (let i = 0; i < starIndexToId.length; i++) {
998
1163
  const id = starIndexToId[i];
999
1164
  if (id) {
1000
- const x = positions[i * 3] ?? 0;
1001
- const y = positions[i * 3 + 1] ?? 0;
1002
- const z = positions[i * 3 + 2] ?? 0;
1003
- if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
1004
- arr[id] = { position: [x, y, z] };
1005
- }
1165
+ const x = attr.getX(i);
1166
+ const y = attr.getY(i);
1167
+ const z = attr.getZ(i);
1168
+ arr[id] = { position: [x, y, z] };
1006
1169
  }
1007
1170
  }
1008
1171
  }
1009
1172
  for (const item of dynamicLabels) {
1010
1173
  arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
1011
1174
  }
1175
+ Object.assign(arr, state.tempArrangement);
1012
1176
  return arr;
1013
1177
  }
1014
1178
  function pick(ev) {
@@ -1094,6 +1258,7 @@ function createEngine({
1094
1258
  state.isDragging = true;
1095
1259
  state.velocityX = 0;
1096
1260
  state.velocityY = 0;
1261
+ state.tempArrangement = {};
1097
1262
  document.body.style.cursor = "grabbing";
1098
1263
  }
1099
1264
  function onMouseMove(e) {
@@ -1114,7 +1279,10 @@ function createEngine({
1114
1279
  } else if (state.draggedGroup && state.draggedNodeId) {
1115
1280
  const group = state.draggedGroup;
1116
1281
  const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
1117
- if (item) item.obj.position.copy(newPos);
1282
+ if (item) {
1283
+ item.obj.position.copy(newPos);
1284
+ state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
1285
+ }
1118
1286
  const vStart = group.labelInitialPos.clone().normalize();
1119
1287
  const vEnd = newPos.clone().normalize();
1120
1288
  const q = new THREE4__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
@@ -1124,6 +1292,10 @@ function createEngine({
1124
1292
  for (const child of group.children) {
1125
1293
  tempVec.copy(child.initialPos).applyQuaternion(q);
1126
1294
  attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
1295
+ const id = starIndexToId[child.index];
1296
+ if (id) {
1297
+ state.tempArrangement[id] = { position: [tempVec.x, tempVec.y, tempVec.z] };
1298
+ }
1127
1299
  }
1128
1300
  attr.needsUpdate = true;
1129
1301
  }
@@ -1143,6 +1315,26 @@ function createEngine({
1143
1315
  state.lat = state.targetLat;
1144
1316
  } else {
1145
1317
  const hit = pick(e);
1318
+ if (hit && hit.type === "star") {
1319
+ if (currentHoverNodeId !== hit.node.id) {
1320
+ currentHoverNodeId = hit.node.id;
1321
+ const res = createTextTexture(hit.node.label, "#ffd700");
1322
+ if (res) {
1323
+ hoverLabelMat.uniforms.uMap.value = res.tex;
1324
+ const baseScale = 0.03;
1325
+ const size = new THREE4__namespace.Vector2(baseScale * res.aspect, baseScale);
1326
+ hoverLabelMat.uniforms.uSize.value = size;
1327
+ hoverLabelMesh.scale.set(size.x, size.y, 1);
1328
+ }
1329
+ }
1330
+ hoverLabelMesh.position.copy(hit.point);
1331
+ hoverLabelMat.uniforms.uAlpha.value = 1;
1332
+ hoverLabelMesh.visible = true;
1333
+ } else {
1334
+ currentHoverNodeId = null;
1335
+ hoverLabelMat.uniforms.uAlpha.value = 0;
1336
+ hoverLabelMesh.visible = false;
1337
+ }
1146
1338
  if (hit?.node.id !== handlers._lastHoverId) {
1147
1339
  handlers._lastHoverId = hit?.node.id;
1148
1340
  handlers.onHover?.(hit?.node);
@@ -1284,30 +1476,107 @@ function createEngine({
1284
1476
  camera.up.normalize();
1285
1477
  camera.lookAt(target);
1286
1478
  updateUniforms();
1287
- const cameraDir = new THREE4__namespace.Vector3();
1288
- camera.getWorldDirection(cameraDir);
1289
- const objPos = new THREE4__namespace.Vector3();
1290
- const objDir = new THREE4__namespace.Vector3();
1291
- const SHOW_LABELS_FOV = 60;
1479
+ const DIVISION_THRESHOLD = 60;
1480
+ const showDivisions = state.fov > DIVISION_THRESHOLD;
1481
+ if (constellationLines) {
1482
+ constellationLines.visible = currentConfig?.showConstellationLines ?? false;
1483
+ }
1484
+ if (boundaryLines) {
1485
+ boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
1486
+ }
1487
+ const rect = renderer.domElement.getBoundingClientRect();
1488
+ const screenW = rect.width;
1489
+ const screenH = rect.height;
1490
+ const aspect = screenW / screenH;
1491
+ const labelsToCheck = [];
1492
+ const occupied = [];
1493
+ function isOverlapping(x2, y2, w, h) {
1494
+ for (const r2 of occupied) {
1495
+ if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
1496
+ }
1497
+ return false;
1498
+ }
1499
+ const showBookLabels = currentConfig?.showBookLabels === true;
1500
+ const showDivisionLabels = currentConfig?.showDivisionLabels === true;
1501
+ const showChapterLabels = currentConfig?.showChapterLabels === true;
1502
+ const showChapters = state.fov < 35;
1292
1503
  for (const item of dynamicLabels) {
1293
1504
  const uniforms = item.obj.material.uniforms;
1294
- let targetAlpha = 0;
1295
- if (state.fov < SHOW_LABELS_FOV) {
1296
- item.obj.getWorldPosition(objPos);
1297
- objDir.subVectors(objPos, camera.position).normalize();
1298
- const dot = cameraDir.dot(objDir);
1299
- const fullVisibleDot = 0.98;
1300
- const invisibleDot = 0.9;
1301
- let gazeOpacity = 0;
1302
- if (dot >= fullVisibleDot) gazeOpacity = 1;
1303
- else if (dot > invisibleDot) gazeOpacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
1304
- const zoomFactor = 1 - THREE4__namespace.MathUtils.smoothstep(40, SHOW_LABELS_FOV, state.fov);
1305
- targetAlpha = gazeOpacity * zoomFactor;
1306
- }
1307
- if (uniforms.uAlpha) {
1308
- uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, targetAlpha, 0.1);
1505
+ const level = item.node.level;
1506
+ let isEnabled = false;
1507
+ if (level === 2 && showBookLabels) isEnabled = true;
1508
+ else if (level === 1 && showDivisionLabels) isEnabled = true;
1509
+ else if (level === 3 && showChapterLabels) isEnabled = true;
1510
+ if (!isEnabled) {
1511
+ uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1309
1512
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1513
+ continue;
1310
1514
  }
1515
+ const pWorld = item.obj.position;
1516
+ const pProj = smartProjectJS(pWorld);
1517
+ if (pProj.z > 0.2) {
1518
+ uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1519
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
1520
+ continue;
1521
+ }
1522
+ if (level === 3 && !showChapters && item.node.id !== state.draggedNodeId) {
1523
+ uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1524
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
1525
+ continue;
1526
+ }
1527
+ const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
1528
+ const ndcY = pProj.y * globalUniforms.uScale.value;
1529
+ const sX = (ndcX * 0.5 + 0.5) * screenW;
1530
+ const sY = (-ndcY * 0.5 + 0.5) * screenH;
1531
+ const size = uniforms.uSize.value;
1532
+ const pixelH = size.y * screenH * 0.8;
1533
+ const pixelW = size.x * screenH * 0.8;
1534
+ labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
1535
+ }
1536
+ const hoverId = handlers._lastHoverId;
1537
+ const selectedId = state.draggedNodeId;
1538
+ labelsToCheck.sort((a, b) => {
1539
+ const getScore = (l) => {
1540
+ if (l.item.node.id === selectedId) return 10;
1541
+ if (l.item.node.id === hoverId) return 9;
1542
+ const level = l.level;
1543
+ if (level === 2) return 5;
1544
+ if (level === 1) return showDivisions ? 6 : 1;
1545
+ return 0;
1546
+ };
1547
+ return getScore(b) - getScore(a);
1548
+ });
1549
+ for (const l of labelsToCheck) {
1550
+ let target2 = 0;
1551
+ const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
1552
+ if (l.level === 1) {
1553
+ let rot = 0;
1554
+ const blend = globalUniforms.uBlend.value;
1555
+ if (blend > 0.5) {
1556
+ const dx = l.sX - screenW / 2;
1557
+ const dy = l.sY - screenH / 2;
1558
+ rot = Math.atan2(-dy, -dx) - Math.PI / 2;
1559
+ }
1560
+ l.uniforms.uAngle.value = THREE4__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
1561
+ }
1562
+ if (l.level === 2) {
1563
+ target2 = 1;
1564
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1565
+ } else if (l.level === 1) {
1566
+ if (showDivisions || isSpecial) {
1567
+ const pad = -5;
1568
+ if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
1569
+ target2 = 1;
1570
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1571
+ }
1572
+ }
1573
+ } else if (l.level === 3) {
1574
+ if (showChapters || isSpecial) {
1575
+ target2 = 1;
1576
+ }
1577
+ }
1578
+ l.uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
1579
+ l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
1311
1580
  }
1312
1581
  renderer.render(scene, camera);
1313
1582
  }
@@ -1392,6 +1661,7 @@ var StarMap = react.forwardRef(
1392
1661
  function bibleToSceneModel(data) {
1393
1662
  const nodes = [];
1394
1663
  const links = [];
1664
+ let bookCounter = 0;
1395
1665
  const id = {
1396
1666
  testament: (t) => `T:${t}`,
1397
1667
  division: (t, d) => `D:${t}:${d}`,
@@ -1412,10 +1682,12 @@ function bibleToSceneModel(data) {
1412
1682
  });
1413
1683
  links.push({ source: did, target: tid });
1414
1684
  for (const b of d.books) {
1685
+ bookCounter++;
1686
+ const bookLabel = `${bookCounter}. ${b.name}`;
1415
1687
  const bid = id.book(b.key);
1416
1688
  nodes.push({
1417
1689
  id: bid,
1418
- label: b.name,
1690
+ label: bookLabel,
1419
1691
  level: 2,
1420
1692
  parent: did,
1421
1693
  meta: { testament: t.name, division: d.name, bookKey: b.key, book: b.name }
@@ -1427,7 +1699,7 @@ function bibleToSceneModel(data) {
1427
1699
  const cid = id.chapter(b.key, chapterNum);
1428
1700
  nodes.push({
1429
1701
  id: cid,
1430
- label: `${b.name} ${chapterNum}`,
1702
+ label: `${bookLabel} ${chapterNum}`,
1431
1703
  level: 3,
1432
1704
  parent: bid,
1433
1705
  weight: verseCounts[i],
@@ -30490,9 +30762,149 @@ var default_stars_default = {
30490
30762
  }
30491
30763
  ]
30492
30764
  };
30765
+ var defaultGenerateOptions = {
30766
+ seed: 12345,
30767
+ discRadius: 2e3,
30768
+ milkyWayEnabled: true,
30769
+ milkyWayAngle: 60,
30770
+ milkyWayWidth: 0.3,
30771
+ // Width in dot-product space
30772
+ milkyWayStrength: 0.7,
30773
+ noiseScale: 2,
30774
+ noiseStrength: 0.4,
30775
+ clusterSpread: 0.08
30776
+ // Radians approx
30777
+ };
30778
+ var RNG = class {
30779
+ seed;
30780
+ constructor(seed) {
30781
+ this.seed = seed;
30782
+ }
30783
+ // Returns 0..1
30784
+ next() {
30785
+ this.seed = (this.seed * 9301 + 49297) % 233280;
30786
+ return this.seed / 233280;
30787
+ }
30788
+ // Returns range [min, max)
30789
+ range(min, max) {
30790
+ return min + this.next() * (max - min);
30791
+ }
30792
+ // Uniform random on upper hemisphere (y > 0)
30793
+ randomOnSphere() {
30794
+ const y = this.next();
30795
+ const theta = 2 * Math.PI * this.next();
30796
+ const r = Math.sqrt(1 - y * y);
30797
+ const x = r * Math.cos(theta);
30798
+ const z = r * Math.sin(theta);
30799
+ return new THREE4__namespace.Vector3(x, y, z);
30800
+ }
30801
+ };
30802
+ function simpleNoise3D(v, scale) {
30803
+ const s = scale;
30804
+ return (Math.sin(v.x * s) + Math.sin(v.y * s * 1.3) + Math.sin(v.z * s * 1.7) + Math.sin(v.x * s * 2.1 + v.y * s * 2.1) * 0.5) / 3.5;
30805
+ }
30806
+ function getDensity(v, opts, mwNormal) {
30807
+ let density = 0.3;
30808
+ if (opts.milkyWayEnabled) {
30809
+ const dot = v.dot(mwNormal);
30810
+ const dist = Math.abs(dot);
30811
+ const band = Math.exp(-(dist * dist) / (opts.milkyWayWidth * opts.milkyWayWidth));
30812
+ density += band * opts.milkyWayStrength;
30813
+ }
30814
+ const noise = simpleNoise3D(v, opts.noiseScale);
30815
+ density *= 1 + noise * opts.noiseStrength;
30816
+ return Math.max(0.01, density);
30817
+ }
30818
+ function generateArrangement(bible, options = {}) {
30819
+ const opts = { ...defaultGenerateOptions, ...options };
30820
+ const rng = new RNG(opts.seed);
30821
+ const arrangement = {};
30822
+ const books = [];
30823
+ bible.testaments.forEach((t) => {
30824
+ t.divisions.forEach((d) => {
30825
+ d.books.forEach((b) => {
30826
+ books.push({
30827
+ key: b.key,
30828
+ name: b.name,
30829
+ chapters: b.chapters,
30830
+ division: d.name,
30831
+ testament: t.name
30832
+ });
30833
+ });
30834
+ });
30835
+ });
30836
+ const bookCount = books.length;
30837
+ const mwRad = THREE4__namespace.MathUtils.degToRad(opts.milkyWayAngle);
30838
+ const mwNormal = new THREE4__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
30839
+ const anchors = [];
30840
+ for (let i = 0; i < bookCount; i++) {
30841
+ let bestP = new THREE4__namespace.Vector3();
30842
+ let valid = false;
30843
+ let attempt = 0;
30844
+ while (!valid && attempt < 100) {
30845
+ const p = rng.randomOnSphere();
30846
+ const d = getDensity(p, opts, mwNormal);
30847
+ if (rng.next() < d) {
30848
+ bestP = p;
30849
+ valid = true;
30850
+ }
30851
+ attempt++;
30852
+ }
30853
+ if (!valid) bestP = rng.randomOnSphere();
30854
+ anchors.push(bestP);
30855
+ }
30856
+ anchors.sort((a, b) => {
30857
+ const lonA = Math.atan2(a.z, a.x);
30858
+ const lonB = Math.atan2(b.z, b.x);
30859
+ return lonA - lonB;
30860
+ });
30861
+ books.forEach((book, i) => {
30862
+ const anchor = anchors[i];
30863
+ const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30864
+ arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
30865
+ for (let c = 0; c < book.chapters; c++) {
30866
+ const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
30867
+ const offset = new THREE4__namespace.Vector3(
30868
+ (rng.next() - 0.5) * 2,
30869
+ (rng.next() - 0.5) * 2,
30870
+ (rng.next() - 0.5) * 2
30871
+ ).normalize().multiplyScalar(rng.next() * localSpread);
30872
+ const starDir = anchor.clone().add(offset).normalize();
30873
+ if (starDir.y < 0.01) {
30874
+ starDir.y = 0.01;
30875
+ starDir.normalize();
30876
+ }
30877
+ const starPos = starDir.multiplyScalar(opts.discRadius);
30878
+ const chapId = `C:${book.key}:${c + 1}`;
30879
+ arrangement[chapId] = { position: [starPos.x, starPos.y, starPos.z] };
30880
+ }
30881
+ });
30882
+ const divisions = /* @__PURE__ */ new Map();
30883
+ books.forEach((book, i) => {
30884
+ const anchor = anchors[i];
30885
+ const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30886
+ const divId = `D:${book.testament}:${book.division}`;
30887
+ if (!divisions.has(divId)) {
30888
+ divisions.set(divId, { sum: new THREE4__namespace.Vector3(), count: 0 });
30889
+ }
30890
+ const entry = divisions.get(divId);
30891
+ entry.sum.add(anchorPos);
30892
+ entry.count++;
30893
+ });
30894
+ divisions.forEach((val, key) => {
30895
+ if (val.count > 0) {
30896
+ val.sum.divideScalar(val.count);
30897
+ val.sum.normalize().multiplyScalar(opts.discRadius * 0.9);
30898
+ arrangement[key] = { position: [val.sum.x, val.sum.y, val.sum.z] };
30899
+ }
30900
+ });
30901
+ return arrangement;
30902
+ }
30493
30903
 
30494
30904
  exports.StarMap = StarMap;
30495
30905
  exports.bibleToSceneModel = bibleToSceneModel;
30906
+ exports.defaultGenerateOptions = defaultGenerateOptions;
30496
30907
  exports.defaultStars = default_stars_default;
30908
+ exports.generateArrangement = generateArrangement;
30497
30909
  //# sourceMappingURL=index.cjs.map
30498
30910
  //# sourceMappingURL=index.cjs.map