@project-skymap/library 0.4.0 → 0.5.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
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var THREE4 = require('three');
3
+ var THREE5 = require('three');
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
22
22
  return Object.freeze(n);
23
23
  }
24
24
 
25
- var THREE4__namespace = /*#__PURE__*/_interopNamespace(THREE4);
25
+ var THREE5__namespace = /*#__PURE__*/_interopNamespace(THREE5);
26
26
 
27
27
  var __defProp = Object.defineProperty;
28
28
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -151,14 +151,14 @@ var init_constellations = __esm({
151
151
  });
152
152
  function lookAt(point, target, up) {
153
153
  const zAxis = target.clone().normalize();
154
- let xAxis = new THREE4__namespace.Vector3().crossVectors(up, zAxis);
154
+ let xAxis = new THREE5__namespace.Vector3().crossVectors(up, zAxis);
155
155
  if (xAxis.lengthSq() < 1e-4) {
156
- xAxis = new THREE4__namespace.Vector3().crossVectors(new THREE4__namespace.Vector3(1, 0, 0), zAxis);
156
+ xAxis = new THREE5__namespace.Vector3().crossVectors(new THREE5__namespace.Vector3(1, 0, 0), zAxis);
157
157
  }
158
158
  xAxis.normalize();
159
- const yAxis = new THREE4__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
160
- const m = new THREE4__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
161
- const v = new THREE4__namespace.Vector3(point.x, point.y, point.z);
159
+ const yAxis = new THREE5__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
160
+ const m = new THREE5__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
161
+ const v = new THREE5__namespace.Vector3(point.x, point.y, point.z);
162
162
  v.applyMatrix4(m);
163
163
  v.add(target);
164
164
  return { x: v.x, y: v.y, z: v.z };
@@ -239,7 +239,7 @@ function computeLayoutPositions(model, layout) {
239
239
  const radiusAtY = Math.sqrt(1 - y * y);
240
240
  const x = Math.cos(midAngle) * radiusAtY;
241
241
  const z = Math.sin(midAngle) * radiusAtY;
242
- const labelPos = new THREE4__namespace.Vector3(x, y, z).multiplyScalar(radius);
242
+ const labelPos = new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
243
243
  uDivision.meta.x = labelPos.x;
244
244
  uDivision.meta.y = labelPos.y;
245
245
  uDivision.meta.z = labelPos.z;
@@ -255,7 +255,7 @@ function computeLayoutPositions(model, layout) {
255
255
  const theta = startAngle + t * angleSpan;
256
256
  const x = Math.cos(theta) * radiusAtY;
257
257
  const z = Math.sin(theta) * radiusAtY;
258
- const bookPos = new THREE4__namespace.Vector3(x, y, z).multiplyScalar(radius);
258
+ const bookPos = new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
259
259
  const labelPos = bookPos.clone();
260
260
  labelPos.y += radius * 0.025;
261
261
  labelPos.setLength(radius);
@@ -266,7 +266,7 @@ function computeLayoutPositions(model, layout) {
266
266
  if (chapters.length > 0) {
267
267
  const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
268
268
  const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
269
- const up = new THREE4__namespace.Vector3(0, 1, 0);
269
+ const up = new THREE5__namespace.Vector3(0, 1, 0);
270
270
  chapters.forEach((chap, idx) => {
271
271
  const uChap = updatedNodeMap.get(chap.id);
272
272
  const lp = localPoints[idx];
@@ -285,10 +285,10 @@ function computeLayoutPositions(model, layout) {
285
285
  testaments.forEach((t) => {
286
286
  const children = childrenMap.get(t.id) ?? [];
287
287
  if (children.length === 0) return;
288
- const centroid = new THREE4__namespace.Vector3();
288
+ const centroid = new THREE5__namespace.Vector3();
289
289
  children.forEach((c) => {
290
290
  const u = updatedNodeMap.get(c.id);
291
- centroid.add(new THREE4__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
291
+ centroid.add(new THREE5__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
292
292
  });
293
293
  centroid.divideScalar(children.length);
294
294
  if (centroid.length() > 0.1) {
@@ -352,11 +352,18 @@ vec4 smartProject(vec4 viewPos) {
352
352
  vec2 projected = vec2(k * dir.x, k * dir.y);
353
353
  projected *= uScale;
354
354
  projected.x /= uAspect;
355
- float zMetric = -1.0 + (dist / 2000.0);
355
+ float zMetric = -1.0 + (dist / 15000.0);
356
+
357
+ // Radial Clipping: Push clipped points off-screen in their natural direction
358
+ // to prevent lines "darting" across the center.
359
+ vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
360
+ vec2 escapePos = escapeDir * 10000.0;
361
+
356
362
  // Clip backward facing points in fisheye mode
357
- if (uBlend > 0.5 && dir.z > 0.4) return vec4(10.0, 10.0, 10.0, 1.0);
363
+ if (uBlend > 0.5 && dir.z > 0.4) return vec4(escapePos, 10.0, 1.0);
358
364
  // Clip very close points in linear mode
359
- if (uBlend < 0.1 && dir.z > -0.1) return vec4(10.0, 10.0, 10.0, 1.0);
365
+ if (uBlend < 0.1 && dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
366
+
360
367
  return vec4(projected, zMetric, 1.0);
361
368
  }
362
369
  `;
@@ -379,7 +386,7 @@ float getMaskAlpha() {
379
386
  });
380
387
  function createSmartMaterial(params) {
381
388
  const uniforms = { ...globalUniforms, ...params.uniforms };
382
- return new THREE4__namespace.ShaderMaterial({
389
+ return new THREE5__namespace.ShaderMaterial({
383
390
  uniforms,
384
391
  vertexShader: `
385
392
  ${BLEND_CHUNK}
@@ -393,8 +400,8 @@ function createSmartMaterial(params) {
393
400
  transparent: params.transparent || false,
394
401
  depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
395
402
  depthTest: params.depthTest !== void 0 ? params.depthTest : true,
396
- side: params.side || THREE4__namespace.FrontSide,
397
- blending: params.blending || THREE4__namespace.NormalBlending
403
+ side: params.side || THREE5__namespace.FrontSide,
404
+ blending: params.blending || THREE5__namespace.NormalBlending
398
405
  });
399
406
  }
400
407
  var globalUniforms;
@@ -404,7 +411,248 @@ var init_materials = __esm({
404
411
  globalUniforms = {
405
412
  uScale: { value: 1 },
406
413
  uAspect: { value: 1 },
407
- uBlend: { value: 0 }
414
+ uBlend: { value: 0 },
415
+ uTime: { value: 0 },
416
+ // Atmosphere Settings
417
+ uAtmGlow: { value: 1 },
418
+ uAtmDark: { value: 0.6 },
419
+ uAtmExtinction: { value: 4 },
420
+ uAtmTwinkle: { value: 0.8 },
421
+ uColorHorizon: { value: new THREE5__namespace.Color(2768476) },
422
+ uColorZenith: { value: new THREE5__namespace.Color(132104) }
423
+ };
424
+ }
425
+ });
426
+ var ConstellationArtworkLayer;
427
+ var init_ConstellationArtworkLayer = __esm({
428
+ "src/engine/ConstellationArtworkLayer.ts"() {
429
+ init_materials();
430
+ ConstellationArtworkLayer = class {
431
+ root;
432
+ items = [];
433
+ textureLoader = new THREE5__namespace.TextureLoader();
434
+ hoveredId = null;
435
+ focusedId = null;
436
+ constructor(root) {
437
+ this.root = new THREE5__namespace.Group();
438
+ this.root.renderOrder = -1;
439
+ root.add(this.root);
440
+ }
441
+ getItems() {
442
+ return this.items;
443
+ }
444
+ setPosition(id, pos) {
445
+ const item = this.items.find((i) => i.config.id === id);
446
+ if (item) {
447
+ item.mesh.position.copy(pos);
448
+ }
449
+ }
450
+ load(config, getPosition) {
451
+ this.clear();
452
+ const basePath = config.atlasBasePath.replace(/\/$/, "");
453
+ config.constellations.forEach((c) => {
454
+ let center = new THREE5__namespace.Vector3();
455
+ let valid = false;
456
+ let radius = 2e3;
457
+ const arrPos = getPosition(c.id);
458
+ if (arrPos) {
459
+ center.copy(arrPos);
460
+ valid = true;
461
+ if (c.anchors.length > 0) {
462
+ const points = [];
463
+ for (const anchorId of c.anchors) {
464
+ const p = getPosition(anchorId);
465
+ if (p) points.push(p);
466
+ }
467
+ if (points.length > 0) {
468
+ radius = points[0].length();
469
+ }
470
+ }
471
+ } else if (c.center) {
472
+ center.set(c.center[0], c.center[1], c.center[2]);
473
+ valid = true;
474
+ } else if (c.anchors.length > 0) {
475
+ const points = [];
476
+ for (const anchorId of c.anchors) {
477
+ const p = getPosition(anchorId);
478
+ if (p) points.push(p);
479
+ }
480
+ if (points.length > 0) {
481
+ for (const p of points) center.add(p);
482
+ center.divideScalar(points.length);
483
+ const len = center.length();
484
+ if (len > 1e-3) {
485
+ radius = points[0].length();
486
+ center.normalize().multiplyScalar(radius);
487
+ }
488
+ valid = true;
489
+ }
490
+ }
491
+ if (!valid) return;
492
+ const normal = center.clone().normalize().negate();
493
+ const upVec = center.clone().normalize();
494
+ let right = new THREE5__namespace.Vector3(1, 0, 0);
495
+ if (c.anchors.length >= 2) {
496
+ const p0 = getPosition(c.anchors[0]);
497
+ const p1 = getPosition(c.anchors[1]);
498
+ if (p0 && p1) {
499
+ const diff = new THREE5__namespace.Vector3().subVectors(p1, p0);
500
+ right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
501
+ }
502
+ } else {
503
+ if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
504
+ else right.set(0, 1, 0).cross(upVec).normalize();
505
+ }
506
+ const top = new THREE5__namespace.Vector3().crossVectors(upVec, right).normalize();
507
+ right.crossVectors(top, upVec).normalize();
508
+ new THREE5__namespace.Matrix4().makeBasis(right, top, normal);
509
+ const geometry = new THREE5__namespace.PlaneGeometry(1, 1);
510
+ let size = c.radius;
511
+ if (size <= 1) size *= radius;
512
+ size *= 2;
513
+ const texPath = `${basePath}/${c.image}`;
514
+ let blending = THREE5__namespace.NormalBlending;
515
+ if (c.blend === "additive") blending = THREE5__namespace.AdditiveBlending;
516
+ const material = createSmartMaterial({
517
+ uniforms: {
518
+ uMap: { value: this.textureLoader.load(texPath) },
519
+ // Placeholder, updated below
520
+ uOpacity: { value: c.opacity },
521
+ uSize: { value: size },
522
+ uImgRotation: { value: THREE5__namespace.MathUtils.degToRad(c.rotationDeg) },
523
+ uImgAspect: { value: c.aspectRatio ?? 1 }
524
+ // uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
525
+ },
526
+ vertexShaderBody: `
527
+ uniform float uSize;
528
+ uniform float uImgRotation;
529
+ uniform float uImgAspect;
530
+
531
+ varying vec2 vUv;
532
+
533
+ void main() {
534
+ vUv = uv;
535
+
536
+ // 1. Project Center Point (Proven Method)
537
+ vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
538
+ vec4 clipCenter = smartProject(mvCenter);
539
+
540
+ // 2. Project "Up" Point (World Zenith)
541
+ // Transform World Up (0,1,0) to View Space
542
+ vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
543
+ // Offset center by a significant amount (1000.0) to ensure screen delta
544
+ vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
545
+ vec4 clipUp = smartProject(mvUp);
546
+
547
+ // 3. Calculate Horizon Angle
548
+ vec2 screenCenter = clipCenter.xy / clipCenter.w;
549
+ vec2 screenUp = clipUp.xy / clipUp.w;
550
+ vec2 screenDelta = screenUp - screenCenter;
551
+
552
+ float horizonAngle = 0.0;
553
+ if (length(screenDelta) > 0.001) {
554
+ vec2 screenDir = normalize(screenDelta);
555
+ horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
556
+ }
557
+
558
+ // 4. Combine with User Rotation
559
+ float finalAngle = uImgRotation + horizonAngle;
560
+
561
+ // 5. Billboard Offset
562
+ vec2 offset = position.xy;
563
+
564
+ float cr = cos(finalAngle);
565
+ float sr = sin(finalAngle);
566
+ vec2 rotated = vec2(
567
+ offset.x * cr - offset.y * sr,
568
+ offset.x * sr + offset.y * cr
569
+ );
570
+
571
+ rotated.x *= uImgAspect;
572
+
573
+ float dist = length(mvCenter.xyz);
574
+ float scale = (uSize / dist) * uScale;
575
+
576
+ rotated *= scale;
577
+ rotated.x /= uAspect;
578
+
579
+ gl_Position = clipCenter;
580
+ gl_Position.xy += rotated * clipCenter.w;
581
+
582
+ vScreenPos = gl_Position.xy / gl_Position.w;
583
+ }
584
+ `,
585
+ fragmentShader: `
586
+ uniform sampler2D uMap;
587
+ uniform float uOpacity;
588
+ varying vec2 vUv;
589
+ void main() {
590
+ float mask = getMaskAlpha();
591
+ if (mask < 0.01) discard;
592
+ vec4 tex = texture2D(uMap, vUv);
593
+ gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
594
+ }
595
+ `,
596
+ transparent: true,
597
+ depthWrite: false,
598
+ depthTest: true,
599
+ blending,
600
+ side: THREE5__namespace.DoubleSide
601
+ });
602
+ material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
603
+ if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
604
+ const natAspect = tex.image.width / tex.image.height;
605
+ material.uniforms.uImgAspect.value = natAspect;
606
+ }
607
+ });
608
+ if (c.zBias) {
609
+ material.polygonOffset = true;
610
+ material.polygonOffsetFactor = -c.zBias;
611
+ }
612
+ const mesh = new THREE5__namespace.Mesh(geometry, material);
613
+ mesh.frustumCulled = false;
614
+ mesh.userData = { id: c.id, type: "constellation" };
615
+ mesh.position.copy(center);
616
+ this.root.add(mesh);
617
+ this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
618
+ });
619
+ }
620
+ update(fov, showArt) {
621
+ this.root.visible = showArt;
622
+ if (!showArt) return;
623
+ for (const item of this.items) {
624
+ const { fade } = item.config;
625
+ let opacity = fade.maxOpacity;
626
+ if (fov >= fade.zoomInStart) {
627
+ opacity = fade.maxOpacity;
628
+ } else if (fov <= fade.zoomInEnd) {
629
+ opacity = fade.minOpacity;
630
+ } else {
631
+ const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
632
+ opacity = THREE5__namespace.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
633
+ }
634
+ opacity = Math.min(Math.max(opacity, 0), 1);
635
+ item.material.uniforms.uOpacity.value = opacity;
636
+ }
637
+ }
638
+ setHovered(id) {
639
+ this.hoveredId = id;
640
+ }
641
+ setFocused(id) {
642
+ this.focusedId = id;
643
+ }
644
+ dispose() {
645
+ this.clear();
646
+ this.root.removeFromParent();
647
+ }
648
+ clear() {
649
+ this.items.forEach((i) => {
650
+ this.root.remove(i.mesh);
651
+ i.material.dispose();
652
+ i.mesh.geometry.dispose();
653
+ });
654
+ this.items = [];
655
+ }
408
656
  };
409
657
  }
410
658
  });
@@ -418,17 +666,37 @@ function createEngine({
418
666
  container,
419
667
  onSelect,
420
668
  onHover,
421
- onArrangementChange
669
+ onArrangementChange,
670
+ onFovChange
422
671
  }) {
423
- const renderer = new THREE4__namespace.WebGLRenderer({ antialias: true, alpha: false });
672
+ let hoveredBookId = null;
673
+ let focusedBookId = null;
674
+ let orderRevealEnabled = true;
675
+ let activeBookIndex = -1;
676
+ let orderRevealStrength = 0;
677
+ const hoverCooldowns = /* @__PURE__ */ new Map();
678
+ const COOLDOWN_MS = 2e3;
679
+ const bookIdToIndex = /* @__PURE__ */ new Map();
680
+ const renderer = new THREE5__namespace.WebGLRenderer({ antialias: true, alpha: false });
424
681
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
425
682
  renderer.setSize(container.clientWidth, container.clientHeight);
426
683
  container.appendChild(renderer.domElement);
427
- const scene = new THREE4__namespace.Scene();
428
- scene.background = new THREE4__namespace.Color(0);
429
- const camera = new THREE4__namespace.PerspectiveCamera(60, 1, 0.1, 3e3);
684
+ const scene = new THREE5__namespace.Scene();
685
+ scene.background = new THREE5__namespace.Color(0);
686
+ const camera = new THREE5__namespace.PerspectiveCamera(60, 1, 0.1, 1e4);
430
687
  camera.position.set(0, 0, 0);
431
688
  camera.up.set(0, 1, 0);
689
+ function setHoveredBook(id) {
690
+ if (id === hoveredBookId) return;
691
+ const now = performance.now();
692
+ if (hoveredBookId) {
693
+ hoverCooldowns.set(hoveredBookId, now);
694
+ }
695
+ if (id) {
696
+ hoverCooldowns.get(id) || 0;
697
+ }
698
+ hoveredBookId = id;
699
+ }
432
700
  let running = false;
433
701
  let raf = 0;
434
702
  const state = {
@@ -449,10 +717,12 @@ function createEngine({
449
717
  draggedGroup: null,
450
718
  tempArrangement: {}
451
719
  };
452
- const mouseNDC = new THREE4__namespace.Vector2();
720
+ const mouseNDC = new THREE5__namespace.Vector2();
453
721
  let isMouseInWindow = false;
454
- let handlers = { onSelect, onHover, onArrangementChange };
722
+ let edgeHoverStart = 0;
723
+ let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
455
724
  let currentConfig;
725
+ const constellationLayer = new ConstellationArtworkLayer(scene);
456
726
  function mix(a, b, t) {
457
727
  return a * (1 - t) + b * t;
458
728
  }
@@ -487,7 +757,7 @@ function createEngine({
487
757
  const phi = Math.atan2(uvY, uvX);
488
758
  const sinTheta = Math.sin(theta);
489
759
  const cosTheta = Math.cos(theta);
490
- return new THREE4__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
760
+ return new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
491
761
  }
492
762
  function getMouseWorldVector(pixelX, pixelY, width, height) {
493
763
  const aspect = width / height;
@@ -506,7 +776,7 @@ function createEngine({
506
776
  const phi = Math.atan2(uvY, uvX);
507
777
  const sinTheta = Math.sin(theta);
508
778
  const cosTheta = Math.cos(theta);
509
- const vView = new THREE4__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
779
+ const vView = new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
510
780
  return vView.applyQuaternion(camera.quaternion);
511
781
  }
512
782
  function smartProjectJS(worldPos) {
@@ -519,147 +789,187 @@ function createEngine({
519
789
  const k = mix(kLinear, kStereo, blend);
520
790
  return { x: k * dir.x, y: k * dir.y, z: dir.z };
521
791
  }
522
- const groundGroup = new THREE4__namespace.Group();
792
+ const groundGroup = new THREE5__namespace.Group();
523
793
  scene.add(groundGroup);
524
794
  function createGround() {
525
795
  groundGroup.clear();
526
796
  const radius = 995;
527
- const geometry = new THREE4__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2);
797
+ const geometry = new THREE5__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
528
798
  const material = createSmartMaterial({
529
- uniforms: { color: { value: new THREE4__namespace.Color(526862) } },
530
- vertexShaderBody: `varying vec3 vPos; void main() { vPos = position; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
531
- fragmentShader: `uniform vec3 color; varying vec3 vPos; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; float noise = sin(vPos.x * 0.2) * sin(vPos.z * 0.2) * 0.05; vec3 col = color + noise; vec3 n = normalize(vPos); float horizon = smoothstep(-0.02, 0.0, n.y); col += vec3(0.1, 0.15, 0.2) * horizon; gl_FragColor = vec4(col, 1.0); }`,
532
- side: THREE4__namespace.BackSide,
799
+ uniforms: {
800
+ color: { value: new THREE5__namespace.Color(131587) },
801
+ // Very dark almost black
802
+ fogColor: { value: new THREE5__namespace.Color(331812) }
803
+ // Matches atmosphere bot color
804
+ },
805
+ vertexShaderBody: `
806
+ varying vec3 vPos;
807
+ varying vec3 vWorldPos;
808
+ void main() {
809
+ vPos = position;
810
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
811
+ gl_Position = smartProject(mvPosition);
812
+ vScreenPos = gl_Position.xy / gl_Position.w;
813
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
814
+ }
815
+ `,
816
+ fragmentShader: `
817
+ uniform vec3 color;
818
+ uniform vec3 fogColor;
819
+ varying vec3 vPos;
820
+ varying vec3 vWorldPos;
821
+
822
+ void main() {
823
+ float alphaMask = getMaskAlpha();
824
+ if (alphaMask < 0.01) discard;
825
+
826
+ // Procedural Horizon (Mountains)
827
+ float angle = atan(vPos.z, vPos.x);
828
+
829
+ // Simple FBM-like terrain
830
+ float h = 0.0;
831
+ h += sin(angle * 6.0) * 20.0;
832
+ h += sin(angle * 13.0 + 1.0) * 10.0;
833
+ h += sin(angle * 29.0 + 2.0) * 5.0;
834
+ h += sin(angle * 63.0 + 4.0) * 2.0;
835
+
836
+ // Base horizon offset (lift slightly)
837
+ float terrainHeight = h + 10.0;
838
+
839
+ if (vPos.y > terrainHeight) discard;
840
+
841
+ // Atmospheric Haze / Fog on the ground
842
+ // Mix ground color with fog color based on vertical height (fade into horizon)
843
+ // Closer to horizon (higher y) -> more fog
844
+ float fogFactor = smoothstep(-100.0, terrainHeight, vPos.y);
845
+ vec3 finalCol = mix(color, fogColor, fogFactor * 0.5);
846
+
847
+ gl_FragColor = vec4(finalCol, 1.0);
848
+ }
849
+ `,
850
+ side: THREE5__namespace.BackSide,
533
851
  transparent: false,
534
852
  depthWrite: true,
535
853
  depthTest: true
536
854
  });
537
- const ground = new THREE4__namespace.Mesh(geometry, material);
855
+ const ground = new THREE5__namespace.Mesh(geometry, material);
538
856
  groundGroup.add(ground);
539
- const boxGeo = new THREE4__namespace.BoxGeometry(8, 30, 8);
540
- for (let i = 0; i < 12; i++) {
541
- const angle = i / 12 * Math.PI * 2;
542
- const b = new THREE4__namespace.Mesh(boxGeo, material);
543
- const r = radius * 0.98;
544
- b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
545
- b.lookAt(0, 0, 0);
546
- groundGroup.add(b);
547
- }
548
857
  }
858
+ let atmosphereMesh = null;
549
859
  function createAtmosphere() {
550
- const geometry = new THREE4__namespace.SphereGeometry(990, 128, 64);
860
+ const geometry = new THREE5__namespace.SphereGeometry(990, 64, 64);
551
861
  const material = createSmartMaterial({
552
- uniforms: { top: { value: new THREE4__namespace.Color(0) }, bot: { value: new THREE4__namespace.Color(1712172) } },
553
862
  vertexShaderBody: `
554
-
555
- varying vec3 vP;
556
-
557
- void main() {
558
-
559
- vP = position;
560
-
561
- vec4 mv = modelViewMatrix * vec4(position, 1.0);
562
-
563
- gl_Position = smartProject(mv);
564
-
565
- vScreenPos = gl_Position.xy / gl_Position.w;
566
-
567
- }
568
-
569
- `,
863
+ varying vec3 vWorldNormal;
864
+ void main() {
865
+ vWorldNormal = normalize(position);
866
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
867
+ gl_Position = smartProject(mv);
868
+ vScreenPos = gl_Position.xy / gl_Position.w;
869
+ }`,
570
870
  fragmentShader: `
571
-
572
- uniform vec3 top;
573
-
574
- uniform vec3 bot;
575
-
576
- varying vec3 vP;
577
-
578
- void main() {
579
-
580
- float alphaMask = getMaskAlpha();
581
-
582
- if (alphaMask < 0.01) discard;
583
-
584
- vec3 n = normalize(vP);
585
-
586
- float h = max(0.0, n.y);
587
-
588
- gl_FragColor = vec4(mix(bot, top, pow(h, 0.6)), 1.0);
589
-
590
- }
591
-
592
- `,
593
- side: THREE4__namespace.BackSide,
871
+ varying vec3 vWorldNormal;
872
+
873
+ uniform float uAtmGlow;
874
+ uniform float uAtmDark;
875
+ uniform vec3 uColorHorizon;
876
+ uniform vec3 uColorZenith;
877
+
878
+ void main() {
879
+ float alphaMask = getMaskAlpha();
880
+ if (alphaMask < 0.01) discard;
881
+
882
+ // Altitude angle (Y is up)
883
+ float h = normalize(vWorldNormal).y;
884
+
885
+ // Gradient Logic
886
+ // 1. Base gradient from Horizon to Zenith
887
+ float t = smoothstep(-0.1, 0.5, h);
888
+
889
+ // Non-linear mix for realistic sky falloff
890
+ // Zenith darkness adjustment
891
+ vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
892
+
893
+ // 2. Horizon Glow Band (Simulate scattering/haze layer)
894
+ float horizonBand = exp(-15.0 * abs(h - 0.02)); // Sharp peak near 0
895
+ skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
896
+
897
+ gl_FragColor = vec4(skyColor, 1.0);
898
+ }
899
+ `,
900
+ side: THREE5__namespace.BackSide,
594
901
  depthWrite: false,
595
902
  depthTest: true
596
903
  });
597
- const atm = new THREE4__namespace.Mesh(geometry, material);
904
+ const atm = new THREE5__namespace.Mesh(geometry, material);
905
+ atmosphereMesh = atm;
598
906
  groundGroup.add(atm);
599
907
  }
600
- const backdropGroup = new THREE4__namespace.Group();
908
+ const backdropGroup = new THREE5__namespace.Group();
601
909
  scene.add(backdropGroup);
602
- function createBackdropStars() {
910
+ function createBackdropStars(count = 31e3) {
603
911
  backdropGroup.clear();
604
- const geometry = new THREE4__namespace.BufferGeometry();
912
+ while (backdropGroup.children.length > 0) {
913
+ const c = backdropGroup.children[0];
914
+ backdropGroup.remove(c);
915
+ if (c.geometry) c.geometry.dispose();
916
+ if (c.material) c.material.dispose();
917
+ }
918
+ const geometry = new THREE5__namespace.BufferGeometry();
605
919
  const positions = [];
606
920
  const sizes = [];
607
921
  const colors = [];
608
- const colorPalette = [
609
- new THREE4__namespace.Color(10203391),
610
- new THREE4__namespace.Color(11190271),
611
- new THREE4__namespace.Color(13293567),
612
- new THREE4__namespace.Color(16316415),
613
- new THREE4__namespace.Color(16774378),
614
- new THREE4__namespace.Color(16765601),
615
- new THREE4__namespace.Color(16764015)
616
- ];
617
922
  const r = 2500;
618
- new THREE4__namespace.Vector3(0, 1, 0.5).normalize();
619
- for (let i = 0; i < 4e3; i++) {
620
- const isMilkyWay = Math.random() < 0.4;
621
- let x, y, z;
622
- if (isMilkyWay) {
623
- const theta = Math.random() * Math.PI * 2;
624
- const scatter = (Math.random() - 0.5) * 0.4;
625
- const v = new THREE4__namespace.Vector3(Math.cos(theta), scatter, Math.sin(theta));
626
- v.normalize();
627
- v.applyAxisAngle(new THREE4__namespace.Vector3(1, 0, 0), THREE4__namespace.MathUtils.degToRad(60));
628
- x = v.x * r;
629
- y = v.y * r;
630
- z = v.z * r;
631
- } else {
632
- const u = Math.random();
633
- const v = Math.random();
634
- const theta = 2 * Math.PI * u;
635
- const phi = Math.acos(2 * v - 1);
636
- x = r * Math.sin(phi) * Math.cos(theta);
637
- y = r * Math.sin(phi) * Math.sin(theta);
638
- z = r * Math.cos(phi);
639
- }
923
+ for (let i = 0; i < count; i++) {
924
+ const u = Math.random();
925
+ const v = Math.random();
926
+ const theta = 2 * Math.PI * u;
927
+ const phi = Math.acos(2 * v - 1);
928
+ const x = r * Math.sin(phi) * Math.cos(theta);
929
+ const y = r * Math.cos(phi);
930
+ const z = r * Math.sin(phi) * Math.sin(theta);
640
931
  positions.push(x, y, z);
641
- const size = 0.5 + -Math.log(Math.random()) * 0.8 * 1.5;
932
+ const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
642
933
  sizes.push(size);
643
- const cIndex = Math.floor(Math.random() * colorPalette.length);
644
- const c = colorPalette[cIndex];
645
- colors.push(c.r, c.g, c.b);
934
+ colors.push(1, 1, 1);
646
935
  }
647
- geometry.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(positions, 3));
648
- geometry.setAttribute("size", new THREE4__namespace.Float32BufferAttribute(sizes, 1));
649
- geometry.setAttribute("color", new THREE4__namespace.Float32BufferAttribute(colors, 3));
936
+ geometry.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(positions, 3));
937
+ geometry.setAttribute("size", new THREE5__namespace.Float32BufferAttribute(sizes, 1));
938
+ geometry.setAttribute("color", new THREE5__namespace.Float32BufferAttribute(colors, 3));
650
939
  const material = createSmartMaterial({
651
- uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
940
+ uniforms: {
941
+ pixelRatio: { value: renderer.getPixelRatio() },
942
+ uScale: globalUniforms.uScale
943
+ },
652
944
  vertexShaderBody: `
653
945
  attribute float size;
654
946
  attribute vec3 color;
655
947
  varying vec3 vColor;
656
948
  uniform float pixelRatio;
949
+
950
+ uniform float uAtmExtinction;
951
+
657
952
  void main() {
658
- vColor = color;
953
+ vec3 nPos = normalize(position);
954
+ float altitude = nPos.y;
955
+
956
+ // Simple Extinction & Horizon Fade
957
+ float horizonFade = smoothstep(-0.1, 0.1, altitude);
958
+ float airmass = 1.0 / (max(0.05, altitude + 0.05));
959
+ float extinction = exp(-uAtmExtinction * 0.15 * airmass);
960
+
961
+ // Boost intensity significantly (3.0x)
962
+ vColor = color * 3.0 * extinction * horizonFade;
963
+
659
964
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
660
965
  gl_Position = smartProject(mvPosition);
661
966
  vScreenPos = gl_Position.xy / gl_Position.w;
662
- gl_PointSize = size * pixelRatio * (600.0 / -mvPosition.z);
967
+
968
+ // Non-linear scale with zoom to keep stars looking like points
969
+ // pow(uScale, 0.5) prevents them from getting too large at low FOV
970
+ float zoomScale = pow(uScale, 0.5);
971
+
972
+ gl_PointSize = size * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade;
663
973
  }
664
974
  `,
665
975
  fragmentShader: `
@@ -670,26 +980,28 @@ function createEngine({
670
980
  if (dist > 1.0) discard;
671
981
  float alphaMask = getMaskAlpha();
672
982
  if (alphaMask < 0.01) discard;
673
- // Use same Gaussian glow for backdrop
674
- float alpha = exp(-3.0 * dist * dist);
983
+
984
+ // Sharp falloff for intense point look
985
+ float alpha = exp(-4.0 * dist * dist);
675
986
  gl_FragColor = vec4(vColor, alpha * alphaMask);
676
987
  }
677
988
  `,
678
989
  transparent: true,
679
990
  depthWrite: false,
680
- depthTest: true
991
+ depthTest: true,
992
+ blending: THREE5__namespace.AdditiveBlending
681
993
  });
682
- const points = new THREE4__namespace.Points(geometry, material);
994
+ const points = new THREE5__namespace.Points(geometry, material);
683
995
  points.frustumCulled = false;
684
996
  backdropGroup.add(points);
685
997
  }
686
998
  createGround();
687
999
  createAtmosphere();
688
1000
  createBackdropStars();
689
- const raycaster = new THREE4__namespace.Raycaster();
1001
+ const raycaster = new THREE5__namespace.Raycaster();
690
1002
  raycaster.params.Points.threshold = 5;
691
- new THREE4__namespace.Vector2();
692
- const root = new THREE4__namespace.Group();
1003
+ new THREE5__namespace.Vector2();
1004
+ const root = new THREE5__namespace.Group();
693
1005
  scene.add(root);
694
1006
  const nodeById = /* @__PURE__ */ new Map();
695
1007
  const starIndexToId = [];
@@ -697,7 +1009,7 @@ function createEngine({
697
1009
  const hoverLabelMat = createSmartMaterial({
698
1010
  uniforms: {
699
1011
  uMap: { value: null },
700
- uSize: { value: new THREE4__namespace.Vector2(1, 1) },
1012
+ uSize: { value: new THREE5__namespace.Vector2(1, 1) },
701
1013
  uAlpha: { value: 0 },
702
1014
  uAngle: { value: 0 }
703
1015
  },
@@ -735,7 +1047,7 @@ function createEngine({
735
1047
  depthTest: false
736
1048
  // Always on top of stars
737
1049
  });
738
- const hoverLabelMesh = new THREE4__namespace.Mesh(new THREE4__namespace.PlaneGeometry(1, 1), hoverLabelMat);
1050
+ const hoverLabelMesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), hoverLabelMat);
739
1051
  hoverLabelMesh.visible = false;
740
1052
  hoverLabelMesh.renderOrder = 999;
741
1053
  hoverLabelMesh.frustumCulled = false;
@@ -766,19 +1078,20 @@ function createEngine({
766
1078
  const ctx = canvas.getContext("2d");
767
1079
  if (!ctx) return null;
768
1080
  const fontSize = 96;
769
- ctx.font = `bold ${fontSize}px sans-serif`;
1081
+ const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
1082
+ ctx.font = font;
770
1083
  const metrics = ctx.measureText(text);
771
1084
  const w = Math.ceil(metrics.width);
772
1085
  const h = Math.ceil(fontSize * 1.2);
773
1086
  canvas.width = w;
774
1087
  canvas.height = h;
775
- ctx.font = `bold ${fontSize}px sans-serif`;
1088
+ ctx.font = font;
776
1089
  ctx.fillStyle = color;
777
1090
  ctx.textAlign = "center";
778
1091
  ctx.textBaseline = "middle";
779
1092
  ctx.fillText(text, w / 2, h / 2);
780
- const tex = new THREE4__namespace.CanvasTexture(canvas);
781
- tex.minFilter = THREE4__namespace.LinearFilter;
1093
+ const tex = new THREE5__namespace.CanvasTexture(canvas);
1094
+ tex.minFilter = THREE5__namespace.LinearFilter;
782
1095
  return { tex, aspect: w / h };
783
1096
  }
784
1097
  function getPosition(n) {
@@ -792,27 +1105,28 @@ function createEngine({
792
1105
  const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
793
1106
  const phi = Math.atan2(y, x);
794
1107
  const theta = r_norm * (Math.PI / 2);
795
- return new THREE4__namespace.Vector3(
1108
+ return new THREE5__namespace.Vector3(
796
1109
  Math.sin(theta) * Math.cos(phi),
797
1110
  Math.cos(theta),
798
1111
  Math.sin(theta) * Math.sin(phi)
799
1112
  ).multiplyScalar(radius);
800
1113
  }
801
- return new THREE4__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
1114
+ return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
802
1115
  }
803
1116
  }
804
- return new THREE4__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
1117
+ return new THREE5__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
805
1118
  }
806
1119
  function getBoundaryPoint(angle, t, radius) {
807
1120
  const y = 0.05 + t * (1 - 0.05);
808
1121
  const rY = Math.sqrt(1 - y * y);
809
1122
  const x = Math.cos(angle) * rY;
810
1123
  const z = Math.sin(angle) * rY;
811
- return new THREE4__namespace.Vector3(x, y, z).multiplyScalar(radius);
1124
+ return new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
812
1125
  }
813
1126
  function buildFromModel(model, cfg) {
814
1127
  clearRoot();
815
- scene.background = cfg.background && cfg.background !== "transparent" ? new THREE4__namespace.Color(cfg.background) : new THREE4__namespace.Color(0);
1128
+ bookIdToIndex.clear();
1129
+ scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5__namespace.Color(cfg.background) : new THREE5__namespace.Color(0);
816
1130
  const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
817
1131
  const laidOut = computeLayoutPositions(model, layoutCfg);
818
1132
  const divisionPositions = /* @__PURE__ */ new Map();
@@ -826,7 +1140,7 @@ function createEngine({
826
1140
  }
827
1141
  }
828
1142
  for (const [divId, books] of divMap.entries()) {
829
- const centroid = new THREE4__namespace.Vector3();
1143
+ const centroid = new THREE5__namespace.Vector3();
830
1144
  let count = 0;
831
1145
  for (const b of books) {
832
1146
  const p = getPosition(b);
@@ -842,21 +1156,24 @@ function createEngine({
842
1156
  const starPositions = [];
843
1157
  const starSizes = [];
844
1158
  const starColors = [];
1159
+ const starPhases = [];
1160
+ const starBookIndices = [];
1161
+ const starChapterIndices = [];
845
1162
  const SPECTRAL_COLORS = [
846
- new THREE4__namespace.Color(10203391),
847
- // O - Blue
848
- new THREE4__namespace.Color(11190271),
849
- // B - Blue-white
850
- new THREE4__namespace.Color(13293567),
851
- // A - White-blue
852
- new THREE4__namespace.Color(16316415),
1163
+ new THREE5__namespace.Color(14544639),
1164
+ // O - Blueish White
1165
+ new THREE5__namespace.Color(15660287),
1166
+ // B - White
1167
+ new THREE5__namespace.Color(16317695),
1168
+ // A - White
1169
+ new THREE5__namespace.Color(16777208),
853
1170
  // F - White
854
- new THREE4__namespace.Color(16774378),
855
- // G - Yellow-white
856
- new THREE4__namespace.Color(16765601),
857
- // K - Yellow-orange
858
- new THREE4__namespace.Color(16764015)
859
- // M - Orange-red
1171
+ new THREE5__namespace.Color(16775406),
1172
+ // G - Yellowish White
1173
+ new THREE5__namespace.Color(16773085),
1174
+ // K - Pale Orange
1175
+ new THREE5__namespace.Color(16771788)
1176
+ // M - Light Orange
860
1177
  ];
861
1178
  let minWeight = Infinity;
862
1179
  let maxWeight = -Infinity;
@@ -881,21 +1198,41 @@ function createEngine({
881
1198
  let baseSize = 3.5;
882
1199
  if (typeof n.weight === "number") {
883
1200
  const t = (n.weight - minWeight) / (maxWeight - minWeight);
884
- baseSize = 3 + t * 4;
1201
+ baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
885
1202
  }
886
1203
  starSizes.push(baseSize);
887
1204
  const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
888
1205
  const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
889
1206
  starColors.push(c.r, c.g, c.b);
1207
+ starPhases.push(Math.random() * Math.PI * 2);
1208
+ let bIdx = -1;
1209
+ if (n.parent) {
1210
+ if (!bookIdToIndex.has(n.parent)) {
1211
+ bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
1212
+ }
1213
+ bIdx = bookIdToIndex.get(n.parent);
1214
+ }
1215
+ starBookIndices.push(bIdx);
1216
+ let cIdx = 0;
1217
+ if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
1218
+ starChapterIndices.push(cIdx);
890
1219
  }
891
1220
  if (n.level === 1 || n.level === 2 || n.level === 3) {
892
- const color = n.level === 1 ? "#38bdf8" : "#ffffff";
893
- const texRes = createTextTexture(n.label, color);
1221
+ let color = "#ffffff";
1222
+ if (n.level === 1) color = "#38bdf8";
1223
+ else if (n.level === 2) color = "#cbd5e1";
1224
+ else if (n.level === 3) color = "#94a3b8";
1225
+ let labelText = n.label;
1226
+ if (n.level === 3 && n.meta?.chapter) {
1227
+ labelText = String(n.meta.chapter);
1228
+ }
1229
+ const texRes = createTextTexture(labelText, color);
894
1230
  if (texRes) {
895
1231
  let baseScale = 0.05;
896
1232
  if (n.level === 1) baseScale = 0.08;
897
- else if (n.level === 3) baseScale = 0.04;
898
- const size = new THREE4__namespace.Vector2(baseScale * texRes.aspect, baseScale);
1233
+ else if (n.level === 2) baseScale = 0.04;
1234
+ else if (n.level === 3) baseScale = 0.03;
1235
+ const size = new THREE5__namespace.Vector2(baseScale * texRes.aspect, baseScale);
899
1236
  const mat = createSmartMaterial({
900
1237
  uniforms: {
901
1238
  uMap: { value: texRes.tex },
@@ -936,7 +1273,7 @@ function createEngine({
936
1273
  depthWrite: false,
937
1274
  depthTest: true
938
1275
  });
939
- const mesh = new THREE4__namespace.Mesh(new THREE4__namespace.PlaneGeometry(1, 1), mat);
1276
+ const mesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), mat);
940
1277
  let p = getPosition(n);
941
1278
  if (n.level === 1) {
942
1279
  if (divisionPositions.has(n.id)) {
@@ -946,7 +1283,8 @@ function createEngine({
946
1283
  const angle = Math.atan2(p.z, p.x);
947
1284
  p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
948
1285
  } else if (n.level === 3) {
949
- p.multiplyScalar(1.002);
1286
+ p.y += 30;
1287
+ p.multiplyScalar(1.001);
950
1288
  }
951
1289
  mesh.position.set(p.x, p.y, p.z);
952
1290
  mesh.scale.set(size.x, size.y, 1);
@@ -957,47 +1295,119 @@ function createEngine({
957
1295
  }
958
1296
  }
959
1297
  }
960
- const starGeo = new THREE4__namespace.BufferGeometry();
961
- starGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(starPositions, 3));
962
- starGeo.setAttribute("size", new THREE4__namespace.Float32BufferAttribute(starSizes, 1));
963
- starGeo.setAttribute("color", new THREE4__namespace.Float32BufferAttribute(starColors, 3));
1298
+ const starGeo = new THREE5__namespace.BufferGeometry();
1299
+ starGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(starPositions, 3));
1300
+ starGeo.setAttribute("size", new THREE5__namespace.Float32BufferAttribute(starSizes, 1));
1301
+ starGeo.setAttribute("color", new THREE5__namespace.Float32BufferAttribute(starColors, 3));
1302
+ starGeo.setAttribute("phase", new THREE5__namespace.Float32BufferAttribute(starPhases, 1));
1303
+ starGeo.setAttribute("bookIndex", new THREE5__namespace.Float32BufferAttribute(starBookIndices, 1));
1304
+ starGeo.setAttribute("chapterIndex", new THREE5__namespace.Float32BufferAttribute(starChapterIndices, 1));
964
1305
  const starMat = createSmartMaterial({
965
- uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
1306
+ uniforms: {
1307
+ pixelRatio: { value: renderer.getPixelRatio() },
1308
+ uScale: globalUniforms.uScale,
1309
+ uTime: globalUniforms.uTime,
1310
+ uActiveBookIndex: { value: -1 },
1311
+ uOrderRevealStrength: { value: 0 },
1312
+ uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
1313
+ uPulseParams: { value: new THREE5__namespace.Vector3(
1314
+ ORDER_REVEAL_CONFIG.pulseDuration,
1315
+ ORDER_REVEAL_CONFIG.delayPerChapter,
1316
+ ORDER_REVEAL_CONFIG.pulseAmplitude
1317
+ ) }
1318
+ },
966
1319
  vertexShaderBody: `
967
1320
  attribute float size;
968
1321
  attribute vec3 color;
1322
+ attribute float phase;
1323
+ attribute float bookIndex;
1324
+ attribute float chapterIndex;
1325
+
969
1326
  varying vec3 vColor;
970
1327
  uniform float pixelRatio;
1328
+
1329
+ uniform float uTime;
1330
+ uniform float uAtmExtinction;
1331
+ uniform float uAtmTwinkle;
1332
+
1333
+ uniform float uActiveBookIndex;
1334
+ uniform float uOrderRevealStrength;
1335
+ uniform float uGlobalDimFactor;
1336
+ uniform vec3 uPulseParams;
1337
+
971
1338
  void main() {
972
- vColor = color;
1339
+ vec3 nPos = normalize(position);
1340
+
1341
+ // 1. Altitude (Y is UP)
1342
+ float altitude = nPos.y;
1343
+
1344
+ // 2. Atmospheric Extinction (Airmass approximation)
1345
+ float airmass = 1.0 / (max(0.02, altitude + 0.05));
1346
+ float extinction = exp(-uAtmExtinction * 0.1 * airmass);
1347
+
1348
+ // Fade out stars below horizon
1349
+ float horizonFade = smoothstep(-0.1, 0.05, altitude);
1350
+
1351
+ // 3. Scintillation
1352
+ float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
1353
+ float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
1354
+ float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
1355
+
1356
+ // --- Order Reveal Logic ---
1357
+ float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
1358
+
1359
+ // Dimming
1360
+ float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
1361
+
1362
+ // Pulse
1363
+ float delay = chapterIndex * uPulseParams.y;
1364
+ float cycleDuration = uPulseParams.x * 2.5;
1365
+ float t = mod(uTime - delay, cycleDuration);
1366
+
1367
+ float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
1368
+ pulse = max(0.0, pulse);
1369
+
1370
+ float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
1371
+
1372
+ vec3 baseColor = color * extinction * horizonFade * scintillation;
1373
+ vColor = baseColor * dimFactor;
1374
+ vColor += vec3(1.0, 0.8, 0.4) * activePulse;
1375
+
973
1376
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
974
1377
  gl_Position = smartProject(mvPosition);
975
1378
  vScreenPos = gl_Position.xy / gl_Position.w;
976
- gl_PointSize = size * pixelRatio * (2000.0 / -mvPosition.z);
1379
+
1380
+ float sizeBoost = 1.0 + activePulse * 0.8;
1381
+ gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
977
1382
  }
978
1383
  `,
979
1384
  fragmentShader: `
980
1385
  varying vec3 vColor;
981
1386
  void main() {
982
1387
  vec2 coord = gl_PointCoord - vec2(0.5);
983
- // Use larger drawing area for glow
984
- float dist = length(coord) * 2.0;
985
- if (dist > 1.0) discard;
1388
+ float d = length(coord) * 2.0;
1389
+ if (d > 1.0) discard;
986
1390
 
987
1391
  float alphaMask = getMaskAlpha();
988
1392
  if (alphaMask < 0.01) discard;
989
1393
 
990
- // Gaussian Glow: Sharp core, soft halo
991
- float alpha = exp(-3.0 * dist * dist);
1394
+ float dd = d * d;
1395
+ // Stellarium Profile
1396
+ float core = exp(-20.0 * dd);
1397
+ float halo = exp(-4.0 * dd);
992
1398
 
993
- gl_FragColor = vec4(vColor, alpha * alphaMask);
1399
+ vec3 cCore = vec3(1.0) * core * 1.5;
1400
+ vec3 cHalo = vColor * halo * 0.6;
1401
+
1402
+ gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
994
1403
  }
995
1404
  `,
996
1405
  transparent: true,
997
1406
  depthWrite: false,
998
- depthTest: true
1407
+ depthTest: true,
1408
+ blending: THREE5__namespace.AdditiveBlending
999
1409
  });
1000
- starPoints = new THREE4__namespace.Points(starGeo, starMat);
1410
+ starPoints = new THREE5__namespace.Points(starGeo, starMat);
1001
1411
  starPoints.frustumCulled = false;
1002
1412
  root.add(starPoints);
1003
1413
  const linePoints = [];
@@ -1023,31 +1433,119 @@ function createEngine({
1023
1433
  }
1024
1434
  }
1025
1435
  if (linePoints.length > 0) {
1026
- const lineGeo = new THREE4__namespace.BufferGeometry();
1027
- lineGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(linePoints, 3));
1436
+ const lineGeo = new THREE5__namespace.BufferGeometry();
1437
+ lineGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(linePoints, 3));
1028
1438
  const lineMat = createSmartMaterial({
1029
- uniforms: { color: { value: new THREE4__namespace.Color(11193599) } },
1439
+ uniforms: { color: { value: new THREE5__namespace.Color(11193599) } },
1030
1440
  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; }`,
1031
1441
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
1032
1442
  transparent: true,
1033
1443
  depthWrite: false,
1034
- blending: THREE4__namespace.AdditiveBlending
1444
+ blending: THREE5__namespace.AdditiveBlending
1035
1445
  });
1036
- constellationLines = new THREE4__namespace.LineSegments(lineGeo, lineMat);
1446
+ constellationLines = new THREE5__namespace.LineSegments(lineGeo, lineMat);
1037
1447
  constellationLines.frustumCulled = false;
1038
1448
  root.add(constellationLines);
1039
1449
  }
1450
+ if (cfg.groups) {
1451
+ for (const [bookId, chapters] of bookMap.entries()) {
1452
+ const bookNode = nodeById.get(bookId);
1453
+ if (!bookNode) continue;
1454
+ const bookName = bookNode.meta?.book || bookNode.label;
1455
+ const groupList = cfg.groups[bookName.toLowerCase()];
1456
+ if (groupList) {
1457
+ groupList.forEach((g, idx) => {
1458
+ const groupId = `G:${bookId}:${idx}`;
1459
+ let p = new THREE5__namespace.Vector3();
1460
+ if (cfg.arrangement && cfg.arrangement[groupId]) {
1461
+ const arr = cfg.arrangement[groupId];
1462
+ p.set(arr.position[0], arr.position[1], arr.position[2]);
1463
+ } else {
1464
+ const relevantChapters = chapters.filter((c) => {
1465
+ const ch = c.meta?.chapter;
1466
+ return ch >= g.start && ch <= g.end;
1467
+ });
1468
+ if (relevantChapters.length === 0) return;
1469
+ for (const c of relevantChapters) {
1470
+ p.add(getPosition(c));
1471
+ }
1472
+ p.divideScalar(relevantChapters.length);
1473
+ }
1474
+ const labelText = `${g.name} (${g.start}-${g.end})`;
1475
+ const texRes = createTextTexture(labelText, "#4fa4fa80");
1476
+ if (texRes) {
1477
+ const baseScale = 0.036;
1478
+ const size = new THREE5__namespace.Vector2(baseScale * texRes.aspect, baseScale);
1479
+ const mat = createSmartMaterial({
1480
+ uniforms: {
1481
+ uMap: { value: texRes.tex },
1482
+ uSize: { value: size },
1483
+ uAlpha: { value: 0 },
1484
+ uAngle: { value: 0 }
1485
+ },
1486
+ vertexShaderBody: `
1487
+ uniform vec2 uSize;
1488
+ uniform float uAngle;
1489
+ varying vec2 vUv;
1490
+ void main() {
1491
+ vUv = uv;
1492
+ vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
1493
+ vec4 projected = smartProject(mvPos);
1494
+
1495
+ float c = cos(uAngle);
1496
+ float s = sin(uAngle);
1497
+ mat2 rot = mat2(c, -s, s, c);
1498
+ vec2 offset = rot * (position.xy * uSize);
1499
+
1500
+ projected.xy += offset / vec2(uAspect, 1.0);
1501
+ gl_Position = projected;
1502
+ }
1503
+ `,
1504
+ fragmentShader: `
1505
+ uniform sampler2D uMap;
1506
+ uniform float uAlpha;
1507
+ varying vec2 vUv;
1508
+ void main() {
1509
+ float mask = getMaskAlpha();
1510
+ if (mask < 0.01) discard;
1511
+ vec4 tex = texture2D(uMap, vUv);
1512
+ gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
1513
+ }
1514
+ `,
1515
+ transparent: true,
1516
+ depthWrite: false,
1517
+ depthTest: true
1518
+ });
1519
+ const mesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), mat);
1520
+ mesh.position.copy(p);
1521
+ mesh.scale.set(size.x, size.y, 1);
1522
+ mesh.frustumCulled = false;
1523
+ mesh.userData = { id: groupId };
1524
+ root.add(mesh);
1525
+ const node = {
1526
+ id: groupId,
1527
+ label: labelText,
1528
+ level: 2.5,
1529
+ // Special Level
1530
+ parent: bookId
1531
+ };
1532
+ dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
1533
+ }
1534
+ });
1535
+ }
1536
+ }
1537
+ }
1040
1538
  const boundaries = laidOut.meta?.divisionBoundaries ?? [];
1041
1539
  if (boundaries.length > 0) {
1042
1540
  const boundaryMat = createSmartMaterial({
1043
- uniforms: { color: { value: new THREE4__namespace.Color(5601177) } },
1541
+ uniforms: { color: { value: new THREE5__namespace.Color(5601177) } },
1044
1542
  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; }`,
1045
1543
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
1046
1544
  transparent: true,
1047
1545
  depthWrite: false,
1048
- blending: THREE4__namespace.AdditiveBlending
1546
+ blending: THREE5__namespace.AdditiveBlending
1049
1547
  });
1050
- const boundaryGeo = new THREE4__namespace.BufferGeometry();
1548
+ const boundaryGeo = new THREE5__namespace.BufferGeometry();
1051
1549
  const bPoints = [];
1052
1550
  boundaries.forEach((angle) => {
1053
1551
  const steps = 32;
@@ -1060,8 +1558,8 @@ function createEngine({
1060
1558
  bPoints.push(p2.x, p2.y, p2.z);
1061
1559
  }
1062
1560
  });
1063
- boundaryGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(bPoints, 3));
1064
- boundaryLines = new THREE4__namespace.LineSegments(boundaryGeo, boundaryMat);
1561
+ boundaryGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(bPoints, 3));
1562
+ boundaryLines = new THREE5__namespace.LineSegments(boundaryGeo, boundaryMat);
1065
1563
  boundaryLines.frustumCulled = false;
1066
1564
  root.add(boundaryLines);
1067
1565
  }
@@ -1080,7 +1578,7 @@ function createEngine({
1080
1578
  const r_norm = Math.sqrt(x * x + y * y);
1081
1579
  const phi = Math.atan2(y, x);
1082
1580
  const theta = r_norm * (Math.PI / 2);
1083
- return new THREE4__namespace.Vector3(
1581
+ return new THREE5__namespace.Vector3(
1084
1582
  Math.sin(theta) * Math.cos(phi),
1085
1583
  Math.cos(theta),
1086
1584
  Math.sin(theta) * Math.sin(phi)
@@ -1093,18 +1591,18 @@ function createEngine({
1093
1591
  }
1094
1592
  }
1095
1593
  if (polyPoints.length > 0) {
1096
- const polyGeo = new THREE4__namespace.BufferGeometry();
1097
- polyGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(polyPoints, 3));
1594
+ const polyGeo = new THREE5__namespace.BufferGeometry();
1595
+ polyGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(polyPoints, 3));
1098
1596
  const polyMat = createSmartMaterial({
1099
- uniforms: { color: { value: new THREE4__namespace.Color(3718648) } },
1597
+ uniforms: { color: { value: new THREE5__namespace.Color(3718648) } },
1100
1598
  // Cyan-ish
1101
1599
  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
1600
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1103
1601
  transparent: true,
1104
1602
  depthWrite: false,
1105
- blending: THREE4__namespace.AdditiveBlending
1603
+ blending: THREE5__namespace.AdditiveBlending
1106
1604
  });
1107
- const polyLines = new THREE4__namespace.LineSegments(polyGeo, polyMat);
1605
+ const polyLines = new THREE5__namespace.LineSegments(polyGeo, polyMat);
1108
1606
  polyLines.frustumCulled = false;
1109
1607
  root.add(polyLines);
1110
1608
  }
@@ -1116,6 +1614,7 @@ function createEngine({
1116
1614
  let lastModel = void 0;
1117
1615
  let lastAppliedLon = void 0;
1118
1616
  let lastAppliedLat = void 0;
1617
+ let lastBackdropCount = void 0;
1119
1618
  function setConfig(cfg) {
1120
1619
  currentConfig = cfg;
1121
1620
  if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
@@ -1128,6 +1627,11 @@ function createEngine({
1128
1627
  state.targetLat = cfg.camera.lat;
1129
1628
  lastAppliedLat = cfg.camera.lat;
1130
1629
  }
1630
+ const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
1631
+ if (lastBackdropCount !== desiredBackdropCount) {
1632
+ createBackdropStars(desiredBackdropCount);
1633
+ lastBackdropCount = desiredBackdropCount;
1634
+ }
1131
1635
  let shouldRebuild = false;
1132
1636
  let model = cfg.model;
1133
1637
  if (!model && cfg.data && cfg.adapter) {
@@ -1151,6 +1655,29 @@ function createEngine({
1151
1655
  } else if (cfg.arrangement && starPoints) {
1152
1656
  if (lastModel) buildFromModel(lastModel, cfg);
1153
1657
  }
1658
+ if (cfg.constellations) {
1659
+ constellationLayer.load(cfg.constellations, (id) => {
1660
+ if (cfg.arrangement && cfg.arrangement[id]) {
1661
+ const arr = cfg.arrangement[id];
1662
+ if (arr.position[2] === 0) {
1663
+ const x = arr.position[0];
1664
+ const y = arr.position[1];
1665
+ const radius = cfg.layout?.radius ?? 2e3;
1666
+ const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
1667
+ const phi = Math.atan2(y, x);
1668
+ const theta = r_norm * (Math.PI / 2);
1669
+ return new THREE5__namespace.Vector3(
1670
+ Math.sin(theta) * Math.cos(phi),
1671
+ Math.cos(theta),
1672
+ Math.sin(theta) * Math.sin(phi)
1673
+ ).multiplyScalar(radius);
1674
+ }
1675
+ return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
1676
+ }
1677
+ const n = nodeById.get(id);
1678
+ return n ? getPosition(n) : null;
1679
+ });
1680
+ }
1154
1681
  }
1155
1682
  function setHandlers(next) {
1156
1683
  handlers = next;
@@ -1170,8 +1697,12 @@ function createEngine({
1170
1697
  }
1171
1698
  }
1172
1699
  for (const item of dynamicLabels) {
1700
+ if (item.node.level === 3) continue;
1173
1701
  arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
1174
1702
  }
1703
+ for (const item of constellationLayer.getItems()) {
1704
+ arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
1705
+ }
1175
1706
  Object.assign(arr, state.tempArrangement);
1176
1707
  return arr;
1177
1708
  }
@@ -1181,16 +1712,18 @@ function createEngine({
1181
1712
  const mY = ev.clientY - rect.top;
1182
1713
  mouseNDC.x = mX / rect.width * 2 - 1;
1183
1714
  mouseNDC.y = -(mY / rect.height) * 2 + 1;
1184
- let closestLabel = null;
1185
- let minLabelDist = 40;
1186
1715
  const uScale = globalUniforms.uScale.value;
1187
1716
  const uAspect = camera.aspect;
1188
1717
  const w = rect.width;
1189
1718
  const h = rect.height;
1719
+ let closestLabel = null;
1720
+ let minLabelDist = 40;
1190
1721
  for (const item of dynamicLabels) {
1191
1722
  if (!item.obj.visible) continue;
1192
1723
  const pWorld = item.obj.position;
1193
1724
  const pProj = smartProjectJS(pWorld);
1725
+ const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1726
+ if (isBehind) continue;
1194
1727
  const xNDC = pProj.x * uScale / uAspect;
1195
1728
  const yNDC = pProj.y * uScale;
1196
1729
  const sX = (xNDC * 0.5 + 0.5) * w;
@@ -1198,24 +1731,72 @@ function createEngine({
1198
1731
  const dx = mX - sX;
1199
1732
  const dy = mY - sY;
1200
1733
  const d = Math.sqrt(dx * dx + dy * dy);
1201
- const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1202
- if (!isBehind && d < minLabelDist) {
1734
+ if (d < minLabelDist) {
1203
1735
  minLabelDist = d;
1204
1736
  closestLabel = item;
1205
1737
  }
1206
1738
  }
1207
- if (closestLabel) return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
1208
- const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1209
- raycaster.ray.origin.set(0, 0, 0);
1210
- raycaster.ray.direction.copy(worldDir);
1211
- raycaster.params.Points.threshold = 5 * (state.fov / 60);
1212
- const hits = raycaster.intersectObject(starPoints, false);
1213
- const pointHit = hits[0];
1214
- if (pointHit && pointHit.index !== void 0) {
1215
- const id = starIndexToId[pointHit.index];
1216
- if (id) {
1217
- const node = nodeById.get(id);
1218
- if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
1739
+ if (closestLabel) {
1740
+ return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
1741
+ }
1742
+ let closestConst = null;
1743
+ let minConstDist = Infinity;
1744
+ for (const item of constellationLayer.getItems()) {
1745
+ if (!item.mesh.visible) continue;
1746
+ const pWorld = item.mesh.position;
1747
+ const pProj = smartProjectJS(pWorld);
1748
+ const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1749
+ if (isBehind) continue;
1750
+ const uniforms = item.material.uniforms;
1751
+ if (!uniforms || !uniforms.uSize) continue;
1752
+ const uSize = uniforms.uSize.value;
1753
+ const uImgAspect = uniforms.uImgAspect.value;
1754
+ const uImgRotation = uniforms.uImgRotation.value;
1755
+ const dist = pWorld.length();
1756
+ if (dist < 1e-3) continue;
1757
+ const scale = uSize / dist * uScale;
1758
+ const halfH_px = scale / 2 * (h / 2);
1759
+ const halfW_px = halfH_px * uImgAspect;
1760
+ const xNDC = pProj.x * uScale / uAspect;
1761
+ const yNDC = pProj.y * uScale;
1762
+ const sX = (xNDC * 0.5 + 0.5) * w;
1763
+ const sY = (-yNDC * 0.5 + 0.5) * h;
1764
+ const dx = mX - sX;
1765
+ const dy = mY - sY;
1766
+ const dy_cart = -dy;
1767
+ const cr = Math.cos(-uImgRotation);
1768
+ const sr = Math.sin(-uImgRotation);
1769
+ const localX = dx * cr - dy_cart * sr;
1770
+ const localY = dx * sr + dy_cart * cr;
1771
+ if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
1772
+ const d = Math.sqrt(dx * dx + dy * dy);
1773
+ if (!closestConst || d < minConstDist) {
1774
+ minConstDist = d;
1775
+ closestConst = item;
1776
+ }
1777
+ }
1778
+ }
1779
+ if (closestConst) {
1780
+ const fakeNode = {
1781
+ id: closestConst.config.id,
1782
+ label: closestConst.config.title,
1783
+ level: -1
1784
+ };
1785
+ return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
1786
+ }
1787
+ if (starPoints) {
1788
+ const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1789
+ raycaster.ray.origin.set(0, 0, 0);
1790
+ raycaster.ray.direction.copy(worldDir);
1791
+ raycaster.params.Points.threshold = 5 * (state.fov / 60);
1792
+ const hits = raycaster.intersectObject(starPoints, false);
1793
+ const pointHit = hits[0];
1794
+ if (pointHit && pointHit.index !== void 0) {
1795
+ const id = starIndexToId[pointHit.index];
1796
+ if (id) {
1797
+ const node = nodeById.get(id);
1798
+ if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
1799
+ }
1219
1800
  }
1220
1801
  }
1221
1802
  return void 0;
@@ -1243,16 +1824,19 @@ function createEngine({
1243
1824
  if (starId) {
1244
1825
  const starNode = nodeById.get(starId);
1245
1826
  if (starNode && starNode.parent === bookId) {
1246
- children.push({ index: i, initialPos: new THREE4__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
1827
+ children.push({ index: i, initialPos: new THREE5__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
1247
1828
  }
1248
1829
  }
1249
1830
  }
1250
1831
  }
1251
1832
  state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
1252
1833
  state.draggedStarIndex = -1;
1834
+ } else if (hit.type === "constellation") {
1835
+ state.draggedGroup = null;
1836
+ state.draggedStarIndex = -1;
1253
1837
  }
1254
- return;
1255
1838
  }
1839
+ return;
1256
1840
  }
1257
1841
  state.dragMode = "camera";
1258
1842
  state.isDragging = true;
@@ -1282,13 +1866,19 @@ function createEngine({
1282
1866
  if (item) {
1283
1867
  item.obj.position.copy(newPos);
1284
1868
  state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
1869
+ } else if (state.draggedNodeId) {
1870
+ const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
1871
+ if (cItem) {
1872
+ cItem.mesh.position.copy(newPos);
1873
+ state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
1874
+ }
1285
1875
  }
1286
1876
  const vStart = group.labelInitialPos.clone().normalize();
1287
1877
  const vEnd = newPos.clone().normalize();
1288
- const q = new THREE4__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
1878
+ const q = new THREE5__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
1289
1879
  if (starPoints && group.children.length > 0) {
1290
1880
  const attr = starPoints.geometry.attributes.position;
1291
- const tempVec = new THREE4__namespace.Vector3();
1881
+ const tempVec = new THREE5__namespace.Vector3();
1292
1882
  for (const child of group.children) {
1293
1883
  tempVec.copy(child.initialPos).applyQuaternion(q);
1294
1884
  attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
@@ -1322,7 +1912,7 @@ function createEngine({
1322
1912
  if (res) {
1323
1913
  hoverLabelMat.uniforms.uMap.value = res.tex;
1324
1914
  const baseScale = 0.03;
1325
- const size = new THREE4__namespace.Vector2(baseScale * res.aspect, baseScale);
1915
+ const size = new THREE5__namespace.Vector2(baseScale * res.aspect, baseScale);
1326
1916
  hoverLabelMat.uniforms.uSize.value = size;
1327
1917
  hoverLabelMesh.scale.set(size.x, size.y, 1);
1328
1918
  }
@@ -1338,6 +1928,7 @@ function createEngine({
1338
1928
  if (hit?.node.id !== handlers._lastHoverId) {
1339
1929
  handlers._lastHoverId = hit?.node.id;
1340
1930
  handlers.onHover?.(hit?.node);
1931
+ constellationLayer.setHovered(hit?.node.id ?? null);
1341
1932
  }
1342
1933
  document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
1343
1934
  }
@@ -1357,7 +1948,14 @@ function createEngine({
1357
1948
  document.body.style.cursor = "default";
1358
1949
  } else {
1359
1950
  const hit = pick(e);
1360
- if (hit) handlers.onSelect?.(hit.node);
1951
+ if (hit) {
1952
+ handlers.onSelect?.(hit.node);
1953
+ constellationLayer.setFocused(hit.node.id);
1954
+ if (hit.node.level === 2) setFocusedBook(hit.node.id);
1955
+ else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
1956
+ } else {
1957
+ setFocusedBook(null);
1958
+ }
1361
1959
  }
1362
1960
  }
1363
1961
  function onWheel(e) {
@@ -1368,25 +1966,26 @@ function createEngine({
1368
1966
  const zoomSpeed = 1e-3 * state.fov;
1369
1967
  state.fov += e.deltaY * zoomSpeed;
1370
1968
  state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
1969
+ handlers.onFovChange?.(state.fov);
1371
1970
  updateUniforms();
1372
1971
  const vAfter = getMouseViewVector(state.fov, aspect);
1373
- const quaternion = new THREE4__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
1972
+ const quaternion = new THREE5__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
1374
1973
  const y = Math.sin(state.lat);
1375
1974
  const r = Math.cos(state.lat);
1376
1975
  const x = r * Math.sin(state.lon);
1377
1976
  const z = -r * Math.cos(state.lon);
1378
- const currentLook = new THREE4__namespace.Vector3(x, y, z);
1977
+ const currentLook = new THREE5__namespace.Vector3(x, y, z);
1379
1978
  const camForward = currentLook.clone().normalize();
1380
1979
  const camUp = camera.up.clone();
1381
- const camRight = new THREE4__namespace.Vector3().crossVectors(camForward, camUp).normalize();
1382
- const camUpOrtho = new THREE4__namespace.Vector3().crossVectors(camRight, camForward).normalize();
1383
- const mat = new THREE4__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
1384
- const qOld = new THREE4__namespace.Quaternion().setFromRotationMatrix(mat);
1980
+ const camRight = new THREE5__namespace.Vector3().crossVectors(camForward, camUp).normalize();
1981
+ const camUpOrtho = new THREE5__namespace.Vector3().crossVectors(camRight, camForward).normalize();
1982
+ const mat = new THREE5__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
1983
+ const qOld = new THREE5__namespace.Quaternion().setFromRotationMatrix(mat);
1385
1984
  const qNew = qOld.clone().multiply(quaternion);
1386
- const newForward = new THREE4__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
1985
+ const newForward = new THREE5__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
1387
1986
  state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
1388
1987
  state.lon = Math.atan2(newForward.x, -newForward.z);
1389
- const newUp = new THREE4__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
1988
+ const newUp = new THREE5__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
1390
1989
  camera.up.copy(newUp);
1391
1990
  if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
1392
1991
  const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
@@ -1427,55 +2026,89 @@ function createEngine({
1427
2026
  function tick() {
1428
2027
  if (!running) return;
1429
2028
  raf = requestAnimationFrame(tick);
1430
- if (!state.isDragging && isMouseInWindow) {
2029
+ const now = performance.now();
2030
+ globalUniforms.uTime.value = now / 1e3;
2031
+ let activeId = null;
2032
+ if (focusedBookId) {
2033
+ activeId = focusedBookId;
2034
+ } else if (hoveredBookId) {
2035
+ const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
2036
+ if (now - lastExit > COOLDOWN_MS) {
2037
+ activeId = hoveredBookId;
2038
+ }
2039
+ }
2040
+ const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
2041
+ orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
2042
+ if (orderRevealStrength > 1e-3 || targetStrength > 0) {
2043
+ if (activeId && bookIdToIndex.has(activeId)) {
2044
+ activeBookIndex = bookIdToIndex.get(activeId);
2045
+ }
2046
+ if (starPoints && starPoints.material) {
2047
+ const m = starPoints.material;
2048
+ if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
2049
+ if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
2050
+ }
2051
+ }
2052
+ let panX = 0;
2053
+ let panY = 0;
2054
+ if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
1431
2055
  const t = ENGINE_CONFIG.edgePanThreshold;
1432
- const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
1433
- let panX = 0;
1434
- let panY = 0;
1435
- if (mouseNDC.x < -1 + t) {
1436
- const s = (-1 + t - mouseNDC.x) / t;
1437
- panX = -s * s * speedBase;
1438
- } else if (mouseNDC.x > 1 - t) {
1439
- const s = (mouseNDC.x - (1 - t)) / t;
1440
- panX = s * s * speedBase;
1441
- }
1442
- if (mouseNDC.y < -1 + t) {
1443
- const s = (-1 + t - mouseNDC.y) / t;
1444
- panY = -s * s * speedBase;
1445
- } else if (mouseNDC.y > 1 - t) {
1446
- const s = (mouseNDC.y - (1 - t)) / t;
1447
- panY = s * s * speedBase;
1448
- }
1449
- if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
1450
- state.lon += panX;
1451
- state.lat += panY;
1452
- state.targetLon = state.lon;
1453
- state.targetLat = state.lat;
2056
+ const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
2057
+ const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
2058
+ if (inZoneX || inZoneY) {
2059
+ if (edgeHoverStart === 0) edgeHoverStart = performance.now();
2060
+ if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
2061
+ const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
2062
+ if (mouseNDC.x < -1 + t) {
2063
+ const s = (-1 + t - mouseNDC.x) / t;
2064
+ panX = -s * s * speedBase;
2065
+ } else if (mouseNDC.x > 1 - t) {
2066
+ const s = (mouseNDC.x - (1 - t)) / t;
2067
+ panX = s * s * speedBase;
2068
+ }
2069
+ if (mouseNDC.y < -1 + t) {
2070
+ const s = (-1 + t - mouseNDC.y) / t;
2071
+ panY = -s * s * speedBase;
2072
+ } else if (mouseNDC.y > 1 - t) {
2073
+ const s = (mouseNDC.y - (1 - t)) / t;
2074
+ panY = s * s * speedBase;
2075
+ }
2076
+ }
1454
2077
  } else {
1455
- state.lon += state.velocityX;
1456
- state.lat += state.velocityY;
1457
- state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1458
- state.velocityY *= ENGINE_CONFIG.inertiaDamping;
1459
- if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
1460
- if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
2078
+ edgeHoverStart = 0;
1461
2079
  }
2080
+ } else {
2081
+ edgeHoverStart = 0;
2082
+ }
2083
+ if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
2084
+ state.lon += panX;
2085
+ state.lat += panY;
2086
+ state.targetLon = state.lon;
2087
+ state.targetLat = state.lat;
1462
2088
  } else if (!state.isDragging) {
1463
2089
  state.lon += state.velocityX;
1464
2090
  state.lat += state.velocityY;
1465
2091
  state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1466
2092
  state.velocityY *= ENGINE_CONFIG.inertiaDamping;
2093
+ if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
2094
+ if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
1467
2095
  }
1468
2096
  state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
1469
2097
  const y = Math.sin(state.lat);
1470
2098
  const r = Math.cos(state.lat);
1471
2099
  const x = r * Math.sin(state.lon);
1472
2100
  const z = -r * Math.cos(state.lon);
1473
- const target = new THREE4__namespace.Vector3(x, y, z);
1474
- const idealUp = new THREE4__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
2101
+ const target = new THREE5__namespace.Vector3(x, y, z);
2102
+ const idealUp = new THREE5__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
1475
2103
  camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
1476
2104
  camera.up.normalize();
1477
2105
  camera.lookAt(target);
2106
+ camera.updateMatrixWorld();
2107
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
1478
2108
  updateUniforms();
2109
+ constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
2110
+ backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
2111
+ if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
1479
2112
  const DIVISION_THRESHOLD = 60;
1480
2113
  const showDivisions = state.fov > DIVISION_THRESHOLD;
1481
2114
  if (constellationLines) {
@@ -1499,7 +2132,9 @@ function createEngine({
1499
2132
  const showBookLabels = currentConfig?.showBookLabels === true;
1500
2133
  const showDivisionLabels = currentConfig?.showDivisionLabels === true;
1501
2134
  const showChapterLabels = currentConfig?.showChapterLabels === true;
1502
- const showChapters = state.fov < 35;
2135
+ const showGroupLabels = currentConfig?.showGroupLabels === true;
2136
+ const showBooks = state.fov < 120;
2137
+ const showChapters = state.fov < 70;
1503
2138
  for (const item of dynamicLabels) {
1504
2139
  const uniforms = item.obj.material.uniforms;
1505
2140
  const level = item.node.level;
@@ -1507,20 +2142,26 @@ function createEngine({
1507
2142
  if (level === 2 && showBookLabels) isEnabled = true;
1508
2143
  else if (level === 1 && showDivisionLabels) isEnabled = true;
1509
2144
  else if (level === 3 && showChapterLabels) isEnabled = true;
2145
+ else if (level === 2.5 && showGroupLabels) isEnabled = true;
1510
2146
  if (!isEnabled) {
1511
- uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2147
+ uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1512
2148
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1513
2149
  continue;
1514
2150
  }
1515
2151
  const pWorld = item.obj.position;
1516
2152
  const pProj = smartProjectJS(pWorld);
1517
2153
  if (pProj.z > 0.2) {
1518
- uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2154
+ uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1519
2155
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1520
2156
  continue;
1521
2157
  }
1522
- if (level === 3 && !showChapters && item.node.id !== state.draggedNodeId) {
1523
- uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2158
+ if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
2159
+ uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2160
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
2161
+ continue;
2162
+ }
2163
+ if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
2164
+ uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1524
2165
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1525
2166
  continue;
1526
2167
  }
@@ -1531,7 +2172,7 @@ function createEngine({
1531
2172
  const size = uniforms.uSize.value;
1532
2173
  const pixelH = size.y * screenH * 0.8;
1533
2174
  const pixelW = size.x * screenH * 0.8;
1534
- labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
2175
+ labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
1535
2176
  }
1536
2177
  const hoverId = handlers._lastHoverId;
1537
2178
  const selectedId = state.draggedNodeId;
@@ -1557,11 +2198,13 @@ function createEngine({
1557
2198
  const dy = l.sY - screenH / 2;
1558
2199
  rot = Math.atan2(-dy, -dx) - Math.PI / 2;
1559
2200
  }
1560
- l.uniforms.uAngle.value = THREE4__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
2201
+ l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
1561
2202
  }
1562
2203
  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 });
2204
+ if (showBooks || isSpecial) {
2205
+ target2 = 1;
2206
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
2207
+ }
1565
2208
  } else if (l.level === 1) {
1566
2209
  if (showDivisions || isSpecial) {
1567
2210
  const pad = -5;
@@ -1570,12 +2213,17 @@ function createEngine({
1570
2213
  occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1571
2214
  }
1572
2215
  }
1573
- } else if (l.level === 3) {
2216
+ } else if (l.level === 2.5 || l.level === 3) {
1574
2217
  if (showChapters || isSpecial) {
1575
2218
  target2 = 1;
2219
+ if (!isSpecial) {
2220
+ const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
2221
+ const focusFade = 1 - THREE5__namespace.MathUtils.smoothstep(0.4, 0.7, dist);
2222
+ target2 *= focusFade;
2223
+ }
1576
2224
  }
1577
2225
  }
1578
- l.uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
2226
+ l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
1579
2227
  l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
1580
2228
  }
1581
2229
  renderer.render(scene, camera);
@@ -1592,16 +2240,31 @@ function createEngine({
1592
2240
  }
1593
2241
  function dispose() {
1594
2242
  stop();
2243
+ constellationLayer.dispose();
1595
2244
  renderer.dispose();
1596
2245
  renderer.domElement.remove();
1597
2246
  }
1598
- return { setConfig, start, stop, dispose, setHandlers, getFullArrangement };
2247
+ function setHoveredBook(id) {
2248
+ if (id === hoveredBookId) return;
2249
+ if (hoveredBookId) {
2250
+ hoverCooldowns.set(hoveredBookId, performance.now());
2251
+ }
2252
+ hoveredBookId = id;
2253
+ }
2254
+ function setFocusedBook(id) {
2255
+ focusedBookId = id;
2256
+ }
2257
+ function setOrderRevealEnabled(enabled) {
2258
+ orderRevealEnabled = enabled;
2259
+ }
2260
+ return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled };
1599
2261
  }
1600
- var ENGINE_CONFIG;
2262
+ var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
1601
2263
  var init_createEngine = __esm({
1602
2264
  "src/engine/createEngine.ts"() {
1603
2265
  init_layout();
1604
2266
  init_materials();
2267
+ init_ConstellationArtworkLayer();
1605
2268
  ENGINE_CONFIG = {
1606
2269
  minFov: 10,
1607
2270
  maxFov: 165,
@@ -1614,16 +2277,26 @@ var init_createEngine = __esm({
1614
2277
  zenithStrength: 0.02,
1615
2278
  horizonLockStrength: 0.05,
1616
2279
  edgePanThreshold: 0.15,
1617
- edgePanMaxSpeed: 0.02
2280
+ edgePanMaxSpeed: 0.02,
2281
+ edgePanDelay: 250
2282
+ };
2283
+ ORDER_REVEAL_CONFIG = {
2284
+ globalDim: 0.85,
2285
+ pulseAmplitude: 0.6,
2286
+ pulseDuration: 2,
2287
+ delayPerChapter: 0.1
1618
2288
  };
1619
2289
  }
1620
2290
  });
1621
2291
  var StarMap = react.forwardRef(
1622
- ({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
2292
+ ({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
1623
2293
  const containerRef = react.useRef(null);
1624
2294
  const engineRef = react.useRef(null);
1625
2295
  react.useImperativeHandle(ref, () => ({
1626
- getFullArrangement: () => engineRef.current?.getFullArrangement?.()
2296
+ getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
2297
+ setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
2298
+ setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
2299
+ setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
1627
2300
  }));
1628
2301
  react.useEffect(() => {
1629
2302
  let disposed = false;
@@ -1635,7 +2308,8 @@ var StarMap = react.forwardRef(
1635
2308
  container: containerRef.current,
1636
2309
  onSelect,
1637
2310
  onHover,
1638
- onArrangementChange
2311
+ onArrangementChange,
2312
+ onFovChange
1639
2313
  });
1640
2314
  engineRef.current.setConfig(config);
1641
2315
  engineRef.current.start();
@@ -1651,8 +2325,8 @@ var StarMap = react.forwardRef(
1651
2325
  engineRef.current?.setConfig?.(config);
1652
2326
  }, [config]);
1653
2327
  react.useEffect(() => {
1654
- engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
1655
- }, [onSelect, onHover, onArrangementChange]);
2328
+ engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
2329
+ }, [onSelect, onHover, onArrangementChange, onFovChange]);
1656
2330
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
1657
2331
  }
1658
2332
  );
@@ -1661,7 +2335,6 @@ var StarMap = react.forwardRef(
1661
2335
  function bibleToSceneModel(data) {
1662
2336
  const nodes = [];
1663
2337
  const links = [];
1664
- let bookCounter = 0;
1665
2338
  const id = {
1666
2339
  testament: (t) => `T:${t}`,
1667
2340
  division: (t, d) => `D:${t}:${d}`,
@@ -1682,8 +2355,7 @@ function bibleToSceneModel(data) {
1682
2355
  });
1683
2356
  links.push({ source: did, target: tid });
1684
2357
  for (const b of d.books) {
1685
- bookCounter++;
1686
- const bookLabel = `${bookCounter}. ${b.name}`;
2358
+ const bookLabel = b.name;
1687
2359
  const bid = id.book(b.key);
1688
2360
  nodes.push({
1689
2361
  id: bid,
@@ -30796,7 +31468,7 @@ var RNG = class {
30796
31468
  const r = Math.sqrt(1 - y * y);
30797
31469
  const x = r * Math.cos(theta);
30798
31470
  const z = r * Math.sin(theta);
30799
- return new THREE4__namespace.Vector3(x, y, z);
31471
+ return new THREE5__namespace.Vector3(x, y, z);
30800
31472
  }
30801
31473
  };
30802
31474
  function simpleNoise3D(v, scale) {
@@ -30834,11 +31506,11 @@ function generateArrangement(bible, options = {}) {
30834
31506
  });
30835
31507
  });
30836
31508
  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();
31509
+ const mwRad = THREE5__namespace.MathUtils.degToRad(opts.milkyWayAngle);
31510
+ const mwNormal = new THREE5__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
30839
31511
  const anchors = [];
30840
31512
  for (let i = 0; i < bookCount; i++) {
30841
- let bestP = new THREE4__namespace.Vector3();
31513
+ let bestP = new THREE5__namespace.Vector3();
30842
31514
  let valid = false;
30843
31515
  let attempt = 0;
30844
31516
  while (!valid && attempt < 100) {
@@ -30864,7 +31536,7 @@ function generateArrangement(bible, options = {}) {
30864
31536
  arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
30865
31537
  for (let c = 0; c < book.chapters; c++) {
30866
31538
  const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
30867
- const offset = new THREE4__namespace.Vector3(
31539
+ const offset = new THREE5__namespace.Vector3(
30868
31540
  (rng.next() - 0.5) * 2,
30869
31541
  (rng.next() - 0.5) * 2,
30870
31542
  (rng.next() - 0.5) * 2
@@ -30885,7 +31557,7 @@ function generateArrangement(bible, options = {}) {
30885
31557
  const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30886
31558
  const divId = `D:${book.testament}:${book.division}`;
30887
31559
  if (!divisions.has(divId)) {
30888
- divisions.set(divId, { sum: new THREE4__namespace.Vector3(), count: 0 });
31560
+ divisions.set(divId, { sum: new THREE5__namespace.Vector3(), count: 0 });
30889
31561
  }
30890
31562
  const entry = divisions.get(divId);
30891
31563
  entry.sum.add(anchorPos);