@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE4 from 'three';
1
+ import * as THREE5 from 'three';
2
2
  import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
@@ -129,14 +129,14 @@ var init_constellations = __esm({
129
129
  });
130
130
  function lookAt(point, target, up) {
131
131
  const zAxis = target.clone().normalize();
132
- let xAxis = new THREE4.Vector3().crossVectors(up, zAxis);
132
+ let xAxis = new THREE5.Vector3().crossVectors(up, zAxis);
133
133
  if (xAxis.lengthSq() < 1e-4) {
134
- xAxis = new THREE4.Vector3().crossVectors(new THREE4.Vector3(1, 0, 0), zAxis);
134
+ xAxis = new THREE5.Vector3().crossVectors(new THREE5.Vector3(1, 0, 0), zAxis);
135
135
  }
136
136
  xAxis.normalize();
137
- const yAxis = new THREE4.Vector3().crossVectors(zAxis, xAxis).normalize();
138
- const m = new THREE4.Matrix4().makeBasis(xAxis, yAxis, zAxis);
139
- const v = new THREE4.Vector3(point.x, point.y, point.z);
137
+ const yAxis = new THREE5.Vector3().crossVectors(zAxis, xAxis).normalize();
138
+ const m = new THREE5.Matrix4().makeBasis(xAxis, yAxis, zAxis);
139
+ const v = new THREE5.Vector3(point.x, point.y, point.z);
140
140
  v.applyMatrix4(m);
141
141
  v.add(target);
142
142
  return { x: v.x, y: v.y, z: v.z };
@@ -217,7 +217,7 @@ function computeLayoutPositions(model, layout) {
217
217
  const radiusAtY = Math.sqrt(1 - y * y);
218
218
  const x = Math.cos(midAngle) * radiusAtY;
219
219
  const z = Math.sin(midAngle) * radiusAtY;
220
- const labelPos = new THREE4.Vector3(x, y, z).multiplyScalar(radius);
220
+ const labelPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
221
221
  uDivision.meta.x = labelPos.x;
222
222
  uDivision.meta.y = labelPos.y;
223
223
  uDivision.meta.z = labelPos.z;
@@ -233,7 +233,7 @@ function computeLayoutPositions(model, layout) {
233
233
  const theta = startAngle + t * angleSpan;
234
234
  const x = Math.cos(theta) * radiusAtY;
235
235
  const z = Math.sin(theta) * radiusAtY;
236
- const bookPos = new THREE4.Vector3(x, y, z).multiplyScalar(radius);
236
+ const bookPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
237
237
  const labelPos = bookPos.clone();
238
238
  labelPos.y += radius * 0.025;
239
239
  labelPos.setLength(radius);
@@ -244,7 +244,7 @@ function computeLayoutPositions(model, layout) {
244
244
  if (chapters.length > 0) {
245
245
  const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
246
246
  const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
247
- const up = new THREE4.Vector3(0, 1, 0);
247
+ const up = new THREE5.Vector3(0, 1, 0);
248
248
  chapters.forEach((chap, idx) => {
249
249
  const uChap = updatedNodeMap.get(chap.id);
250
250
  const lp = localPoints[idx];
@@ -263,10 +263,10 @@ function computeLayoutPositions(model, layout) {
263
263
  testaments.forEach((t) => {
264
264
  const children = childrenMap.get(t.id) ?? [];
265
265
  if (children.length === 0) return;
266
- const centroid = new THREE4.Vector3();
266
+ const centroid = new THREE5.Vector3();
267
267
  children.forEach((c) => {
268
268
  const u = updatedNodeMap.get(c.id);
269
- centroid.add(new THREE4.Vector3(u.meta.x, u.meta.y, u.meta.z));
269
+ centroid.add(new THREE5.Vector3(u.meta.x, u.meta.y, u.meta.z));
270
270
  });
271
271
  centroid.divideScalar(children.length);
272
272
  if (centroid.length() > 0.1) {
@@ -330,11 +330,18 @@ vec4 smartProject(vec4 viewPos) {
330
330
  vec2 projected = vec2(k * dir.x, k * dir.y);
331
331
  projected *= uScale;
332
332
  projected.x /= uAspect;
333
- float zMetric = -1.0 + (dist / 2000.0);
333
+ float zMetric = -1.0 + (dist / 15000.0);
334
+
335
+ // Radial Clipping: Push clipped points off-screen in their natural direction
336
+ // to prevent lines "darting" across the center.
337
+ vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
338
+ vec2 escapePos = escapeDir * 10000.0;
339
+
334
340
  // Clip backward facing points in fisheye mode
335
- if (uBlend > 0.5 && dir.z > 0.4) return vec4(10.0, 10.0, 10.0, 1.0);
341
+ if (uBlend > 0.5 && dir.z > 0.4) return vec4(escapePos, 10.0, 1.0);
336
342
  // Clip very close points in linear mode
337
- if (uBlend < 0.1 && dir.z > -0.1) return vec4(10.0, 10.0, 10.0, 1.0);
343
+ if (uBlend < 0.1 && dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
344
+
338
345
  return vec4(projected, zMetric, 1.0);
339
346
  }
340
347
  `;
@@ -357,7 +364,7 @@ float getMaskAlpha() {
357
364
  });
358
365
  function createSmartMaterial(params) {
359
366
  const uniforms = { ...globalUniforms, ...params.uniforms };
360
- return new THREE4.ShaderMaterial({
367
+ return new THREE5.ShaderMaterial({
361
368
  uniforms,
362
369
  vertexShader: `
363
370
  ${BLEND_CHUNK}
@@ -371,8 +378,8 @@ function createSmartMaterial(params) {
371
378
  transparent: params.transparent || false,
372
379
  depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
373
380
  depthTest: params.depthTest !== void 0 ? params.depthTest : true,
374
- side: params.side || THREE4.FrontSide,
375
- blending: params.blending || THREE4.NormalBlending
381
+ side: params.side || THREE5.FrontSide,
382
+ blending: params.blending || THREE5.NormalBlending
376
383
  });
377
384
  }
378
385
  var globalUniforms;
@@ -382,7 +389,248 @@ var init_materials = __esm({
382
389
  globalUniforms = {
383
390
  uScale: { value: 1 },
384
391
  uAspect: { value: 1 },
385
- uBlend: { value: 0 }
392
+ uBlend: { value: 0 },
393
+ uTime: { value: 0 },
394
+ // Atmosphere Settings
395
+ uAtmGlow: { value: 1 },
396
+ uAtmDark: { value: 0.6 },
397
+ uAtmExtinction: { value: 4 },
398
+ uAtmTwinkle: { value: 0.8 },
399
+ uColorHorizon: { value: new THREE5.Color(2768476) },
400
+ uColorZenith: { value: new THREE5.Color(132104) }
401
+ };
402
+ }
403
+ });
404
+ var ConstellationArtworkLayer;
405
+ var init_ConstellationArtworkLayer = __esm({
406
+ "src/engine/ConstellationArtworkLayer.ts"() {
407
+ init_materials();
408
+ ConstellationArtworkLayer = class {
409
+ root;
410
+ items = [];
411
+ textureLoader = new THREE5.TextureLoader();
412
+ hoveredId = null;
413
+ focusedId = null;
414
+ constructor(root) {
415
+ this.root = new THREE5.Group();
416
+ this.root.renderOrder = -1;
417
+ root.add(this.root);
418
+ }
419
+ getItems() {
420
+ return this.items;
421
+ }
422
+ setPosition(id, pos) {
423
+ const item = this.items.find((i) => i.config.id === id);
424
+ if (item) {
425
+ item.mesh.position.copy(pos);
426
+ }
427
+ }
428
+ load(config, getPosition) {
429
+ this.clear();
430
+ const basePath = config.atlasBasePath.replace(/\/$/, "");
431
+ config.constellations.forEach((c) => {
432
+ let center = new THREE5.Vector3();
433
+ let valid = false;
434
+ let radius = 2e3;
435
+ const arrPos = getPosition(c.id);
436
+ if (arrPos) {
437
+ center.copy(arrPos);
438
+ valid = true;
439
+ if (c.anchors.length > 0) {
440
+ const points = [];
441
+ for (const anchorId of c.anchors) {
442
+ const p = getPosition(anchorId);
443
+ if (p) points.push(p);
444
+ }
445
+ if (points.length > 0) {
446
+ radius = points[0].length();
447
+ }
448
+ }
449
+ } else if (c.center) {
450
+ center.set(c.center[0], c.center[1], c.center[2]);
451
+ valid = true;
452
+ } else if (c.anchors.length > 0) {
453
+ const points = [];
454
+ for (const anchorId of c.anchors) {
455
+ const p = getPosition(anchorId);
456
+ if (p) points.push(p);
457
+ }
458
+ if (points.length > 0) {
459
+ for (const p of points) center.add(p);
460
+ center.divideScalar(points.length);
461
+ const len = center.length();
462
+ if (len > 1e-3) {
463
+ radius = points[0].length();
464
+ center.normalize().multiplyScalar(radius);
465
+ }
466
+ valid = true;
467
+ }
468
+ }
469
+ if (!valid) return;
470
+ const normal = center.clone().normalize().negate();
471
+ const upVec = center.clone().normalize();
472
+ let right = new THREE5.Vector3(1, 0, 0);
473
+ if (c.anchors.length >= 2) {
474
+ const p0 = getPosition(c.anchors[0]);
475
+ const p1 = getPosition(c.anchors[1]);
476
+ if (p0 && p1) {
477
+ const diff = new THREE5.Vector3().subVectors(p1, p0);
478
+ right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
479
+ }
480
+ } else {
481
+ if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
482
+ else right.set(0, 1, 0).cross(upVec).normalize();
483
+ }
484
+ const top = new THREE5.Vector3().crossVectors(upVec, right).normalize();
485
+ right.crossVectors(top, upVec).normalize();
486
+ new THREE5.Matrix4().makeBasis(right, top, normal);
487
+ const geometry = new THREE5.PlaneGeometry(1, 1);
488
+ let size = c.radius;
489
+ if (size <= 1) size *= radius;
490
+ size *= 2;
491
+ const texPath = `${basePath}/${c.image}`;
492
+ let blending = THREE5.NormalBlending;
493
+ if (c.blend === "additive") blending = THREE5.AdditiveBlending;
494
+ const material = createSmartMaterial({
495
+ uniforms: {
496
+ uMap: { value: this.textureLoader.load(texPath) },
497
+ // Placeholder, updated below
498
+ uOpacity: { value: c.opacity },
499
+ uSize: { value: size },
500
+ uImgRotation: { value: THREE5.MathUtils.degToRad(c.rotationDeg) },
501
+ uImgAspect: { value: c.aspectRatio ?? 1 }
502
+ // uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
503
+ },
504
+ vertexShaderBody: `
505
+ uniform float uSize;
506
+ uniform float uImgRotation;
507
+ uniform float uImgAspect;
508
+
509
+ varying vec2 vUv;
510
+
511
+ void main() {
512
+ vUv = uv;
513
+
514
+ // 1. Project Center Point (Proven Method)
515
+ vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
516
+ vec4 clipCenter = smartProject(mvCenter);
517
+
518
+ // 2. Project "Up" Point (World Zenith)
519
+ // Transform World Up (0,1,0) to View Space
520
+ vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
521
+ // Offset center by a significant amount (1000.0) to ensure screen delta
522
+ vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
523
+ vec4 clipUp = smartProject(mvUp);
524
+
525
+ // 3. Calculate Horizon Angle
526
+ vec2 screenCenter = clipCenter.xy / clipCenter.w;
527
+ vec2 screenUp = clipUp.xy / clipUp.w;
528
+ vec2 screenDelta = screenUp - screenCenter;
529
+
530
+ float horizonAngle = 0.0;
531
+ if (length(screenDelta) > 0.001) {
532
+ vec2 screenDir = normalize(screenDelta);
533
+ horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
534
+ }
535
+
536
+ // 4. Combine with User Rotation
537
+ float finalAngle = uImgRotation + horizonAngle;
538
+
539
+ // 5. Billboard Offset
540
+ vec2 offset = position.xy;
541
+
542
+ float cr = cos(finalAngle);
543
+ float sr = sin(finalAngle);
544
+ vec2 rotated = vec2(
545
+ offset.x * cr - offset.y * sr,
546
+ offset.x * sr + offset.y * cr
547
+ );
548
+
549
+ rotated.x *= uImgAspect;
550
+
551
+ float dist = length(mvCenter.xyz);
552
+ float scale = (uSize / dist) * uScale;
553
+
554
+ rotated *= scale;
555
+ rotated.x /= uAspect;
556
+
557
+ gl_Position = clipCenter;
558
+ gl_Position.xy += rotated * clipCenter.w;
559
+
560
+ vScreenPos = gl_Position.xy / gl_Position.w;
561
+ }
562
+ `,
563
+ fragmentShader: `
564
+ uniform sampler2D uMap;
565
+ uniform float uOpacity;
566
+ varying vec2 vUv;
567
+ void main() {
568
+ float mask = getMaskAlpha();
569
+ if (mask < 0.01) discard;
570
+ vec4 tex = texture2D(uMap, vUv);
571
+ gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
572
+ }
573
+ `,
574
+ transparent: true,
575
+ depthWrite: false,
576
+ depthTest: true,
577
+ blending,
578
+ side: THREE5.DoubleSide
579
+ });
580
+ material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
581
+ if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
582
+ const natAspect = tex.image.width / tex.image.height;
583
+ material.uniforms.uImgAspect.value = natAspect;
584
+ }
585
+ });
586
+ if (c.zBias) {
587
+ material.polygonOffset = true;
588
+ material.polygonOffsetFactor = -c.zBias;
589
+ }
590
+ const mesh = new THREE5.Mesh(geometry, material);
591
+ mesh.frustumCulled = false;
592
+ mesh.userData = { id: c.id, type: "constellation" };
593
+ mesh.position.copy(center);
594
+ this.root.add(mesh);
595
+ this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
596
+ });
597
+ }
598
+ update(fov, showArt) {
599
+ this.root.visible = showArt;
600
+ if (!showArt) return;
601
+ for (const item of this.items) {
602
+ const { fade } = item.config;
603
+ let opacity = fade.maxOpacity;
604
+ if (fov >= fade.zoomInStart) {
605
+ opacity = fade.maxOpacity;
606
+ } else if (fov <= fade.zoomInEnd) {
607
+ opacity = fade.minOpacity;
608
+ } else {
609
+ const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
610
+ opacity = THREE5.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
611
+ }
612
+ opacity = Math.min(Math.max(opacity, 0), 1);
613
+ item.material.uniforms.uOpacity.value = opacity;
614
+ }
615
+ }
616
+ setHovered(id) {
617
+ this.hoveredId = id;
618
+ }
619
+ setFocused(id) {
620
+ this.focusedId = id;
621
+ }
622
+ dispose() {
623
+ this.clear();
624
+ this.root.removeFromParent();
625
+ }
626
+ clear() {
627
+ this.items.forEach((i) => {
628
+ this.root.remove(i.mesh);
629
+ i.material.dispose();
630
+ i.mesh.geometry.dispose();
631
+ });
632
+ this.items = [];
633
+ }
386
634
  };
387
635
  }
388
636
  });
@@ -396,17 +644,37 @@ function createEngine({
396
644
  container,
397
645
  onSelect,
398
646
  onHover,
399
- onArrangementChange
647
+ onArrangementChange,
648
+ onFovChange
400
649
  }) {
401
- const renderer = new THREE4.WebGLRenderer({ antialias: true, alpha: false });
650
+ let hoveredBookId = null;
651
+ let focusedBookId = null;
652
+ let orderRevealEnabled = true;
653
+ let activeBookIndex = -1;
654
+ let orderRevealStrength = 0;
655
+ const hoverCooldowns = /* @__PURE__ */ new Map();
656
+ const COOLDOWN_MS = 2e3;
657
+ const bookIdToIndex = /* @__PURE__ */ new Map();
658
+ const renderer = new THREE5.WebGLRenderer({ antialias: true, alpha: false });
402
659
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
403
660
  renderer.setSize(container.clientWidth, container.clientHeight);
404
661
  container.appendChild(renderer.domElement);
405
- const scene = new THREE4.Scene();
406
- scene.background = new THREE4.Color(0);
407
- const camera = new THREE4.PerspectiveCamera(60, 1, 0.1, 3e3);
662
+ const scene = new THREE5.Scene();
663
+ scene.background = new THREE5.Color(0);
664
+ const camera = new THREE5.PerspectiveCamera(60, 1, 0.1, 1e4);
408
665
  camera.position.set(0, 0, 0);
409
666
  camera.up.set(0, 1, 0);
667
+ function setHoveredBook(id) {
668
+ if (id === hoveredBookId) return;
669
+ const now = performance.now();
670
+ if (hoveredBookId) {
671
+ hoverCooldowns.set(hoveredBookId, now);
672
+ }
673
+ if (id) {
674
+ hoverCooldowns.get(id) || 0;
675
+ }
676
+ hoveredBookId = id;
677
+ }
410
678
  let running = false;
411
679
  let raf = 0;
412
680
  const state = {
@@ -427,10 +695,12 @@ function createEngine({
427
695
  draggedGroup: null,
428
696
  tempArrangement: {}
429
697
  };
430
- const mouseNDC = new THREE4.Vector2();
698
+ const mouseNDC = new THREE5.Vector2();
431
699
  let isMouseInWindow = false;
432
- let handlers = { onSelect, onHover, onArrangementChange };
700
+ let edgeHoverStart = 0;
701
+ let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
433
702
  let currentConfig;
703
+ const constellationLayer = new ConstellationArtworkLayer(scene);
434
704
  function mix(a, b, t) {
435
705
  return a * (1 - t) + b * t;
436
706
  }
@@ -465,7 +735,7 @@ function createEngine({
465
735
  const phi = Math.atan2(uvY, uvX);
466
736
  const sinTheta = Math.sin(theta);
467
737
  const cosTheta = Math.cos(theta);
468
- return new THREE4.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
738
+ return new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
469
739
  }
470
740
  function getMouseWorldVector(pixelX, pixelY, width, height) {
471
741
  const aspect = width / height;
@@ -484,7 +754,7 @@ function createEngine({
484
754
  const phi = Math.atan2(uvY, uvX);
485
755
  const sinTheta = Math.sin(theta);
486
756
  const cosTheta = Math.cos(theta);
487
- const vView = new THREE4.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
757
+ const vView = new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
488
758
  return vView.applyQuaternion(camera.quaternion);
489
759
  }
490
760
  function smartProjectJS(worldPos) {
@@ -497,147 +767,187 @@ function createEngine({
497
767
  const k = mix(kLinear, kStereo, blend);
498
768
  return { x: k * dir.x, y: k * dir.y, z: dir.z };
499
769
  }
500
- const groundGroup = new THREE4.Group();
770
+ const groundGroup = new THREE5.Group();
501
771
  scene.add(groundGroup);
502
772
  function createGround() {
503
773
  groundGroup.clear();
504
774
  const radius = 995;
505
- const geometry = new THREE4.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2);
775
+ const geometry = new THREE5.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
506
776
  const material = createSmartMaterial({
507
- uniforms: { color: { value: new THREE4.Color(526862) } },
508
- 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; }`,
509
- 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); }`,
510
- side: THREE4.BackSide,
777
+ uniforms: {
778
+ color: { value: new THREE5.Color(131587) },
779
+ // Very dark almost black
780
+ fogColor: { value: new THREE5.Color(331812) }
781
+ // Matches atmosphere bot color
782
+ },
783
+ vertexShaderBody: `
784
+ varying vec3 vPos;
785
+ varying vec3 vWorldPos;
786
+ void main() {
787
+ vPos = position;
788
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
789
+ gl_Position = smartProject(mvPosition);
790
+ vScreenPos = gl_Position.xy / gl_Position.w;
791
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
792
+ }
793
+ `,
794
+ fragmentShader: `
795
+ uniform vec3 color;
796
+ uniform vec3 fogColor;
797
+ varying vec3 vPos;
798
+ varying vec3 vWorldPos;
799
+
800
+ void main() {
801
+ float alphaMask = getMaskAlpha();
802
+ if (alphaMask < 0.01) discard;
803
+
804
+ // Procedural Horizon (Mountains)
805
+ float angle = atan(vPos.z, vPos.x);
806
+
807
+ // Simple FBM-like terrain
808
+ float h = 0.0;
809
+ h += sin(angle * 6.0) * 20.0;
810
+ h += sin(angle * 13.0 + 1.0) * 10.0;
811
+ h += sin(angle * 29.0 + 2.0) * 5.0;
812
+ h += sin(angle * 63.0 + 4.0) * 2.0;
813
+
814
+ // Base horizon offset (lift slightly)
815
+ float terrainHeight = h + 10.0;
816
+
817
+ if (vPos.y > terrainHeight) discard;
818
+
819
+ // Atmospheric Haze / Fog on the ground
820
+ // Mix ground color with fog color based on vertical height (fade into horizon)
821
+ // Closer to horizon (higher y) -> more fog
822
+ float fogFactor = smoothstep(-100.0, terrainHeight, vPos.y);
823
+ vec3 finalCol = mix(color, fogColor, fogFactor * 0.5);
824
+
825
+ gl_FragColor = vec4(finalCol, 1.0);
826
+ }
827
+ `,
828
+ side: THREE5.BackSide,
511
829
  transparent: false,
512
830
  depthWrite: true,
513
831
  depthTest: true
514
832
  });
515
- const ground = new THREE4.Mesh(geometry, material);
833
+ const ground = new THREE5.Mesh(geometry, material);
516
834
  groundGroup.add(ground);
517
- const boxGeo = new THREE4.BoxGeometry(8, 30, 8);
518
- for (let i = 0; i < 12; i++) {
519
- const angle = i / 12 * Math.PI * 2;
520
- const b = new THREE4.Mesh(boxGeo, material);
521
- const r = radius * 0.98;
522
- b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
523
- b.lookAt(0, 0, 0);
524
- groundGroup.add(b);
525
- }
526
835
  }
836
+ let atmosphereMesh = null;
527
837
  function createAtmosphere() {
528
- const geometry = new THREE4.SphereGeometry(990, 128, 64);
838
+ const geometry = new THREE5.SphereGeometry(990, 64, 64);
529
839
  const material = createSmartMaterial({
530
- uniforms: { top: { value: new THREE4.Color(0) }, bot: { value: new THREE4.Color(1712172) } },
531
840
  vertexShaderBody: `
532
-
533
- varying vec3 vP;
534
-
535
- void main() {
536
-
537
- vP = position;
538
-
539
- vec4 mv = modelViewMatrix * vec4(position, 1.0);
540
-
541
- gl_Position = smartProject(mv);
542
-
543
- vScreenPos = gl_Position.xy / gl_Position.w;
544
-
545
- }
546
-
547
- `,
841
+ varying vec3 vWorldNormal;
842
+ void main() {
843
+ vWorldNormal = normalize(position);
844
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
845
+ gl_Position = smartProject(mv);
846
+ vScreenPos = gl_Position.xy / gl_Position.w;
847
+ }`,
548
848
  fragmentShader: `
549
-
550
- uniform vec3 top;
551
-
552
- uniform vec3 bot;
553
-
554
- varying vec3 vP;
555
-
556
- void main() {
557
-
558
- float alphaMask = getMaskAlpha();
559
-
560
- if (alphaMask < 0.01) discard;
561
-
562
- vec3 n = normalize(vP);
563
-
564
- float h = max(0.0, n.y);
565
-
566
- gl_FragColor = vec4(mix(bot, top, pow(h, 0.6)), 1.0);
567
-
568
- }
569
-
570
- `,
571
- side: THREE4.BackSide,
849
+ varying vec3 vWorldNormal;
850
+
851
+ uniform float uAtmGlow;
852
+ uniform float uAtmDark;
853
+ uniform vec3 uColorHorizon;
854
+ uniform vec3 uColorZenith;
855
+
856
+ void main() {
857
+ float alphaMask = getMaskAlpha();
858
+ if (alphaMask < 0.01) discard;
859
+
860
+ // Altitude angle (Y is up)
861
+ float h = normalize(vWorldNormal).y;
862
+
863
+ // Gradient Logic
864
+ // 1. Base gradient from Horizon to Zenith
865
+ float t = smoothstep(-0.1, 0.5, h);
866
+
867
+ // Non-linear mix for realistic sky falloff
868
+ // Zenith darkness adjustment
869
+ vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
870
+
871
+ // 2. Horizon Glow Band (Simulate scattering/haze layer)
872
+ float horizonBand = exp(-15.0 * abs(h - 0.02)); // Sharp peak near 0
873
+ skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
874
+
875
+ gl_FragColor = vec4(skyColor, 1.0);
876
+ }
877
+ `,
878
+ side: THREE5.BackSide,
572
879
  depthWrite: false,
573
880
  depthTest: true
574
881
  });
575
- const atm = new THREE4.Mesh(geometry, material);
882
+ const atm = new THREE5.Mesh(geometry, material);
883
+ atmosphereMesh = atm;
576
884
  groundGroup.add(atm);
577
885
  }
578
- const backdropGroup = new THREE4.Group();
886
+ const backdropGroup = new THREE5.Group();
579
887
  scene.add(backdropGroup);
580
- function createBackdropStars() {
888
+ function createBackdropStars(count = 31e3) {
581
889
  backdropGroup.clear();
582
- const geometry = new THREE4.BufferGeometry();
890
+ while (backdropGroup.children.length > 0) {
891
+ const c = backdropGroup.children[0];
892
+ backdropGroup.remove(c);
893
+ if (c.geometry) c.geometry.dispose();
894
+ if (c.material) c.material.dispose();
895
+ }
896
+ const geometry = new THREE5.BufferGeometry();
583
897
  const positions = [];
584
898
  const sizes = [];
585
899
  const colors = [];
586
- const colorPalette = [
587
- new THREE4.Color(10203391),
588
- new THREE4.Color(11190271),
589
- new THREE4.Color(13293567),
590
- new THREE4.Color(16316415),
591
- new THREE4.Color(16774378),
592
- new THREE4.Color(16765601),
593
- new THREE4.Color(16764015)
594
- ];
595
900
  const r = 2500;
596
- new THREE4.Vector3(0, 1, 0.5).normalize();
597
- for (let i = 0; i < 4e3; i++) {
598
- const isMilkyWay = Math.random() < 0.4;
599
- let x, y, z;
600
- if (isMilkyWay) {
601
- const theta = Math.random() * Math.PI * 2;
602
- const scatter = (Math.random() - 0.5) * 0.4;
603
- const v = new THREE4.Vector3(Math.cos(theta), scatter, Math.sin(theta));
604
- v.normalize();
605
- v.applyAxisAngle(new THREE4.Vector3(1, 0, 0), THREE4.MathUtils.degToRad(60));
606
- x = v.x * r;
607
- y = v.y * r;
608
- z = v.z * r;
609
- } else {
610
- const u = Math.random();
611
- const v = Math.random();
612
- const theta = 2 * Math.PI * u;
613
- const phi = Math.acos(2 * v - 1);
614
- x = r * Math.sin(phi) * Math.cos(theta);
615
- y = r * Math.sin(phi) * Math.sin(theta);
616
- z = r * Math.cos(phi);
617
- }
901
+ for (let i = 0; i < count; i++) {
902
+ const u = Math.random();
903
+ const v = Math.random();
904
+ const theta = 2 * Math.PI * u;
905
+ const phi = Math.acos(2 * v - 1);
906
+ const x = r * Math.sin(phi) * Math.cos(theta);
907
+ const y = r * Math.cos(phi);
908
+ const z = r * Math.sin(phi) * Math.sin(theta);
618
909
  positions.push(x, y, z);
619
- const size = 0.5 + -Math.log(Math.random()) * 0.8 * 1.5;
910
+ const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
620
911
  sizes.push(size);
621
- const cIndex = Math.floor(Math.random() * colorPalette.length);
622
- const c = colorPalette[cIndex];
623
- colors.push(c.r, c.g, c.b);
912
+ colors.push(1, 1, 1);
624
913
  }
625
- geometry.setAttribute("position", new THREE4.Float32BufferAttribute(positions, 3));
626
- geometry.setAttribute("size", new THREE4.Float32BufferAttribute(sizes, 1));
627
- geometry.setAttribute("color", new THREE4.Float32BufferAttribute(colors, 3));
914
+ geometry.setAttribute("position", new THREE5.Float32BufferAttribute(positions, 3));
915
+ geometry.setAttribute("size", new THREE5.Float32BufferAttribute(sizes, 1));
916
+ geometry.setAttribute("color", new THREE5.Float32BufferAttribute(colors, 3));
628
917
  const material = createSmartMaterial({
629
- uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
918
+ uniforms: {
919
+ pixelRatio: { value: renderer.getPixelRatio() },
920
+ uScale: globalUniforms.uScale
921
+ },
630
922
  vertexShaderBody: `
631
923
  attribute float size;
632
924
  attribute vec3 color;
633
925
  varying vec3 vColor;
634
926
  uniform float pixelRatio;
927
+
928
+ uniform float uAtmExtinction;
929
+
635
930
  void main() {
636
- vColor = color;
931
+ vec3 nPos = normalize(position);
932
+ float altitude = nPos.y;
933
+
934
+ // Simple Extinction & Horizon Fade
935
+ float horizonFade = smoothstep(-0.1, 0.1, altitude);
936
+ float airmass = 1.0 / (max(0.05, altitude + 0.05));
937
+ float extinction = exp(-uAtmExtinction * 0.15 * airmass);
938
+
939
+ // Boost intensity significantly (3.0x)
940
+ vColor = color * 3.0 * extinction * horizonFade;
941
+
637
942
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
638
943
  gl_Position = smartProject(mvPosition);
639
944
  vScreenPos = gl_Position.xy / gl_Position.w;
640
- gl_PointSize = size * pixelRatio * (600.0 / -mvPosition.z);
945
+
946
+ // Non-linear scale with zoom to keep stars looking like points
947
+ // pow(uScale, 0.5) prevents them from getting too large at low FOV
948
+ float zoomScale = pow(uScale, 0.5);
949
+
950
+ gl_PointSize = size * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade;
641
951
  }
642
952
  `,
643
953
  fragmentShader: `
@@ -648,26 +958,28 @@ function createEngine({
648
958
  if (dist > 1.0) discard;
649
959
  float alphaMask = getMaskAlpha();
650
960
  if (alphaMask < 0.01) discard;
651
- // Use same Gaussian glow for backdrop
652
- float alpha = exp(-3.0 * dist * dist);
961
+
962
+ // Sharp falloff for intense point look
963
+ float alpha = exp(-4.0 * dist * dist);
653
964
  gl_FragColor = vec4(vColor, alpha * alphaMask);
654
965
  }
655
966
  `,
656
967
  transparent: true,
657
968
  depthWrite: false,
658
- depthTest: true
969
+ depthTest: true,
970
+ blending: THREE5.AdditiveBlending
659
971
  });
660
- const points = new THREE4.Points(geometry, material);
972
+ const points = new THREE5.Points(geometry, material);
661
973
  points.frustumCulled = false;
662
974
  backdropGroup.add(points);
663
975
  }
664
976
  createGround();
665
977
  createAtmosphere();
666
978
  createBackdropStars();
667
- const raycaster = new THREE4.Raycaster();
979
+ const raycaster = new THREE5.Raycaster();
668
980
  raycaster.params.Points.threshold = 5;
669
- new THREE4.Vector2();
670
- const root = new THREE4.Group();
981
+ new THREE5.Vector2();
982
+ const root = new THREE5.Group();
671
983
  scene.add(root);
672
984
  const nodeById = /* @__PURE__ */ new Map();
673
985
  const starIndexToId = [];
@@ -675,7 +987,7 @@ function createEngine({
675
987
  const hoverLabelMat = createSmartMaterial({
676
988
  uniforms: {
677
989
  uMap: { value: null },
678
- uSize: { value: new THREE4.Vector2(1, 1) },
990
+ uSize: { value: new THREE5.Vector2(1, 1) },
679
991
  uAlpha: { value: 0 },
680
992
  uAngle: { value: 0 }
681
993
  },
@@ -713,7 +1025,7 @@ function createEngine({
713
1025
  depthTest: false
714
1026
  // Always on top of stars
715
1027
  });
716
- const hoverLabelMesh = new THREE4.Mesh(new THREE4.PlaneGeometry(1, 1), hoverLabelMat);
1028
+ const hoverLabelMesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), hoverLabelMat);
717
1029
  hoverLabelMesh.visible = false;
718
1030
  hoverLabelMesh.renderOrder = 999;
719
1031
  hoverLabelMesh.frustumCulled = false;
@@ -744,19 +1056,20 @@ function createEngine({
744
1056
  const ctx = canvas.getContext("2d");
745
1057
  if (!ctx) return null;
746
1058
  const fontSize = 96;
747
- ctx.font = `bold ${fontSize}px sans-serif`;
1059
+ const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
1060
+ ctx.font = font;
748
1061
  const metrics = ctx.measureText(text);
749
1062
  const w = Math.ceil(metrics.width);
750
1063
  const h = Math.ceil(fontSize * 1.2);
751
1064
  canvas.width = w;
752
1065
  canvas.height = h;
753
- ctx.font = `bold ${fontSize}px sans-serif`;
1066
+ ctx.font = font;
754
1067
  ctx.fillStyle = color;
755
1068
  ctx.textAlign = "center";
756
1069
  ctx.textBaseline = "middle";
757
1070
  ctx.fillText(text, w / 2, h / 2);
758
- const tex = new THREE4.CanvasTexture(canvas);
759
- tex.minFilter = THREE4.LinearFilter;
1071
+ const tex = new THREE5.CanvasTexture(canvas);
1072
+ tex.minFilter = THREE5.LinearFilter;
760
1073
  return { tex, aspect: w / h };
761
1074
  }
762
1075
  function getPosition(n) {
@@ -770,27 +1083,28 @@ function createEngine({
770
1083
  const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
771
1084
  const phi = Math.atan2(y, x);
772
1085
  const theta = r_norm * (Math.PI / 2);
773
- return new THREE4.Vector3(
1086
+ return new THREE5.Vector3(
774
1087
  Math.sin(theta) * Math.cos(phi),
775
1088
  Math.cos(theta),
776
1089
  Math.sin(theta) * Math.sin(phi)
777
1090
  ).multiplyScalar(radius);
778
1091
  }
779
- return new THREE4.Vector3(arr.position[0], arr.position[1], arr.position[2]);
1092
+ return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
780
1093
  }
781
1094
  }
782
- return new THREE4.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
1095
+ return new THREE5.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
783
1096
  }
784
1097
  function getBoundaryPoint(angle, t, radius) {
785
1098
  const y = 0.05 + t * (1 - 0.05);
786
1099
  const rY = Math.sqrt(1 - y * y);
787
1100
  const x = Math.cos(angle) * rY;
788
1101
  const z = Math.sin(angle) * rY;
789
- return new THREE4.Vector3(x, y, z).multiplyScalar(radius);
1102
+ return new THREE5.Vector3(x, y, z).multiplyScalar(radius);
790
1103
  }
791
1104
  function buildFromModel(model, cfg) {
792
1105
  clearRoot();
793
- scene.background = cfg.background && cfg.background !== "transparent" ? new THREE4.Color(cfg.background) : new THREE4.Color(0);
1106
+ bookIdToIndex.clear();
1107
+ scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5.Color(cfg.background) : new THREE5.Color(0);
794
1108
  const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
795
1109
  const laidOut = computeLayoutPositions(model, layoutCfg);
796
1110
  const divisionPositions = /* @__PURE__ */ new Map();
@@ -804,7 +1118,7 @@ function createEngine({
804
1118
  }
805
1119
  }
806
1120
  for (const [divId, books] of divMap.entries()) {
807
- const centroid = new THREE4.Vector3();
1121
+ const centroid = new THREE5.Vector3();
808
1122
  let count = 0;
809
1123
  for (const b of books) {
810
1124
  const p = getPosition(b);
@@ -820,21 +1134,24 @@ function createEngine({
820
1134
  const starPositions = [];
821
1135
  const starSizes = [];
822
1136
  const starColors = [];
1137
+ const starPhases = [];
1138
+ const starBookIndices = [];
1139
+ const starChapterIndices = [];
823
1140
  const SPECTRAL_COLORS = [
824
- new THREE4.Color(10203391),
825
- // O - Blue
826
- new THREE4.Color(11190271),
827
- // B - Blue-white
828
- new THREE4.Color(13293567),
829
- // A - White-blue
830
- new THREE4.Color(16316415),
1141
+ new THREE5.Color(14544639),
1142
+ // O - Blueish White
1143
+ new THREE5.Color(15660287),
1144
+ // B - White
1145
+ new THREE5.Color(16317695),
1146
+ // A - White
1147
+ new THREE5.Color(16777208),
831
1148
  // F - White
832
- new THREE4.Color(16774378),
833
- // G - Yellow-white
834
- new THREE4.Color(16765601),
835
- // K - Yellow-orange
836
- new THREE4.Color(16764015)
837
- // M - Orange-red
1149
+ new THREE5.Color(16775406),
1150
+ // G - Yellowish White
1151
+ new THREE5.Color(16773085),
1152
+ // K - Pale Orange
1153
+ new THREE5.Color(16771788)
1154
+ // M - Light Orange
838
1155
  ];
839
1156
  let minWeight = Infinity;
840
1157
  let maxWeight = -Infinity;
@@ -859,21 +1176,41 @@ function createEngine({
859
1176
  let baseSize = 3.5;
860
1177
  if (typeof n.weight === "number") {
861
1178
  const t = (n.weight - minWeight) / (maxWeight - minWeight);
862
- baseSize = 3 + t * 4;
1179
+ baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
863
1180
  }
864
1181
  starSizes.push(baseSize);
865
1182
  const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
866
1183
  const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
867
1184
  starColors.push(c.r, c.g, c.b);
1185
+ starPhases.push(Math.random() * Math.PI * 2);
1186
+ let bIdx = -1;
1187
+ if (n.parent) {
1188
+ if (!bookIdToIndex.has(n.parent)) {
1189
+ bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
1190
+ }
1191
+ bIdx = bookIdToIndex.get(n.parent);
1192
+ }
1193
+ starBookIndices.push(bIdx);
1194
+ let cIdx = 0;
1195
+ if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
1196
+ starChapterIndices.push(cIdx);
868
1197
  }
869
1198
  if (n.level === 1 || n.level === 2 || n.level === 3) {
870
- const color = n.level === 1 ? "#38bdf8" : "#ffffff";
871
- const texRes = createTextTexture(n.label, color);
1199
+ let color = "#ffffff";
1200
+ if (n.level === 1) color = "#38bdf8";
1201
+ else if (n.level === 2) color = "#cbd5e1";
1202
+ else if (n.level === 3) color = "#94a3b8";
1203
+ let labelText = n.label;
1204
+ if (n.level === 3 && n.meta?.chapter) {
1205
+ labelText = String(n.meta.chapter);
1206
+ }
1207
+ const texRes = createTextTexture(labelText, color);
872
1208
  if (texRes) {
873
1209
  let baseScale = 0.05;
874
1210
  if (n.level === 1) baseScale = 0.08;
875
- else if (n.level === 3) baseScale = 0.04;
876
- const size = new THREE4.Vector2(baseScale * texRes.aspect, baseScale);
1211
+ else if (n.level === 2) baseScale = 0.04;
1212
+ else if (n.level === 3) baseScale = 0.03;
1213
+ const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
877
1214
  const mat = createSmartMaterial({
878
1215
  uniforms: {
879
1216
  uMap: { value: texRes.tex },
@@ -914,7 +1251,7 @@ function createEngine({
914
1251
  depthWrite: false,
915
1252
  depthTest: true
916
1253
  });
917
- const mesh = new THREE4.Mesh(new THREE4.PlaneGeometry(1, 1), mat);
1254
+ const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
918
1255
  let p = getPosition(n);
919
1256
  if (n.level === 1) {
920
1257
  if (divisionPositions.has(n.id)) {
@@ -924,7 +1261,8 @@ function createEngine({
924
1261
  const angle = Math.atan2(p.z, p.x);
925
1262
  p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
926
1263
  } else if (n.level === 3) {
927
- p.multiplyScalar(1.002);
1264
+ p.y += 30;
1265
+ p.multiplyScalar(1.001);
928
1266
  }
929
1267
  mesh.position.set(p.x, p.y, p.z);
930
1268
  mesh.scale.set(size.x, size.y, 1);
@@ -935,47 +1273,119 @@ function createEngine({
935
1273
  }
936
1274
  }
937
1275
  }
938
- const starGeo = new THREE4.BufferGeometry();
939
- starGeo.setAttribute("position", new THREE4.Float32BufferAttribute(starPositions, 3));
940
- starGeo.setAttribute("size", new THREE4.Float32BufferAttribute(starSizes, 1));
941
- starGeo.setAttribute("color", new THREE4.Float32BufferAttribute(starColors, 3));
1276
+ const starGeo = new THREE5.BufferGeometry();
1277
+ starGeo.setAttribute("position", new THREE5.Float32BufferAttribute(starPositions, 3));
1278
+ starGeo.setAttribute("size", new THREE5.Float32BufferAttribute(starSizes, 1));
1279
+ starGeo.setAttribute("color", new THREE5.Float32BufferAttribute(starColors, 3));
1280
+ starGeo.setAttribute("phase", new THREE5.Float32BufferAttribute(starPhases, 1));
1281
+ starGeo.setAttribute("bookIndex", new THREE5.Float32BufferAttribute(starBookIndices, 1));
1282
+ starGeo.setAttribute("chapterIndex", new THREE5.Float32BufferAttribute(starChapterIndices, 1));
942
1283
  const starMat = createSmartMaterial({
943
- uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
1284
+ uniforms: {
1285
+ pixelRatio: { value: renderer.getPixelRatio() },
1286
+ uScale: globalUniforms.uScale,
1287
+ uTime: globalUniforms.uTime,
1288
+ uActiveBookIndex: { value: -1 },
1289
+ uOrderRevealStrength: { value: 0 },
1290
+ uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
1291
+ uPulseParams: { value: new THREE5.Vector3(
1292
+ ORDER_REVEAL_CONFIG.pulseDuration,
1293
+ ORDER_REVEAL_CONFIG.delayPerChapter,
1294
+ ORDER_REVEAL_CONFIG.pulseAmplitude
1295
+ ) }
1296
+ },
944
1297
  vertexShaderBody: `
945
1298
  attribute float size;
946
1299
  attribute vec3 color;
1300
+ attribute float phase;
1301
+ attribute float bookIndex;
1302
+ attribute float chapterIndex;
1303
+
947
1304
  varying vec3 vColor;
948
1305
  uniform float pixelRatio;
1306
+
1307
+ uniform float uTime;
1308
+ uniform float uAtmExtinction;
1309
+ uniform float uAtmTwinkle;
1310
+
1311
+ uniform float uActiveBookIndex;
1312
+ uniform float uOrderRevealStrength;
1313
+ uniform float uGlobalDimFactor;
1314
+ uniform vec3 uPulseParams;
1315
+
949
1316
  void main() {
950
- vColor = color;
1317
+ vec3 nPos = normalize(position);
1318
+
1319
+ // 1. Altitude (Y is UP)
1320
+ float altitude = nPos.y;
1321
+
1322
+ // 2. Atmospheric Extinction (Airmass approximation)
1323
+ float airmass = 1.0 / (max(0.02, altitude + 0.05));
1324
+ float extinction = exp(-uAtmExtinction * 0.1 * airmass);
1325
+
1326
+ // Fade out stars below horizon
1327
+ float horizonFade = smoothstep(-0.1, 0.05, altitude);
1328
+
1329
+ // 3. Scintillation
1330
+ float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
1331
+ float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
1332
+ float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
1333
+
1334
+ // --- Order Reveal Logic ---
1335
+ float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
1336
+
1337
+ // Dimming
1338
+ float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
1339
+
1340
+ // Pulse
1341
+ float delay = chapterIndex * uPulseParams.y;
1342
+ float cycleDuration = uPulseParams.x * 2.5;
1343
+ float t = mod(uTime - delay, cycleDuration);
1344
+
1345
+ float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
1346
+ pulse = max(0.0, pulse);
1347
+
1348
+ float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
1349
+
1350
+ vec3 baseColor = color * extinction * horizonFade * scintillation;
1351
+ vColor = baseColor * dimFactor;
1352
+ vColor += vec3(1.0, 0.8, 0.4) * activePulse;
1353
+
951
1354
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
952
1355
  gl_Position = smartProject(mvPosition);
953
1356
  vScreenPos = gl_Position.xy / gl_Position.w;
954
- gl_PointSize = size * pixelRatio * (2000.0 / -mvPosition.z);
1357
+
1358
+ float sizeBoost = 1.0 + activePulse * 0.8;
1359
+ gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
955
1360
  }
956
1361
  `,
957
1362
  fragmentShader: `
958
1363
  varying vec3 vColor;
959
1364
  void main() {
960
1365
  vec2 coord = gl_PointCoord - vec2(0.5);
961
- // Use larger drawing area for glow
962
- float dist = length(coord) * 2.0;
963
- if (dist > 1.0) discard;
1366
+ float d = length(coord) * 2.0;
1367
+ if (d > 1.0) discard;
964
1368
 
965
1369
  float alphaMask = getMaskAlpha();
966
1370
  if (alphaMask < 0.01) discard;
967
1371
 
968
- // Gaussian Glow: Sharp core, soft halo
969
- float alpha = exp(-3.0 * dist * dist);
1372
+ float dd = d * d;
1373
+ // Stellarium Profile
1374
+ float core = exp(-20.0 * dd);
1375
+ float halo = exp(-4.0 * dd);
970
1376
 
971
- gl_FragColor = vec4(vColor, alpha * alphaMask);
1377
+ vec3 cCore = vec3(1.0) * core * 1.5;
1378
+ vec3 cHalo = vColor * halo * 0.6;
1379
+
1380
+ gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
972
1381
  }
973
1382
  `,
974
1383
  transparent: true,
975
1384
  depthWrite: false,
976
- depthTest: true
1385
+ depthTest: true,
1386
+ blending: THREE5.AdditiveBlending
977
1387
  });
978
- starPoints = new THREE4.Points(starGeo, starMat);
1388
+ starPoints = new THREE5.Points(starGeo, starMat);
979
1389
  starPoints.frustumCulled = false;
980
1390
  root.add(starPoints);
981
1391
  const linePoints = [];
@@ -1001,31 +1411,119 @@ function createEngine({
1001
1411
  }
1002
1412
  }
1003
1413
  if (linePoints.length > 0) {
1004
- const lineGeo = new THREE4.BufferGeometry();
1005
- lineGeo.setAttribute("position", new THREE4.Float32BufferAttribute(linePoints, 3));
1414
+ const lineGeo = new THREE5.BufferGeometry();
1415
+ lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(linePoints, 3));
1006
1416
  const lineMat = createSmartMaterial({
1007
- uniforms: { color: { value: new THREE4.Color(11193599) } },
1417
+ uniforms: { color: { value: new THREE5.Color(11193599) } },
1008
1418
  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; }`,
1009
1419
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
1010
1420
  transparent: true,
1011
1421
  depthWrite: false,
1012
- blending: THREE4.AdditiveBlending
1422
+ blending: THREE5.AdditiveBlending
1013
1423
  });
1014
- constellationLines = new THREE4.LineSegments(lineGeo, lineMat);
1424
+ constellationLines = new THREE5.LineSegments(lineGeo, lineMat);
1015
1425
  constellationLines.frustumCulled = false;
1016
1426
  root.add(constellationLines);
1017
1427
  }
1428
+ if (cfg.groups) {
1429
+ for (const [bookId, chapters] of bookMap.entries()) {
1430
+ const bookNode = nodeById.get(bookId);
1431
+ if (!bookNode) continue;
1432
+ const bookName = bookNode.meta?.book || bookNode.label;
1433
+ const groupList = cfg.groups[bookName.toLowerCase()];
1434
+ if (groupList) {
1435
+ groupList.forEach((g, idx) => {
1436
+ const groupId = `G:${bookId}:${idx}`;
1437
+ let p = new THREE5.Vector3();
1438
+ if (cfg.arrangement && cfg.arrangement[groupId]) {
1439
+ const arr = cfg.arrangement[groupId];
1440
+ p.set(arr.position[0], arr.position[1], arr.position[2]);
1441
+ } else {
1442
+ const relevantChapters = chapters.filter((c) => {
1443
+ const ch = c.meta?.chapter;
1444
+ return ch >= g.start && ch <= g.end;
1445
+ });
1446
+ if (relevantChapters.length === 0) return;
1447
+ for (const c of relevantChapters) {
1448
+ p.add(getPosition(c));
1449
+ }
1450
+ p.divideScalar(relevantChapters.length);
1451
+ }
1452
+ const labelText = `${g.name} (${g.start}-${g.end})`;
1453
+ const texRes = createTextTexture(labelText, "#4fa4fa80");
1454
+ if (texRes) {
1455
+ const baseScale = 0.036;
1456
+ const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
1457
+ const mat = createSmartMaterial({
1458
+ uniforms: {
1459
+ uMap: { value: texRes.tex },
1460
+ uSize: { value: size },
1461
+ uAlpha: { value: 0 },
1462
+ uAngle: { value: 0 }
1463
+ },
1464
+ vertexShaderBody: `
1465
+ uniform vec2 uSize;
1466
+ uniform float uAngle;
1467
+ varying vec2 vUv;
1468
+ void main() {
1469
+ vUv = uv;
1470
+ vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
1471
+ vec4 projected = smartProject(mvPos);
1472
+
1473
+ float c = cos(uAngle);
1474
+ float s = sin(uAngle);
1475
+ mat2 rot = mat2(c, -s, s, c);
1476
+ vec2 offset = rot * (position.xy * uSize);
1477
+
1478
+ projected.xy += offset / vec2(uAspect, 1.0);
1479
+ gl_Position = projected;
1480
+ }
1481
+ `,
1482
+ fragmentShader: `
1483
+ uniform sampler2D uMap;
1484
+ uniform float uAlpha;
1485
+ varying vec2 vUv;
1486
+ void main() {
1487
+ float mask = getMaskAlpha();
1488
+ if (mask < 0.01) discard;
1489
+ vec4 tex = texture2D(uMap, vUv);
1490
+ gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
1491
+ }
1492
+ `,
1493
+ transparent: true,
1494
+ depthWrite: false,
1495
+ depthTest: true
1496
+ });
1497
+ const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
1498
+ mesh.position.copy(p);
1499
+ mesh.scale.set(size.x, size.y, 1);
1500
+ mesh.frustumCulled = false;
1501
+ mesh.userData = { id: groupId };
1502
+ root.add(mesh);
1503
+ const node = {
1504
+ id: groupId,
1505
+ label: labelText,
1506
+ level: 2.5,
1507
+ // Special Level
1508
+ parent: bookId
1509
+ };
1510
+ dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
1511
+ }
1512
+ });
1513
+ }
1514
+ }
1515
+ }
1018
1516
  const boundaries = laidOut.meta?.divisionBoundaries ?? [];
1019
1517
  if (boundaries.length > 0) {
1020
1518
  const boundaryMat = createSmartMaterial({
1021
- uniforms: { color: { value: new THREE4.Color(5601177) } },
1519
+ uniforms: { color: { value: new THREE5.Color(5601177) } },
1022
1520
  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; }`,
1023
1521
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
1024
1522
  transparent: true,
1025
1523
  depthWrite: false,
1026
- blending: THREE4.AdditiveBlending
1524
+ blending: THREE5.AdditiveBlending
1027
1525
  });
1028
- const boundaryGeo = new THREE4.BufferGeometry();
1526
+ const boundaryGeo = new THREE5.BufferGeometry();
1029
1527
  const bPoints = [];
1030
1528
  boundaries.forEach((angle) => {
1031
1529
  const steps = 32;
@@ -1038,8 +1536,8 @@ function createEngine({
1038
1536
  bPoints.push(p2.x, p2.y, p2.z);
1039
1537
  }
1040
1538
  });
1041
- boundaryGeo.setAttribute("position", new THREE4.Float32BufferAttribute(bPoints, 3));
1042
- boundaryLines = new THREE4.LineSegments(boundaryGeo, boundaryMat);
1539
+ boundaryGeo.setAttribute("position", new THREE5.Float32BufferAttribute(bPoints, 3));
1540
+ boundaryLines = new THREE5.LineSegments(boundaryGeo, boundaryMat);
1043
1541
  boundaryLines.frustumCulled = false;
1044
1542
  root.add(boundaryLines);
1045
1543
  }
@@ -1058,7 +1556,7 @@ function createEngine({
1058
1556
  const r_norm = Math.sqrt(x * x + y * y);
1059
1557
  const phi = Math.atan2(y, x);
1060
1558
  const theta = r_norm * (Math.PI / 2);
1061
- return new THREE4.Vector3(
1559
+ return new THREE5.Vector3(
1062
1560
  Math.sin(theta) * Math.cos(phi),
1063
1561
  Math.cos(theta),
1064
1562
  Math.sin(theta) * Math.sin(phi)
@@ -1071,18 +1569,18 @@ function createEngine({
1071
1569
  }
1072
1570
  }
1073
1571
  if (polyPoints.length > 0) {
1074
- const polyGeo = new THREE4.BufferGeometry();
1075
- polyGeo.setAttribute("position", new THREE4.Float32BufferAttribute(polyPoints, 3));
1572
+ const polyGeo = new THREE5.BufferGeometry();
1573
+ polyGeo.setAttribute("position", new THREE5.Float32BufferAttribute(polyPoints, 3));
1076
1574
  const polyMat = createSmartMaterial({
1077
- uniforms: { color: { value: new THREE4.Color(3718648) } },
1575
+ uniforms: { color: { value: new THREE5.Color(3718648) } },
1078
1576
  // Cyan-ish
1079
1577
  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
1578
  fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
1081
1579
  transparent: true,
1082
1580
  depthWrite: false,
1083
- blending: THREE4.AdditiveBlending
1581
+ blending: THREE5.AdditiveBlending
1084
1582
  });
1085
- const polyLines = new THREE4.LineSegments(polyGeo, polyMat);
1583
+ const polyLines = new THREE5.LineSegments(polyGeo, polyMat);
1086
1584
  polyLines.frustumCulled = false;
1087
1585
  root.add(polyLines);
1088
1586
  }
@@ -1094,6 +1592,7 @@ function createEngine({
1094
1592
  let lastModel = void 0;
1095
1593
  let lastAppliedLon = void 0;
1096
1594
  let lastAppliedLat = void 0;
1595
+ let lastBackdropCount = void 0;
1097
1596
  function setConfig(cfg) {
1098
1597
  currentConfig = cfg;
1099
1598
  if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
@@ -1106,6 +1605,11 @@ function createEngine({
1106
1605
  state.targetLat = cfg.camera.lat;
1107
1606
  lastAppliedLat = cfg.camera.lat;
1108
1607
  }
1608
+ const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
1609
+ if (lastBackdropCount !== desiredBackdropCount) {
1610
+ createBackdropStars(desiredBackdropCount);
1611
+ lastBackdropCount = desiredBackdropCount;
1612
+ }
1109
1613
  let shouldRebuild = false;
1110
1614
  let model = cfg.model;
1111
1615
  if (!model && cfg.data && cfg.adapter) {
@@ -1129,6 +1633,29 @@ function createEngine({
1129
1633
  } else if (cfg.arrangement && starPoints) {
1130
1634
  if (lastModel) buildFromModel(lastModel, cfg);
1131
1635
  }
1636
+ if (cfg.constellations) {
1637
+ constellationLayer.load(cfg.constellations, (id) => {
1638
+ if (cfg.arrangement && cfg.arrangement[id]) {
1639
+ const arr = cfg.arrangement[id];
1640
+ if (arr.position[2] === 0) {
1641
+ const x = arr.position[0];
1642
+ const y = arr.position[1];
1643
+ const radius = cfg.layout?.radius ?? 2e3;
1644
+ const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
1645
+ const phi = Math.atan2(y, x);
1646
+ const theta = r_norm * (Math.PI / 2);
1647
+ return new THREE5.Vector3(
1648
+ Math.sin(theta) * Math.cos(phi),
1649
+ Math.cos(theta),
1650
+ Math.sin(theta) * Math.sin(phi)
1651
+ ).multiplyScalar(radius);
1652
+ }
1653
+ return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
1654
+ }
1655
+ const n = nodeById.get(id);
1656
+ return n ? getPosition(n) : null;
1657
+ });
1658
+ }
1132
1659
  }
1133
1660
  function setHandlers(next) {
1134
1661
  handlers = next;
@@ -1148,8 +1675,12 @@ function createEngine({
1148
1675
  }
1149
1676
  }
1150
1677
  for (const item of dynamicLabels) {
1678
+ if (item.node.level === 3) continue;
1151
1679
  arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
1152
1680
  }
1681
+ for (const item of constellationLayer.getItems()) {
1682
+ arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
1683
+ }
1153
1684
  Object.assign(arr, state.tempArrangement);
1154
1685
  return arr;
1155
1686
  }
@@ -1159,16 +1690,18 @@ function createEngine({
1159
1690
  const mY = ev.clientY - rect.top;
1160
1691
  mouseNDC.x = mX / rect.width * 2 - 1;
1161
1692
  mouseNDC.y = -(mY / rect.height) * 2 + 1;
1162
- let closestLabel = null;
1163
- let minLabelDist = 40;
1164
1693
  const uScale = globalUniforms.uScale.value;
1165
1694
  const uAspect = camera.aspect;
1166
1695
  const w = rect.width;
1167
1696
  const h = rect.height;
1697
+ let closestLabel = null;
1698
+ let minLabelDist = 40;
1168
1699
  for (const item of dynamicLabels) {
1169
1700
  if (!item.obj.visible) continue;
1170
1701
  const pWorld = item.obj.position;
1171
1702
  const pProj = smartProjectJS(pWorld);
1703
+ const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1704
+ if (isBehind) continue;
1172
1705
  const xNDC = pProj.x * uScale / uAspect;
1173
1706
  const yNDC = pProj.y * uScale;
1174
1707
  const sX = (xNDC * 0.5 + 0.5) * w;
@@ -1176,24 +1709,72 @@ function createEngine({
1176
1709
  const dx = mX - sX;
1177
1710
  const dy = mY - sY;
1178
1711
  const d = Math.sqrt(dx * dx + dy * dy);
1179
- const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1180
- if (!isBehind && d < minLabelDist) {
1712
+ if (d < minLabelDist) {
1181
1713
  minLabelDist = d;
1182
1714
  closestLabel = item;
1183
1715
  }
1184
1716
  }
1185
- if (closestLabel) return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
1186
- const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1187
- raycaster.ray.origin.set(0, 0, 0);
1188
- raycaster.ray.direction.copy(worldDir);
1189
- raycaster.params.Points.threshold = 5 * (state.fov / 60);
1190
- const hits = raycaster.intersectObject(starPoints, false);
1191
- const pointHit = hits[0];
1192
- if (pointHit && pointHit.index !== void 0) {
1193
- const id = starIndexToId[pointHit.index];
1194
- if (id) {
1195
- const node = nodeById.get(id);
1196
- if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
1717
+ if (closestLabel) {
1718
+ return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
1719
+ }
1720
+ let closestConst = null;
1721
+ let minConstDist = Infinity;
1722
+ for (const item of constellationLayer.getItems()) {
1723
+ if (!item.mesh.visible) continue;
1724
+ const pWorld = item.mesh.position;
1725
+ const pProj = smartProjectJS(pWorld);
1726
+ const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1727
+ if (isBehind) continue;
1728
+ const uniforms = item.material.uniforms;
1729
+ if (!uniforms || !uniforms.uSize) continue;
1730
+ const uSize = uniforms.uSize.value;
1731
+ const uImgAspect = uniforms.uImgAspect.value;
1732
+ const uImgRotation = uniforms.uImgRotation.value;
1733
+ const dist = pWorld.length();
1734
+ if (dist < 1e-3) continue;
1735
+ const scale = uSize / dist * uScale;
1736
+ const halfH_px = scale / 2 * (h / 2);
1737
+ const halfW_px = halfH_px * uImgAspect;
1738
+ const xNDC = pProj.x * uScale / uAspect;
1739
+ const yNDC = pProj.y * uScale;
1740
+ const sX = (xNDC * 0.5 + 0.5) * w;
1741
+ const sY = (-yNDC * 0.5 + 0.5) * h;
1742
+ const dx = mX - sX;
1743
+ const dy = mY - sY;
1744
+ const dy_cart = -dy;
1745
+ const cr = Math.cos(-uImgRotation);
1746
+ const sr = Math.sin(-uImgRotation);
1747
+ const localX = dx * cr - dy_cart * sr;
1748
+ const localY = dx * sr + dy_cart * cr;
1749
+ if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
1750
+ const d = Math.sqrt(dx * dx + dy * dy);
1751
+ if (!closestConst || d < minConstDist) {
1752
+ minConstDist = d;
1753
+ closestConst = item;
1754
+ }
1755
+ }
1756
+ }
1757
+ if (closestConst) {
1758
+ const fakeNode = {
1759
+ id: closestConst.config.id,
1760
+ label: closestConst.config.title,
1761
+ level: -1
1762
+ };
1763
+ return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
1764
+ }
1765
+ if (starPoints) {
1766
+ const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1767
+ raycaster.ray.origin.set(0, 0, 0);
1768
+ raycaster.ray.direction.copy(worldDir);
1769
+ raycaster.params.Points.threshold = 5 * (state.fov / 60);
1770
+ const hits = raycaster.intersectObject(starPoints, false);
1771
+ const pointHit = hits[0];
1772
+ if (pointHit && pointHit.index !== void 0) {
1773
+ const id = starIndexToId[pointHit.index];
1774
+ if (id) {
1775
+ const node = nodeById.get(id);
1776
+ if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
1777
+ }
1197
1778
  }
1198
1779
  }
1199
1780
  return void 0;
@@ -1221,16 +1802,19 @@ function createEngine({
1221
1802
  if (starId) {
1222
1803
  const starNode = nodeById.get(starId);
1223
1804
  if (starNode && starNode.parent === bookId) {
1224
- children.push({ index: i, initialPos: new THREE4.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
1805
+ children.push({ index: i, initialPos: new THREE5.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
1225
1806
  }
1226
1807
  }
1227
1808
  }
1228
1809
  }
1229
1810
  state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
1230
1811
  state.draggedStarIndex = -1;
1812
+ } else if (hit.type === "constellation") {
1813
+ state.draggedGroup = null;
1814
+ state.draggedStarIndex = -1;
1231
1815
  }
1232
- return;
1233
1816
  }
1817
+ return;
1234
1818
  }
1235
1819
  state.dragMode = "camera";
1236
1820
  state.isDragging = true;
@@ -1260,13 +1844,19 @@ function createEngine({
1260
1844
  if (item) {
1261
1845
  item.obj.position.copy(newPos);
1262
1846
  state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
1847
+ } else if (state.draggedNodeId) {
1848
+ const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
1849
+ if (cItem) {
1850
+ cItem.mesh.position.copy(newPos);
1851
+ state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
1852
+ }
1263
1853
  }
1264
1854
  const vStart = group.labelInitialPos.clone().normalize();
1265
1855
  const vEnd = newPos.clone().normalize();
1266
- const q = new THREE4.Quaternion().setFromUnitVectors(vStart, vEnd);
1856
+ const q = new THREE5.Quaternion().setFromUnitVectors(vStart, vEnd);
1267
1857
  if (starPoints && group.children.length > 0) {
1268
1858
  const attr = starPoints.geometry.attributes.position;
1269
- const tempVec = new THREE4.Vector3();
1859
+ const tempVec = new THREE5.Vector3();
1270
1860
  for (const child of group.children) {
1271
1861
  tempVec.copy(child.initialPos).applyQuaternion(q);
1272
1862
  attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
@@ -1300,7 +1890,7 @@ function createEngine({
1300
1890
  if (res) {
1301
1891
  hoverLabelMat.uniforms.uMap.value = res.tex;
1302
1892
  const baseScale = 0.03;
1303
- const size = new THREE4.Vector2(baseScale * res.aspect, baseScale);
1893
+ const size = new THREE5.Vector2(baseScale * res.aspect, baseScale);
1304
1894
  hoverLabelMat.uniforms.uSize.value = size;
1305
1895
  hoverLabelMesh.scale.set(size.x, size.y, 1);
1306
1896
  }
@@ -1316,6 +1906,7 @@ function createEngine({
1316
1906
  if (hit?.node.id !== handlers._lastHoverId) {
1317
1907
  handlers._lastHoverId = hit?.node.id;
1318
1908
  handlers.onHover?.(hit?.node);
1909
+ constellationLayer.setHovered(hit?.node.id ?? null);
1319
1910
  }
1320
1911
  document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
1321
1912
  }
@@ -1335,7 +1926,14 @@ function createEngine({
1335
1926
  document.body.style.cursor = "default";
1336
1927
  } else {
1337
1928
  const hit = pick(e);
1338
- if (hit) handlers.onSelect?.(hit.node);
1929
+ if (hit) {
1930
+ handlers.onSelect?.(hit.node);
1931
+ constellationLayer.setFocused(hit.node.id);
1932
+ if (hit.node.level === 2) setFocusedBook(hit.node.id);
1933
+ else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
1934
+ } else {
1935
+ setFocusedBook(null);
1936
+ }
1339
1937
  }
1340
1938
  }
1341
1939
  function onWheel(e) {
@@ -1346,25 +1944,26 @@ function createEngine({
1346
1944
  const zoomSpeed = 1e-3 * state.fov;
1347
1945
  state.fov += e.deltaY * zoomSpeed;
1348
1946
  state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
1947
+ handlers.onFovChange?.(state.fov);
1349
1948
  updateUniforms();
1350
1949
  const vAfter = getMouseViewVector(state.fov, aspect);
1351
- const quaternion = new THREE4.Quaternion().setFromUnitVectors(vAfter, vBefore);
1950
+ const quaternion = new THREE5.Quaternion().setFromUnitVectors(vAfter, vBefore);
1352
1951
  const y = Math.sin(state.lat);
1353
1952
  const r = Math.cos(state.lat);
1354
1953
  const x = r * Math.sin(state.lon);
1355
1954
  const z = -r * Math.cos(state.lon);
1356
- const currentLook = new THREE4.Vector3(x, y, z);
1955
+ const currentLook = new THREE5.Vector3(x, y, z);
1357
1956
  const camForward = currentLook.clone().normalize();
1358
1957
  const camUp = camera.up.clone();
1359
- const camRight = new THREE4.Vector3().crossVectors(camForward, camUp).normalize();
1360
- const camUpOrtho = new THREE4.Vector3().crossVectors(camRight, camForward).normalize();
1361
- const mat = new THREE4.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
1362
- const qOld = new THREE4.Quaternion().setFromRotationMatrix(mat);
1958
+ const camRight = new THREE5.Vector3().crossVectors(camForward, camUp).normalize();
1959
+ const camUpOrtho = new THREE5.Vector3().crossVectors(camRight, camForward).normalize();
1960
+ const mat = new THREE5.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
1961
+ const qOld = new THREE5.Quaternion().setFromRotationMatrix(mat);
1363
1962
  const qNew = qOld.clone().multiply(quaternion);
1364
- const newForward = new THREE4.Vector3(0, 0, -1).applyQuaternion(qNew);
1963
+ const newForward = new THREE5.Vector3(0, 0, -1).applyQuaternion(qNew);
1365
1964
  state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
1366
1965
  state.lon = Math.atan2(newForward.x, -newForward.z);
1367
- const newUp = new THREE4.Vector3(0, 1, 0).applyQuaternion(qNew);
1966
+ const newUp = new THREE5.Vector3(0, 1, 0).applyQuaternion(qNew);
1368
1967
  camera.up.copy(newUp);
1369
1968
  if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
1370
1969
  const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
@@ -1405,55 +2004,89 @@ function createEngine({
1405
2004
  function tick() {
1406
2005
  if (!running) return;
1407
2006
  raf = requestAnimationFrame(tick);
1408
- if (!state.isDragging && isMouseInWindow) {
2007
+ const now = performance.now();
2008
+ globalUniforms.uTime.value = now / 1e3;
2009
+ let activeId = null;
2010
+ if (focusedBookId) {
2011
+ activeId = focusedBookId;
2012
+ } else if (hoveredBookId) {
2013
+ const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
2014
+ if (now - lastExit > COOLDOWN_MS) {
2015
+ activeId = hoveredBookId;
2016
+ }
2017
+ }
2018
+ const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
2019
+ orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
2020
+ if (orderRevealStrength > 1e-3 || targetStrength > 0) {
2021
+ if (activeId && bookIdToIndex.has(activeId)) {
2022
+ activeBookIndex = bookIdToIndex.get(activeId);
2023
+ }
2024
+ if (starPoints && starPoints.material) {
2025
+ const m = starPoints.material;
2026
+ if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
2027
+ if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
2028
+ }
2029
+ }
2030
+ let panX = 0;
2031
+ let panY = 0;
2032
+ if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
1409
2033
  const t = ENGINE_CONFIG.edgePanThreshold;
1410
- const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
1411
- let panX = 0;
1412
- let panY = 0;
1413
- if (mouseNDC.x < -1 + t) {
1414
- const s = (-1 + t - mouseNDC.x) / t;
1415
- panX = -s * s * speedBase;
1416
- } else if (mouseNDC.x > 1 - t) {
1417
- const s = (mouseNDC.x - (1 - t)) / t;
1418
- panX = s * s * speedBase;
1419
- }
1420
- if (mouseNDC.y < -1 + t) {
1421
- const s = (-1 + t - mouseNDC.y) / t;
1422
- panY = -s * s * speedBase;
1423
- } else if (mouseNDC.y > 1 - t) {
1424
- const s = (mouseNDC.y - (1 - t)) / t;
1425
- panY = s * s * speedBase;
1426
- }
1427
- if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
1428
- state.lon += panX;
1429
- state.lat += panY;
1430
- state.targetLon = state.lon;
1431
- state.targetLat = state.lat;
2034
+ const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
2035
+ const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
2036
+ if (inZoneX || inZoneY) {
2037
+ if (edgeHoverStart === 0) edgeHoverStart = performance.now();
2038
+ if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
2039
+ const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
2040
+ if (mouseNDC.x < -1 + t) {
2041
+ const s = (-1 + t - mouseNDC.x) / t;
2042
+ panX = -s * s * speedBase;
2043
+ } else if (mouseNDC.x > 1 - t) {
2044
+ const s = (mouseNDC.x - (1 - t)) / t;
2045
+ panX = s * s * speedBase;
2046
+ }
2047
+ if (mouseNDC.y < -1 + t) {
2048
+ const s = (-1 + t - mouseNDC.y) / t;
2049
+ panY = -s * s * speedBase;
2050
+ } else if (mouseNDC.y > 1 - t) {
2051
+ const s = (mouseNDC.y - (1 - t)) / t;
2052
+ panY = s * s * speedBase;
2053
+ }
2054
+ }
1432
2055
  } else {
1433
- state.lon += state.velocityX;
1434
- state.lat += state.velocityY;
1435
- state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1436
- state.velocityY *= ENGINE_CONFIG.inertiaDamping;
1437
- if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
1438
- if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
2056
+ edgeHoverStart = 0;
1439
2057
  }
2058
+ } else {
2059
+ edgeHoverStart = 0;
2060
+ }
2061
+ if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
2062
+ state.lon += panX;
2063
+ state.lat += panY;
2064
+ state.targetLon = state.lon;
2065
+ state.targetLat = state.lat;
1440
2066
  } else if (!state.isDragging) {
1441
2067
  state.lon += state.velocityX;
1442
2068
  state.lat += state.velocityY;
1443
2069
  state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1444
2070
  state.velocityY *= ENGINE_CONFIG.inertiaDamping;
2071
+ if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
2072
+ if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
1445
2073
  }
1446
2074
  state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
1447
2075
  const y = Math.sin(state.lat);
1448
2076
  const r = Math.cos(state.lat);
1449
2077
  const x = r * Math.sin(state.lon);
1450
2078
  const z = -r * Math.cos(state.lon);
1451
- const target = new THREE4.Vector3(x, y, z);
1452
- const idealUp = new THREE4.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
2079
+ const target = new THREE5.Vector3(x, y, z);
2080
+ const idealUp = new THREE5.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
1453
2081
  camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
1454
2082
  camera.up.normalize();
1455
2083
  camera.lookAt(target);
2084
+ camera.updateMatrixWorld();
2085
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
1456
2086
  updateUniforms();
2087
+ constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
2088
+ backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
2089
+ if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
1457
2090
  const DIVISION_THRESHOLD = 60;
1458
2091
  const showDivisions = state.fov > DIVISION_THRESHOLD;
1459
2092
  if (constellationLines) {
@@ -1477,7 +2110,9 @@ function createEngine({
1477
2110
  const showBookLabels = currentConfig?.showBookLabels === true;
1478
2111
  const showDivisionLabels = currentConfig?.showDivisionLabels === true;
1479
2112
  const showChapterLabels = currentConfig?.showChapterLabels === true;
1480
- const showChapters = state.fov < 35;
2113
+ const showGroupLabels = currentConfig?.showGroupLabels === true;
2114
+ const showBooks = state.fov < 120;
2115
+ const showChapters = state.fov < 70;
1481
2116
  for (const item of dynamicLabels) {
1482
2117
  const uniforms = item.obj.material.uniforms;
1483
2118
  const level = item.node.level;
@@ -1485,20 +2120,26 @@ function createEngine({
1485
2120
  if (level === 2 && showBookLabels) isEnabled = true;
1486
2121
  else if (level === 1 && showDivisionLabels) isEnabled = true;
1487
2122
  else if (level === 3 && showChapterLabels) isEnabled = true;
2123
+ else if (level === 2.5 && showGroupLabels) isEnabled = true;
1488
2124
  if (!isEnabled) {
1489
- uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2125
+ uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1490
2126
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1491
2127
  continue;
1492
2128
  }
1493
2129
  const pWorld = item.obj.position;
1494
2130
  const pProj = smartProjectJS(pWorld);
1495
2131
  if (pProj.z > 0.2) {
1496
- uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2132
+ uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1497
2133
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1498
2134
  continue;
1499
2135
  }
1500
- if (level === 3 && !showChapters && item.node.id !== state.draggedNodeId) {
1501
- uniforms.uAlpha.value = THREE4.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2136
+ if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
2137
+ uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
2138
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
2139
+ continue;
2140
+ }
2141
+ if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
2142
+ uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
1502
2143
  item.obj.visible = uniforms.uAlpha.value > 0.01;
1503
2144
  continue;
1504
2145
  }
@@ -1509,7 +2150,7 @@ function createEngine({
1509
2150
  const size = uniforms.uSize.value;
1510
2151
  const pixelH = size.y * screenH * 0.8;
1511
2152
  const pixelW = size.x * screenH * 0.8;
1512
- labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
2153
+ labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
1513
2154
  }
1514
2155
  const hoverId = handlers._lastHoverId;
1515
2156
  const selectedId = state.draggedNodeId;
@@ -1535,11 +2176,13 @@ function createEngine({
1535
2176
  const dy = l.sY - screenH / 2;
1536
2177
  rot = Math.atan2(-dy, -dx) - Math.PI / 2;
1537
2178
  }
1538
- l.uniforms.uAngle.value = THREE4.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
2179
+ l.uniforms.uAngle.value = THREE5.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
1539
2180
  }
1540
2181
  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 });
2182
+ if (showBooks || isSpecial) {
2183
+ target2 = 1;
2184
+ occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
2185
+ }
1543
2186
  } else if (l.level === 1) {
1544
2187
  if (showDivisions || isSpecial) {
1545
2188
  const pad = -5;
@@ -1548,12 +2191,17 @@ function createEngine({
1548
2191
  occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
1549
2192
  }
1550
2193
  }
1551
- } else if (l.level === 3) {
2194
+ } else if (l.level === 2.5 || l.level === 3) {
1552
2195
  if (showChapters || isSpecial) {
1553
2196
  target2 = 1;
2197
+ if (!isSpecial) {
2198
+ const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
2199
+ const focusFade = 1 - THREE5.MathUtils.smoothstep(0.4, 0.7, dist);
2200
+ target2 *= focusFade;
2201
+ }
1554
2202
  }
1555
2203
  }
1556
- l.uniforms.uAlpha.value = THREE4.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
2204
+ l.uniforms.uAlpha.value = THREE5.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
1557
2205
  l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
1558
2206
  }
1559
2207
  renderer.render(scene, camera);
@@ -1570,16 +2218,31 @@ function createEngine({
1570
2218
  }
1571
2219
  function dispose() {
1572
2220
  stop();
2221
+ constellationLayer.dispose();
1573
2222
  renderer.dispose();
1574
2223
  renderer.domElement.remove();
1575
2224
  }
1576
- return { setConfig, start, stop, dispose, setHandlers, getFullArrangement };
2225
+ function setHoveredBook(id) {
2226
+ if (id === hoveredBookId) return;
2227
+ if (hoveredBookId) {
2228
+ hoverCooldowns.set(hoveredBookId, performance.now());
2229
+ }
2230
+ hoveredBookId = id;
2231
+ }
2232
+ function setFocusedBook(id) {
2233
+ focusedBookId = id;
2234
+ }
2235
+ function setOrderRevealEnabled(enabled) {
2236
+ orderRevealEnabled = enabled;
2237
+ }
2238
+ return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled };
1577
2239
  }
1578
- var ENGINE_CONFIG;
2240
+ var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
1579
2241
  var init_createEngine = __esm({
1580
2242
  "src/engine/createEngine.ts"() {
1581
2243
  init_layout();
1582
2244
  init_materials();
2245
+ init_ConstellationArtworkLayer();
1583
2246
  ENGINE_CONFIG = {
1584
2247
  minFov: 10,
1585
2248
  maxFov: 165,
@@ -1592,16 +2255,26 @@ var init_createEngine = __esm({
1592
2255
  zenithStrength: 0.02,
1593
2256
  horizonLockStrength: 0.05,
1594
2257
  edgePanThreshold: 0.15,
1595
- edgePanMaxSpeed: 0.02
2258
+ edgePanMaxSpeed: 0.02,
2259
+ edgePanDelay: 250
2260
+ };
2261
+ ORDER_REVEAL_CONFIG = {
2262
+ globalDim: 0.85,
2263
+ pulseAmplitude: 0.6,
2264
+ pulseDuration: 2,
2265
+ delayPerChapter: 0.1
1596
2266
  };
1597
2267
  }
1598
2268
  });
1599
2269
  var StarMap = forwardRef(
1600
- ({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
2270
+ ({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
1601
2271
  const containerRef = useRef(null);
1602
2272
  const engineRef = useRef(null);
1603
2273
  useImperativeHandle(ref, () => ({
1604
- getFullArrangement: () => engineRef.current?.getFullArrangement?.()
2274
+ getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
2275
+ setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
2276
+ setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
2277
+ setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
1605
2278
  }));
1606
2279
  useEffect(() => {
1607
2280
  let disposed = false;
@@ -1613,7 +2286,8 @@ var StarMap = forwardRef(
1613
2286
  container: containerRef.current,
1614
2287
  onSelect,
1615
2288
  onHover,
1616
- onArrangementChange
2289
+ onArrangementChange,
2290
+ onFovChange
1617
2291
  });
1618
2292
  engineRef.current.setConfig(config);
1619
2293
  engineRef.current.start();
@@ -1629,8 +2303,8 @@ var StarMap = forwardRef(
1629
2303
  engineRef.current?.setConfig?.(config);
1630
2304
  }, [config]);
1631
2305
  useEffect(() => {
1632
- engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
1633
- }, [onSelect, onHover, onArrangementChange]);
2306
+ engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
2307
+ }, [onSelect, onHover, onArrangementChange, onFovChange]);
1634
2308
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
1635
2309
  }
1636
2310
  );
@@ -1639,7 +2313,6 @@ var StarMap = forwardRef(
1639
2313
  function bibleToSceneModel(data) {
1640
2314
  const nodes = [];
1641
2315
  const links = [];
1642
- let bookCounter = 0;
1643
2316
  const id = {
1644
2317
  testament: (t) => `T:${t}`,
1645
2318
  division: (t, d) => `D:${t}:${d}`,
@@ -1660,8 +2333,7 @@ function bibleToSceneModel(data) {
1660
2333
  });
1661
2334
  links.push({ source: did, target: tid });
1662
2335
  for (const b of d.books) {
1663
- bookCounter++;
1664
- const bookLabel = `${bookCounter}. ${b.name}`;
2336
+ const bookLabel = b.name;
1665
2337
  const bid = id.book(b.key);
1666
2338
  nodes.push({
1667
2339
  id: bid,
@@ -30774,7 +31446,7 @@ var RNG = class {
30774
31446
  const r = Math.sqrt(1 - y * y);
30775
31447
  const x = r * Math.cos(theta);
30776
31448
  const z = r * Math.sin(theta);
30777
- return new THREE4.Vector3(x, y, z);
31449
+ return new THREE5.Vector3(x, y, z);
30778
31450
  }
30779
31451
  };
30780
31452
  function simpleNoise3D(v, scale) {
@@ -30812,11 +31484,11 @@ function generateArrangement(bible, options = {}) {
30812
31484
  });
30813
31485
  });
30814
31486
  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();
31487
+ const mwRad = THREE5.MathUtils.degToRad(opts.milkyWayAngle);
31488
+ const mwNormal = new THREE5.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
30817
31489
  const anchors = [];
30818
31490
  for (let i = 0; i < bookCount; i++) {
30819
- let bestP = new THREE4.Vector3();
31491
+ let bestP = new THREE5.Vector3();
30820
31492
  let valid = false;
30821
31493
  let attempt = 0;
30822
31494
  while (!valid && attempt < 100) {
@@ -30842,7 +31514,7 @@ function generateArrangement(bible, options = {}) {
30842
31514
  arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
30843
31515
  for (let c = 0; c < book.chapters; c++) {
30844
31516
  const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
30845
- const offset = new THREE4.Vector3(
31517
+ const offset = new THREE5.Vector3(
30846
31518
  (rng.next() - 0.5) * 2,
30847
31519
  (rng.next() - 0.5) * 2,
30848
31520
  (rng.next() - 0.5) * 2
@@ -30863,7 +31535,7 @@ function generateArrangement(bible, options = {}) {
30863
31535
  const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
30864
31536
  const divId = `D:${book.testament}:${book.division}`;
30865
31537
  if (!divisions.has(divId)) {
30866
- divisions.set(divId, { sum: new THREE4.Vector3(), count: 0 });
31538
+ divisions.set(divId, { sum: new THREE5.Vector3(), count: 0 });
30867
31539
  }
30868
31540
  const entry = divisions.get(divId);
30869
31541
  entry.sum.add(anchorPos);