@project-skymap/library 0.3.0 → 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.js CHANGED
@@ -424,7 +424,8 @@ function createEngine({
424
424
  draggedNodeId: null,
425
425
  draggedStarIndex: -1,
426
426
  draggedDist: 2e3,
427
- draggedGroup: null
427
+ draggedGroup: null,
428
+ tempArrangement: {}
428
429
  };
429
430
  const mouseNDC = new THREE4.Vector2();
430
431
  let isMouseInWindow = false;
@@ -671,7 +672,55 @@ function createEngine({
671
672
  const nodeById = /* @__PURE__ */ new Map();
672
673
  const starIndexToId = [];
673
674
  const dynamicLabels = [];
675
+ const hoverLabelMat = createSmartMaterial({
676
+ uniforms: {
677
+ uMap: { value: null },
678
+ uSize: { value: new THREE4.Vector2(1, 1) },
679
+ uAlpha: { value: 0 },
680
+ uAngle: { value: 0 }
681
+ },
682
+ vertexShaderBody: `
683
+ uniform vec2 uSize;
684
+ uniform float uAngle;
685
+ varying vec2 vUv;
686
+ void main() {
687
+ vUv = uv;
688
+ vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
689
+ vec4 projected = smartProject(mvPos);
690
+
691
+ float c = cos(uAngle);
692
+ float s = sin(uAngle);
693
+ mat2 rot = mat2(c, -s, s, c);
694
+ vec2 offset = rot * (position.xy * uSize);
695
+
696
+ projected.xy += offset / vec2(uAspect, 1.0);
697
+ gl_Position = projected;
698
+ }
699
+ `,
700
+ fragmentShader: `
701
+ uniform sampler2D uMap;
702
+ uniform float uAlpha;
703
+ varying vec2 vUv;
704
+ void main() {
705
+ float mask = getMaskAlpha();
706
+ if (mask < 0.01) discard;
707
+ vec4 tex = texture2D(uMap, vUv);
708
+ gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
709
+ }
710
+ `,
711
+ transparent: true,
712
+ depthWrite: false,
713
+ depthTest: false
714
+ // Always on top of stars
715
+ });
716
+ const hoverLabelMesh = new THREE4.Mesh(new THREE4.PlaneGeometry(1, 1), hoverLabelMat);
717
+ hoverLabelMesh.visible = false;
718
+ hoverLabelMesh.renderOrder = 999;
719
+ hoverLabelMesh.frustumCulled = false;
720
+ root.add(hoverLabelMesh);
721
+ let currentHoverNodeId = null;
674
722
  let constellationLines = null;
723
+ let boundaryLines = null;
675
724
  let starPoints = null;
676
725
  function clearRoot() {
677
726
  for (const child of [...root.children]) {
@@ -687,6 +736,7 @@ function createEngine({
687
736
  starIndexToId.length = 0;
688
737
  dynamicLabels.length = 0;
689
738
  constellationLines = null;
739
+ boundaryLines = null;
690
740
  starPoints = null;
691
741
  }
692
742
  function createTextTexture(text, color = "#ffffff") {
@@ -712,7 +762,22 @@ function createEngine({
712
762
  function getPosition(n) {
713
763
  if (currentConfig?.arrangement) {
714
764
  const arr = currentConfig.arrangement[n.id];
715
- if (arr) return new THREE4.Vector3(arr.position[0], arr.position[1], arr.position[2]);
765
+ if (arr) {
766
+ if (arr.position[2] === 0) {
767
+ const x = arr.position[0];
768
+ const y = arr.position[1];
769
+ const radius = currentConfig.layout?.radius ?? 2e3;
770
+ const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
771
+ const phi = Math.atan2(y, x);
772
+ const theta = r_norm * (Math.PI / 2);
773
+ return new THREE4.Vector3(
774
+ Math.sin(theta) * Math.cos(phi),
775
+ Math.cos(theta),
776
+ Math.sin(theta) * Math.sin(phi)
777
+ ).multiplyScalar(radius);
778
+ }
779
+ return new THREE4.Vector3(arr.position[0], arr.position[1], arr.position[2]);
780
+ }
716
781
  }
717
782
  return new THREE4.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
718
783
  }
@@ -728,6 +793,30 @@ function createEngine({
728
793
  scene.background = cfg.background && cfg.background !== "transparent" ? new THREE4.Color(cfg.background) : new THREE4.Color(0);
729
794
  const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
730
795
  const laidOut = computeLayoutPositions(model, layoutCfg);
796
+ const divisionPositions = /* @__PURE__ */ new Map();
797
+ if (cfg.arrangement) {
798
+ const divMap = /* @__PURE__ */ new Map();
799
+ for (const n of laidOut.nodes) {
800
+ if (n.level === 2 && n.parent) {
801
+ const list = divMap.get(n.parent) ?? [];
802
+ list.push(n);
803
+ divMap.set(n.parent, list);
804
+ }
805
+ }
806
+ for (const [divId, books] of divMap.entries()) {
807
+ const centroid = new THREE4.Vector3();
808
+ let count = 0;
809
+ for (const b of books) {
810
+ const p = getPosition(b);
811
+ centroid.add(p);
812
+ count++;
813
+ }
814
+ if (count > 0) {
815
+ centroid.divideScalar(count);
816
+ divisionPositions.set(divId, centroid);
817
+ }
818
+ }
819
+ }
731
820
  const starPositions = [];
732
821
  const starSizes = [];
733
822
  const starColors = [];
@@ -776,26 +865,36 @@ function createEngine({
776
865
  const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
777
866
  const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
778
867
  starColors.push(c.r, c.g, c.b);
779
- } else if (n.level === 2) {
780
- const color = "#ffffff";
868
+ }
869
+ if (n.level === 1 || n.level === 2 || n.level === 3) {
870
+ const color = n.level === 1 ? "#38bdf8" : "#ffffff";
781
871
  const texRes = createTextTexture(n.label, color);
782
872
  if (texRes) {
783
- const baseScale = 0.05;
873
+ let baseScale = 0.05;
874
+ if (n.level === 1) baseScale = 0.08;
875
+ else if (n.level === 3) baseScale = 0.04;
784
876
  const size = new THREE4.Vector2(baseScale * texRes.aspect, baseScale);
785
877
  const mat = createSmartMaterial({
786
878
  uniforms: {
787
879
  uMap: { value: texRes.tex },
788
880
  uSize: { value: size },
789
- uAlpha: { value: 0 }
881
+ uAlpha: { value: 0 },
882
+ uAngle: { value: 0 }
790
883
  },
791
884
  vertexShaderBody: `
792
885
  uniform vec2 uSize;
886
+ uniform float uAngle;
793
887
  varying vec2 vUv;
794
888
  void main() {
795
889
  vUv = uv;
796
890
  vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
797
891
  vec4 projected = smartProject(mvPos);
798
- vec2 offset = position.xy * uSize;
892
+
893
+ float c = cos(uAngle);
894
+ float s = sin(uAngle);
895
+ mat2 rot = mat2(c, -s, s, c);
896
+ vec2 offset = rot * (position.xy * uSize);
897
+
799
898
  projected.xy += offset / vec2(uAspect, 1.0);
800
899
  gl_Position = projected;
801
900
  }
@@ -816,7 +915,17 @@ function createEngine({
816
915
  depthTest: true
817
916
  });
818
917
  const mesh = new THREE4.Mesh(new THREE4.PlaneGeometry(1, 1), mat);
819
- const p = getPosition(n);
918
+ let p = getPosition(n);
919
+ if (n.level === 1) {
920
+ if (divisionPositions.has(n.id)) {
921
+ p.copy(divisionPositions.get(n.id));
922
+ }
923
+ const r = layoutCfg.radius * 0.95;
924
+ const angle = Math.atan2(p.z, p.x);
925
+ p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
926
+ } else if (n.level === 3) {
927
+ p.multiplyScalar(1.002);
928
+ }
820
929
  mesh.position.set(p.x, p.y, p.z);
821
930
  mesh.scale.set(size.x, size.y, 1);
822
931
  mesh.frustumCulled = false;
@@ -895,9 +1004,9 @@ function createEngine({
895
1004
  const lineGeo = new THREE4.BufferGeometry();
896
1005
  lineGeo.setAttribute("position", new THREE4.Float32BufferAttribute(linePoints, 3));
897
1006
  const lineMat = createSmartMaterial({
898
- uniforms: { color: { value: new THREE4.Color(4478310) } },
1007
+ uniforms: { color: { value: new THREE4.Color(11193599) } },
899
1008
  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; }`,
900
- fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1009
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
901
1010
  transparent: true,
902
1011
  depthWrite: false,
903
1012
  blending: THREE4.AdditiveBlending
@@ -930,17 +1039,73 @@ function createEngine({
930
1039
  }
931
1040
  });
932
1041
  boundaryGeo.setAttribute("position", new THREE4.Float32BufferAttribute(bPoints, 3));
933
- const boundaryLines = new THREE4.LineSegments(boundaryGeo, boundaryMat);
1042
+ boundaryLines = new THREE4.LineSegments(boundaryGeo, boundaryMat);
934
1043
  boundaryLines.frustumCulled = false;
935
1044
  root.add(boundaryLines);
936
1045
  }
1046
+ if (cfg.polygons) {
1047
+ const polyPoints = [];
1048
+ const rBase = layoutCfg.radius;
1049
+ for (const pts of Object.values(cfg.polygons)) {
1050
+ if (pts.length < 2) continue;
1051
+ for (let i = 0; i < pts.length; i++) {
1052
+ const p1_2d = pts[i];
1053
+ const p2_2d = pts[(i + 1) % pts.length];
1054
+ if (!p1_2d || !p2_2d) continue;
1055
+ const project2dTo3d = (p) => {
1056
+ const x = p[0];
1057
+ const y = p[1];
1058
+ const r_norm = Math.sqrt(x * x + y * y);
1059
+ const phi = Math.atan2(y, x);
1060
+ const theta = r_norm * (Math.PI / 2);
1061
+ return new THREE4.Vector3(
1062
+ Math.sin(theta) * Math.cos(phi),
1063
+ Math.cos(theta),
1064
+ Math.sin(theta) * Math.sin(phi)
1065
+ ).multiplyScalar(rBase);
1066
+ };
1067
+ const v1 = project2dTo3d(p1_2d);
1068
+ const v2 = project2dTo3d(p2_2d);
1069
+ polyPoints.push(v1.x, v1.y, v1.z);
1070
+ polyPoints.push(v2.x, v2.y, v2.z);
1071
+ }
1072
+ }
1073
+ if (polyPoints.length > 0) {
1074
+ const polyGeo = new THREE4.BufferGeometry();
1075
+ polyGeo.setAttribute("position", new THREE4.Float32BufferAttribute(polyPoints, 3));
1076
+ const polyMat = createSmartMaterial({
1077
+ uniforms: { color: { value: new THREE4.Color(3718648) } },
1078
+ // Cyan-ish
1079
+ 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; }`,
1080
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1081
+ transparent: true,
1082
+ depthWrite: false,
1083
+ blending: THREE4.AdditiveBlending
1084
+ });
1085
+ const polyLines = new THREE4.LineSegments(polyGeo, polyMat);
1086
+ polyLines.frustumCulled = false;
1087
+ root.add(polyLines);
1088
+ }
1089
+ }
937
1090
  resize();
938
1091
  }
939
1092
  let lastData = void 0;
940
1093
  let lastAdapter = void 0;
941
1094
  let lastModel = void 0;
1095
+ let lastAppliedLon = void 0;
1096
+ let lastAppliedLat = void 0;
942
1097
  function setConfig(cfg) {
943
1098
  currentConfig = cfg;
1099
+ if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
1100
+ state.lon = cfg.camera.lon;
1101
+ state.targetLon = cfg.camera.lon;
1102
+ lastAppliedLon = cfg.camera.lon;
1103
+ }
1104
+ if (typeof cfg.camera?.lat === "number" && cfg.camera.lat !== lastAppliedLat) {
1105
+ state.lat = cfg.camera.lat;
1106
+ state.targetLat = cfg.camera.lat;
1107
+ lastAppliedLat = cfg.camera.lat;
1108
+ }
944
1109
  let shouldRebuild = false;
945
1110
  let model = cfg.model;
946
1111
  if (!model && cfg.data && cfg.adapter) {
@@ -971,22 +1136,21 @@ function createEngine({
971
1136
  function getFullArrangement() {
972
1137
  const arr = {};
973
1138
  if (starPoints && starPoints.geometry.attributes.position) {
974
- const positions = starPoints.geometry.attributes.position.array;
1139
+ const attr = starPoints.geometry.attributes.position;
975
1140
  for (let i = 0; i < starIndexToId.length; i++) {
976
1141
  const id = starIndexToId[i];
977
1142
  if (id) {
978
- const x = positions[i * 3] ?? 0;
979
- const y = positions[i * 3 + 1] ?? 0;
980
- const z = positions[i * 3 + 2] ?? 0;
981
- if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
982
- arr[id] = { position: [x, y, z] };
983
- }
1143
+ const x = attr.getX(i);
1144
+ const y = attr.getY(i);
1145
+ const z = attr.getZ(i);
1146
+ arr[id] = { position: [x, y, z] };
984
1147
  }
985
1148
  }
986
1149
  }
987
1150
  for (const item of dynamicLabels) {
988
1151
  arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
989
1152
  }
1153
+ Object.assign(arr, state.tempArrangement);
990
1154
  return arr;
991
1155
  }
992
1156
  function pick(ev) {
@@ -1072,6 +1236,7 @@ function createEngine({
1072
1236
  state.isDragging = true;
1073
1237
  state.velocityX = 0;
1074
1238
  state.velocityY = 0;
1239
+ state.tempArrangement = {};
1075
1240
  document.body.style.cursor = "grabbing";
1076
1241
  }
1077
1242
  function onMouseMove(e) {
@@ -1092,7 +1257,10 @@ function createEngine({
1092
1257
  } else if (state.draggedGroup && state.draggedNodeId) {
1093
1258
  const group = state.draggedGroup;
1094
1259
  const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
1095
- if (item) item.obj.position.copy(newPos);
1260
+ if (item) {
1261
+ item.obj.position.copy(newPos);
1262
+ state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
1263
+ }
1096
1264
  const vStart = group.labelInitialPos.clone().normalize();
1097
1265
  const vEnd = newPos.clone().normalize();
1098
1266
  const q = new THREE4.Quaternion().setFromUnitVectors(vStart, vEnd);
@@ -1102,6 +1270,10 @@ function createEngine({
1102
1270
  for (const child of group.children) {
1103
1271
  tempVec.copy(child.initialPos).applyQuaternion(q);
1104
1272
  attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
1273
+ const id = starIndexToId[child.index];
1274
+ if (id) {
1275
+ state.tempArrangement[id] = { position: [tempVec.x, tempVec.y, tempVec.z] };
1276
+ }
1105
1277
  }
1106
1278
  attr.needsUpdate = true;
1107
1279
  }
@@ -1121,6 +1293,26 @@ function createEngine({
1121
1293
  state.lat = state.targetLat;
1122
1294
  } else {
1123
1295
  const hit = pick(e);
1296
+ if (hit && hit.type === "star") {
1297
+ if (currentHoverNodeId !== hit.node.id) {
1298
+ currentHoverNodeId = hit.node.id;
1299
+ const res = createTextTexture(hit.node.label, "#ffd700");
1300
+ if (res) {
1301
+ hoverLabelMat.uniforms.uMap.value = res.tex;
1302
+ const baseScale = 0.03;
1303
+ const size = new THREE4.Vector2(baseScale * res.aspect, baseScale);
1304
+ hoverLabelMat.uniforms.uSize.value = size;
1305
+ hoverLabelMesh.scale.set(size.x, size.y, 1);
1306
+ }
1307
+ }
1308
+ hoverLabelMesh.position.copy(hit.point);
1309
+ hoverLabelMat.uniforms.uAlpha.value = 1;
1310
+ hoverLabelMesh.visible = true;
1311
+ } else {
1312
+ currentHoverNodeId = null;
1313
+ hoverLabelMat.uniforms.uAlpha.value = 0;
1314
+ hoverLabelMesh.visible = false;
1315
+ }
1124
1316
  if (hit?.node.id !== handlers._lastHoverId) {
1125
1317
  handlers._lastHoverId = hit?.node.id;
1126
1318
  handlers.onHover?.(hit?.node);
@@ -1262,30 +1454,107 @@ function createEngine({
1262
1454
  camera.up.normalize();
1263
1455
  camera.lookAt(target);
1264
1456
  updateUniforms();
1265
- const cameraDir = new THREE4.Vector3();
1266
- camera.getWorldDirection(cameraDir);
1267
- const objPos = new THREE4.Vector3();
1268
- const objDir = new THREE4.Vector3();
1269
- const SHOW_LABELS_FOV = 60;
1457
+ const DIVISION_THRESHOLD = 60;
1458
+ const showDivisions = state.fov > DIVISION_THRESHOLD;
1459
+ if (constellationLines) {
1460
+ constellationLines.visible = currentConfig?.showConstellationLines ?? false;
1461
+ }
1462
+ if (boundaryLines) {
1463
+ boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
1464
+ }
1465
+ const rect = renderer.domElement.getBoundingClientRect();
1466
+ const screenW = rect.width;
1467
+ const screenH = rect.height;
1468
+ const aspect = screenW / screenH;
1469
+ const labelsToCheck = [];
1470
+ const occupied = [];
1471
+ function isOverlapping(x2, y2, w, h) {
1472
+ for (const r2 of occupied) {
1473
+ if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
1474
+ }
1475
+ return false;
1476
+ }
1477
+ const showBookLabels = currentConfig?.showBookLabels === true;
1478
+ const showDivisionLabels = currentConfig?.showDivisionLabels === true;
1479
+ const showChapterLabels = currentConfig?.showChapterLabels === true;
1480
+ const showChapters = state.fov < 35;
1270
1481
  for (const item of dynamicLabels) {
1271
1482
  const uniforms = item.obj.material.uniforms;
1272
- let targetAlpha = 0;
1273
- if (state.fov < SHOW_LABELS_FOV) {
1274
- item.obj.getWorldPosition(objPos);
1275
- objDir.subVectors(objPos, camera.position).normalize();
1276
- const dot = cameraDir.dot(objDir);
1277
- const fullVisibleDot = 0.98;
1278
- const invisibleDot = 0.9;
1279
- let gazeOpacity = 0;
1280
- if (dot >= fullVisibleDot) gazeOpacity = 1;
1281
- else if (dot > invisibleDot) gazeOpacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
1282
- const zoomFactor = 1 - THREE4.MathUtils.smoothstep(40, SHOW_LABELS_FOV, state.fov);
1283
- targetAlpha = gazeOpacity * zoomFactor;
1284
- }
1285
- if (uniforms.uAlpha) {
1286
- uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, targetAlpha, 0.1);
1483
+ const level = item.node.level;
1484
+ let isEnabled = false;
1485
+ if (level === 2 && showBookLabels) isEnabled = true;
1486
+ else if (level === 1 && showDivisionLabels) isEnabled = true;
1487
+ else if (level === 3 && showChapterLabels) isEnabled = true;
1488
+ if (!isEnabled) {
1489
+ uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1287
1490
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1491
+ continue;
1288
1492
  }
1493
+ const pWorld = item.obj.position;
1494
+ const pProj = smartProjectJS(pWorld);
1495
+ if (pProj.z > 0.2) {
1496
+ uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1497
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
1498
+ continue;
1499
+ }
1500
+ if (level === 3 && !showChapters && item.node.id !== state.draggedNodeId) {
1501
+ uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1502
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
1503
+ continue;
1504
+ }
1505
+ const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
1506
+ const ndcY = pProj.y * globalUniforms.uScale.value;
1507
+ const sX = (ndcX * 0.5 + 0.5) * screenW;
1508
+ const sY = (-ndcY * 0.5 + 0.5) * screenH;
1509
+ const size = uniforms.uSize.value;
1510
+ const pixelH = size.y * screenH * 0.8;
1511
+ const pixelW = size.x * screenH * 0.8;
1512
+ labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
1513
+ }
1514
+ const hoverId = handlers._lastHoverId;
1515
+ const selectedId = state.draggedNodeId;
1516
+ labelsToCheck.sort((a, b) => {
1517
+ const getScore = (l) => {
1518
+ if (l.item.node.id === selectedId) return 10;
1519
+ if (l.item.node.id === hoverId) return 9;
1520
+ const level = l.level;
1521
+ if (level === 2) return 5;
1522
+ if (level === 1) return showDivisions ? 6 : 1;
1523
+ return 0;
1524
+ };
1525
+ return getScore(b) - getScore(a);
1526
+ });
1527
+ for (const l of labelsToCheck) {
1528
+ let target2 = 0;
1529
+ const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
1530
+ if (l.level === 1) {
1531
+ let rot = 0;
1532
+ const blend = globalUniforms.uBlend.value;
1533
+ if (blend > 0.5) {
1534
+ const dx = l.sX - screenW / 2;
1535
+ const dy = l.sY - screenH / 2;
1536
+ rot = Math.atan2(-dy, -dx) - Math.PI / 2;
1537
+ }
1538
+ l.uniforms.uAngle.value = THREE4.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
1539
+ }
1540
+ if (l.level === 2) {
1541
+ target2 = 1;
1542
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1543
+ } else if (l.level === 1) {
1544
+ if (showDivisions || isSpecial) {
1545
+ const pad = -5;
1546
+ if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
1547
+ target2 = 1;
1548
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1549
+ }
1550
+ }
1551
+ } else if (l.level === 3) {
1552
+ if (showChapters || isSpecial) {
1553
+ target2 = 1;
1554
+ }
1555
+ }
1556
+ l.uniforms.uAlpha.value = THREE4.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
1557
+ l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
1289
1558
  }
1290
1559
  renderer.render(scene, camera);
1291
1560
  }
@@ -1370,6 +1639,7 @@ var StarMap = forwardRef(
1370
1639
  function bibleToSceneModel(data) {
1371
1640
  const nodes = [];
1372
1641
  const links = [];
1642
+ let bookCounter = 0;
1373
1643
  const id = {
1374
1644
  testament: (t) => `T:${t}`,
1375
1645
  division: (t, d) => `D:${t}:${d}`,
@@ -1390,10 +1660,12 @@ function bibleToSceneModel(data) {
1390
1660
  });
1391
1661
  links.push({ source: did, target: tid });
1392
1662
  for (const b of d.books) {
1663
+ bookCounter++;
1664
+ const bookLabel = `${bookCounter}. ${b.name}`;
1393
1665
  const bid = id.book(b.key);
1394
1666
  nodes.push({
1395
1667
  id: bid,
1396
- label: b.name,
1668
+ label: bookLabel,
1397
1669
  level: 2,
1398
1670
  parent: did,
1399
1671
  meta: { testament: t.name, division: d.name, bookKey: b.key, book: b.name }
@@ -1405,7 +1677,7 @@ function bibleToSceneModel(data) {
1405
1677
  const cid = id.chapter(b.key, chapterNum);
1406
1678
  nodes.push({
1407
1679
  id: cid,
1408
- label: `${b.name} ${chapterNum}`,
1680
+ label: `${bookLabel} ${chapterNum}`,
1409
1681
  level: 3,
1410
1682
  parent: bid,
1411
1683
  weight: verseCounts[i],
@@ -30468,7 +30740,145 @@ var default_stars_default = {
30468
30740
  }
30469
30741
  ]
30470
30742
  };
30743
+ var defaultGenerateOptions = {
30744
+ seed: 12345,
30745
+ discRadius: 2e3,
30746
+ milkyWayEnabled: true,
30747
+ milkyWayAngle: 60,
30748
+ milkyWayWidth: 0.3,
30749
+ // Width in dot-product space
30750
+ milkyWayStrength: 0.7,
30751
+ noiseScale: 2,
30752
+ noiseStrength: 0.4,
30753
+ clusterSpread: 0.08
30754
+ // Radians approx
30755
+ };
30756
+ var RNG = class {
30757
+ seed;
30758
+ constructor(seed) {
30759
+ this.seed = seed;
30760
+ }
30761
+ // Returns 0..1
30762
+ next() {
30763
+ this.seed = (this.seed * 9301 + 49297) % 233280;
30764
+ return this.seed / 233280;
30765
+ }
30766
+ // Returns range [min, max)
30767
+ range(min, max) {
30768
+ return min + this.next() * (max - min);
30769
+ }
30770
+ // Uniform random on upper hemisphere (y > 0)
30771
+ randomOnSphere() {
30772
+ const y = this.next();
30773
+ const theta = 2 * Math.PI * this.next();
30774
+ const r = Math.sqrt(1 - y * y);
30775
+ const x = r * Math.cos(theta);
30776
+ const z = r * Math.sin(theta);
30777
+ return new THREE4.Vector3(x, y, z);
30778
+ }
30779
+ };
30780
+ function simpleNoise3D(v, scale) {
30781
+ const s = scale;
30782
+ 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;
30783
+ }
30784
+ function getDensity(v, opts, mwNormal) {
30785
+ let density = 0.3;
30786
+ if (opts.milkyWayEnabled) {
30787
+ const dot = v.dot(mwNormal);
30788
+ const dist = Math.abs(dot);
30789
+ const band = Math.exp(-(dist * dist) / (opts.milkyWayWidth * opts.milkyWayWidth));
30790
+ density += band * opts.milkyWayStrength;
30791
+ }
30792
+ const noise = simpleNoise3D(v, opts.noiseScale);
30793
+ density *= 1 + noise * opts.noiseStrength;
30794
+ return Math.max(0.01, density);
30795
+ }
30796
+ function generateArrangement(bible, options = {}) {
30797
+ const opts = { ...defaultGenerateOptions, ...options };
30798
+ const rng = new RNG(opts.seed);
30799
+ const arrangement = {};
30800
+ const books = [];
30801
+ bible.testaments.forEach((t) => {
30802
+ t.divisions.forEach((d) => {
30803
+ d.books.forEach((b) => {
30804
+ books.push({
30805
+ key: b.key,
30806
+ name: b.name,
30807
+ chapters: b.chapters,
30808
+ division: d.name,
30809
+ testament: t.name
30810
+ });
30811
+ });
30812
+ });
30813
+ });
30814
+ const bookCount = books.length;
30815
+ const mwRad = THREE4.MathUtils.degToRad(opts.milkyWayAngle);
30816
+ const mwNormal = new THREE4.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
30817
+ const anchors = [];
30818
+ for (let i = 0; i < bookCount; i++) {
30819
+ let bestP = new THREE4.Vector3();
30820
+ let valid = false;
30821
+ let attempt = 0;
30822
+ while (!valid && attempt < 100) {
30823
+ const p = rng.randomOnSphere();
30824
+ const d = getDensity(p, opts, mwNormal);
30825
+ if (rng.next() < d) {
30826
+ bestP = p;
30827
+ valid = true;
30828
+ }
30829
+ attempt++;
30830
+ }
30831
+ if (!valid) bestP = rng.randomOnSphere();
30832
+ anchors.push(bestP);
30833
+ }
30834
+ anchors.sort((a, b) => {
30835
+ const lonA = Math.atan2(a.z, a.x);
30836
+ const lonB = Math.atan2(b.z, b.x);
30837
+ return lonA - lonB;
30838
+ });
30839
+ books.forEach((book, i) => {
30840
+ const anchor = anchors[i];
30841
+ const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30842
+ arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
30843
+ for (let c = 0; c < book.chapters; c++) {
30844
+ const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
30845
+ const offset = new THREE4.Vector3(
30846
+ (rng.next() - 0.5) * 2,
30847
+ (rng.next() - 0.5) * 2,
30848
+ (rng.next() - 0.5) * 2
30849
+ ).normalize().multiplyScalar(rng.next() * localSpread);
30850
+ const starDir = anchor.clone().add(offset).normalize();
30851
+ if (starDir.y < 0.01) {
30852
+ starDir.y = 0.01;
30853
+ starDir.normalize();
30854
+ }
30855
+ const starPos = starDir.multiplyScalar(opts.discRadius);
30856
+ const chapId = `C:${book.key}:${c + 1}`;
30857
+ arrangement[chapId] = { position: [starPos.x, starPos.y, starPos.z] };
30858
+ }
30859
+ });
30860
+ const divisions = /* @__PURE__ */ new Map();
30861
+ books.forEach((book, i) => {
30862
+ const anchor = anchors[i];
30863
+ const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30864
+ const divId = `D:${book.testament}:${book.division}`;
30865
+ if (!divisions.has(divId)) {
30866
+ divisions.set(divId, { sum: new THREE4.Vector3(), count: 0 });
30867
+ }
30868
+ const entry = divisions.get(divId);
30869
+ entry.sum.add(anchorPos);
30870
+ entry.count++;
30871
+ });
30872
+ divisions.forEach((val, key) => {
30873
+ if (val.count > 0) {
30874
+ val.sum.divideScalar(val.count);
30875
+ val.sum.normalize().multiplyScalar(opts.discRadius * 0.9);
30876
+ arrangement[key] = { position: [val.sum.x, val.sum.y, val.sum.z] };
30877
+ }
30878
+ });
30879
+ return arrangement;
30880
+ }
30471
30881
 
30472
- export { StarMap, bibleToSceneModel, default_stars_default as defaultStars };
30882
+ export { StarMap, bibleToSceneModel, defaultGenerateOptions, default_stars_default as defaultStars, generateArrangement };
30473
30883
  //# sourceMappingURL=index.js.map
30474
30884
  //# sourceMappingURL=index.js.map