@project-skymap/library 0.7.5 → 0.8.1
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 +2446 -621
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -2
- package/dist/index.d.ts +91 -2
- package/dist/index.js +2445 -620
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var THREE6 = 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
|
|
25
|
+
var THREE6__namespace = /*#__PURE__*/_interopNamespace(THREE6);
|
|
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
|
|
154
|
+
let xAxis = new THREE6__namespace.Vector3().crossVectors(up, zAxis);
|
|
155
155
|
if (xAxis.lengthSq() < 1e-4) {
|
|
156
|
-
xAxis = new
|
|
156
|
+
xAxis = new THREE6__namespace.Vector3().crossVectors(new THREE6__namespace.Vector3(1, 0, 0), zAxis);
|
|
157
157
|
}
|
|
158
158
|
xAxis.normalize();
|
|
159
|
-
const yAxis = new
|
|
160
|
-
const m = new
|
|
161
|
-
const v = new
|
|
159
|
+
const yAxis = new THREE6__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
160
|
+
const m = new THREE6__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
161
|
+
const v = new THREE6__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
|
|
242
|
+
const labelPos = new THREE6__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
|
|
258
|
+
const bookPos = new THREE6__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
|
|
269
|
+
const up = new THREE6__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
|
|
288
|
+
const centroid = new THREE6__namespace.Vector3();
|
|
289
289
|
children.forEach((c) => {
|
|
290
290
|
const u = updatedNodeMap.get(c.id);
|
|
291
|
-
centroid.add(new
|
|
291
|
+
centroid.add(new THREE6__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) {
|
|
@@ -409,7 +409,7 @@ float getMaskAlpha() {
|
|
|
409
409
|
});
|
|
410
410
|
function createSmartMaterial(params) {
|
|
411
411
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
412
|
-
return new
|
|
412
|
+
return new THREE6__namespace.ShaderMaterial({
|
|
413
413
|
uniforms,
|
|
414
414
|
vertexShader: `
|
|
415
415
|
${BLEND_CHUNK}
|
|
@@ -423,8 +423,8 @@ function createSmartMaterial(params) {
|
|
|
423
423
|
transparent: params.transparent || false,
|
|
424
424
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
425
425
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
426
|
-
side: params.side ||
|
|
427
|
-
blending: params.blending ||
|
|
426
|
+
side: params.side || THREE6__namespace.FrontSide,
|
|
427
|
+
blending: params.blending || THREE6__namespace.NormalBlending
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
var globalUniforms;
|
|
@@ -442,16 +442,102 @@ var init_materials = __esm({
|
|
|
442
442
|
uAtmGlow: { value: 1 },
|
|
443
443
|
uAtmDark: { value: 0.6 },
|
|
444
444
|
uAtmExtinction: { value: 4 },
|
|
445
|
-
uAtmTwinkle: { value: 0
|
|
446
|
-
uColorHorizon: { value: new
|
|
447
|
-
uColorZenith: { value: new
|
|
445
|
+
uAtmTwinkle: { value: 0 },
|
|
446
|
+
uColorHorizon: { value: new THREE6__namespace.Color(3825292) },
|
|
447
|
+
uColorZenith: { value: new THREE6__namespace.Color(132104) }
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
// src/engine/fader.ts
|
|
453
|
+
var Fader;
|
|
454
|
+
var init_fader = __esm({
|
|
455
|
+
"src/engine/fader.ts"() {
|
|
456
|
+
Fader = class {
|
|
457
|
+
target = false;
|
|
458
|
+
value = 0;
|
|
459
|
+
duration;
|
|
460
|
+
constructor(duration = 0.3) {
|
|
461
|
+
this.duration = duration;
|
|
462
|
+
}
|
|
463
|
+
update(dt) {
|
|
464
|
+
const goal = this.target ? 1 : 0;
|
|
465
|
+
if (this.value === goal) return;
|
|
466
|
+
const speed = 1 / this.duration;
|
|
467
|
+
const step = speed * dt;
|
|
468
|
+
const diff = goal - this.value;
|
|
469
|
+
this.value += Math.sign(diff) * Math.min(step, Math.abs(diff));
|
|
470
|
+
}
|
|
471
|
+
/** Smoothstep-eased value for perceptually smooth transitions */
|
|
472
|
+
get eased() {
|
|
473
|
+
const v = this.value;
|
|
474
|
+
return v * v * (3 - 2 * v);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
function buildSphereQuad(center, rightDir, upDir, halfWidth, halfHeight, domeRadius, subdivisions = 8) {
|
|
480
|
+
const vertsPerSide = subdivisions + 1;
|
|
481
|
+
const vertCount = vertsPerSide * vertsPerSide;
|
|
482
|
+
const positions = new Float32Array(vertCount * 3);
|
|
483
|
+
const uvs = new Float32Array(vertCount * 2);
|
|
484
|
+
const centerNorm = center.clone().normalize();
|
|
485
|
+
const temp = new THREE6__namespace.Vector3();
|
|
486
|
+
const tangent = new THREE6__namespace.Vector3();
|
|
487
|
+
const q = new THREE6__namespace.Quaternion();
|
|
488
|
+
const halfAngleX = Math.atan2(halfWidth, domeRadius);
|
|
489
|
+
const halfAngleY = Math.atan2(halfHeight, domeRadius);
|
|
490
|
+
for (let iy = 0; iy < vertsPerSide; iy++) {
|
|
491
|
+
for (let ix = 0; ix < vertsPerSide; ix++) {
|
|
492
|
+
const idx = iy * vertsPerSide + ix;
|
|
493
|
+
const u = ix / subdivisions;
|
|
494
|
+
const v = iy / subdivisions;
|
|
495
|
+
const angX = (u - 0.5) * 2 * halfAngleX;
|
|
496
|
+
const angY = (v - 0.5) * 2 * halfAngleY;
|
|
497
|
+
tangent.copy(rightDir).multiplyScalar(angX).addScaledVector(upDir, angY);
|
|
498
|
+
const angle = tangent.length();
|
|
499
|
+
if (angle > 1e-5) {
|
|
500
|
+
tangent.normalize();
|
|
501
|
+
q.setFromAxisAngle(tangent, angle);
|
|
502
|
+
temp.copy(centerNorm).applyQuaternion(q).multiplyScalar(domeRadius);
|
|
503
|
+
} else {
|
|
504
|
+
temp.copy(center);
|
|
505
|
+
}
|
|
506
|
+
positions[idx * 3 + 0] = temp.x;
|
|
507
|
+
positions[idx * 3 + 1] = temp.y;
|
|
508
|
+
positions[idx * 3 + 2] = temp.z;
|
|
509
|
+
uvs[idx * 2 + 0] = u;
|
|
510
|
+
uvs[idx * 2 + 1] = 1 - v;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const indexCount = subdivisions * subdivisions * 6;
|
|
514
|
+
const indices = new Uint16Array(indexCount);
|
|
515
|
+
let ii = 0;
|
|
516
|
+
for (let iy = 0; iy < subdivisions; iy++) {
|
|
517
|
+
for (let ix = 0; ix < subdivisions; ix++) {
|
|
518
|
+
const a = iy * vertsPerSide + ix;
|
|
519
|
+
const b = a + 1;
|
|
520
|
+
const c = a + vertsPerSide;
|
|
521
|
+
const d = c + 1;
|
|
522
|
+
indices[ii++] = a;
|
|
523
|
+
indices[ii++] = c;
|
|
524
|
+
indices[ii++] = b;
|
|
525
|
+
indices[ii++] = b;
|
|
526
|
+
indices[ii++] = c;
|
|
527
|
+
indices[ii++] = d;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const geometry = new THREE6__namespace.BufferGeometry();
|
|
531
|
+
geometry.setAttribute("position", new THREE6__namespace.BufferAttribute(positions, 3));
|
|
532
|
+
geometry.setAttribute("uv", new THREE6__namespace.BufferAttribute(uvs, 2));
|
|
533
|
+
geometry.setIndex(new THREE6__namespace.BufferAttribute(indices, 1));
|
|
534
|
+
return geometry;
|
|
535
|
+
}
|
|
451
536
|
var ConstellationArtworkLayer;
|
|
452
537
|
var init_ConstellationArtworkLayer = __esm({
|
|
453
538
|
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
454
539
|
init_materials();
|
|
540
|
+
init_fader();
|
|
455
541
|
ConstellationArtworkLayer = class {
|
|
456
542
|
root;
|
|
457
543
|
items = [];
|
|
@@ -459,27 +545,22 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
459
545
|
hoveredId = null;
|
|
460
546
|
focusedId = null;
|
|
461
547
|
constructor(root) {
|
|
462
|
-
this.textureLoader = new
|
|
548
|
+
this.textureLoader = new THREE6__namespace.TextureLoader();
|
|
463
549
|
this.textureLoader.crossOrigin = "anonymous";
|
|
464
|
-
this.root = new
|
|
550
|
+
this.root = new THREE6__namespace.Group();
|
|
465
551
|
this.root.renderOrder = -1;
|
|
466
552
|
root.add(this.root);
|
|
467
553
|
}
|
|
468
554
|
getItems() {
|
|
469
555
|
return this.items;
|
|
470
556
|
}
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
if (item) {
|
|
474
|
-
item.mesh.position.copy(pos);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
load(config, getPosition) {
|
|
557
|
+
load(config, getPosition, getOrientationPosition) {
|
|
558
|
+
const getAnchorPos = getOrientationPosition ?? getPosition;
|
|
478
559
|
this.clear();
|
|
479
560
|
console.log(`[Constellation] Loading ${config.constellations.length} constellations from ${config.atlasBasePath}`);
|
|
480
561
|
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
481
562
|
config.constellations.forEach((c) => {
|
|
482
|
-
let center = new
|
|
563
|
+
let center = new THREE6__namespace.Vector3();
|
|
483
564
|
let valid = false;
|
|
484
565
|
let radius = 2e3;
|
|
485
566
|
const arrPos = getPosition(c.id);
|
|
@@ -497,7 +578,7 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
497
578
|
}
|
|
498
579
|
}
|
|
499
580
|
} else if (c.center) {
|
|
500
|
-
center.set(c.center[0], c.center[1],
|
|
581
|
+
center.set(c.center[0], c.center[1], 0);
|
|
501
582
|
valid = true;
|
|
502
583
|
} else if (c.anchors.length > 0) {
|
|
503
584
|
const points = [];
|
|
@@ -517,100 +598,65 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
517
598
|
}
|
|
518
599
|
}
|
|
519
600
|
if (!valid) return;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
let
|
|
601
|
+
const centerNorm = center.clone().normalize();
|
|
602
|
+
let rightDir = new THREE6__namespace.Vector3();
|
|
603
|
+
let upDir = new THREE6__namespace.Vector3();
|
|
523
604
|
if (c.anchors.length >= 2) {
|
|
524
|
-
const p0 =
|
|
525
|
-
const p1 =
|
|
526
|
-
if (p0 && p1) {
|
|
527
|
-
const diff = new
|
|
528
|
-
|
|
605
|
+
const p0 = getAnchorPos(c.anchors[0]);
|
|
606
|
+
const p1 = getAnchorPos(c.anchors[1]);
|
|
607
|
+
if (p0 && p1 && p0.distanceTo(p1) > 1e-3) {
|
|
608
|
+
const diff = new THREE6__namespace.Vector3().subVectors(p1, p0);
|
|
609
|
+
rightDir.copy(diff).sub(centerNorm.clone().multiplyScalar(diff.dot(centerNorm))).normalize();
|
|
610
|
+
upDir.crossVectors(centerNorm, rightDir).normalize();
|
|
611
|
+
rightDir.crossVectors(upDir, centerNorm).normalize();
|
|
612
|
+
} else {
|
|
613
|
+
this._defaultTangentFrame(centerNorm, rightDir, upDir);
|
|
529
614
|
}
|
|
530
615
|
} else {
|
|
531
|
-
|
|
532
|
-
|
|
616
|
+
this._defaultTangentFrame(centerNorm, rightDir, upDir);
|
|
617
|
+
}
|
|
618
|
+
if (c.rotationDeg !== 0) {
|
|
619
|
+
const q = new THREE6__namespace.Quaternion().setFromAxisAngle(centerNorm, THREE6__namespace.MathUtils.degToRad(c.rotationDeg));
|
|
620
|
+
rightDir.applyQuaternion(q);
|
|
621
|
+
upDir.applyQuaternion(q);
|
|
533
622
|
}
|
|
534
|
-
const top = new THREE5__namespace.Vector3().crossVectors(upVec, right).normalize();
|
|
535
|
-
right.crossVectors(top, upVec).normalize();
|
|
536
|
-
new THREE5__namespace.Matrix4().makeBasis(right, top, normal);
|
|
537
|
-
const geometry = new THREE5__namespace.PlaneGeometry(1, 1);
|
|
538
623
|
let size = c.radius;
|
|
539
624
|
if (size <= 1) size *= radius;
|
|
540
625
|
size *= 2;
|
|
626
|
+
const sqrtAspect = Math.sqrt(c.aspectRatio ?? 1);
|
|
627
|
+
const halfWidth = size / 2 * sqrtAspect;
|
|
628
|
+
const halfHeight = size / 2 / sqrtAspect;
|
|
629
|
+
const geometry = buildSphereQuad(center, rightDir, upDir, halfWidth, halfHeight, radius, 8);
|
|
541
630
|
const texPath = `${basePath}/${c.image}`;
|
|
542
|
-
|
|
543
|
-
if (c.blend === "additive") blending = THREE5__namespace.AdditiveBlending;
|
|
631
|
+
const blending = c.blend === "additive" ? THREE6__namespace.AdditiveBlending : THREE6__namespace.NormalBlending;
|
|
544
632
|
const material = createSmartMaterial({
|
|
545
633
|
uniforms: {
|
|
546
634
|
uMap: { value: this.textureLoader.load(texPath) },
|
|
547
|
-
|
|
548
|
-
uOpacity: { value: c.opacity },
|
|
549
|
-
uSize: { value: size },
|
|
550
|
-
uImgRotation: { value: THREE5__namespace.MathUtils.degToRad(c.rotationDeg) },
|
|
551
|
-
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
552
|
-
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
635
|
+
uOpacity: { value: c.opacity }
|
|
553
636
|
},
|
|
554
637
|
vertexShaderBody: `
|
|
555
|
-
#ifdef GL_ES
|
|
556
|
-
precision highp float;
|
|
557
|
-
#endif
|
|
558
|
-
|
|
559
|
-
uniform float uSize;
|
|
560
|
-
uniform float uImgRotation;
|
|
561
|
-
uniform float uImgAspect;
|
|
562
|
-
|
|
563
638
|
varying vec2 vUv;
|
|
564
|
-
|
|
639
|
+
varying float vClipFade;
|
|
565
640
|
void main() {
|
|
566
641
|
vUv = uv;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
581
|
-
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
582
|
-
vec2 screenDelta = screenUp - screenCenter;
|
|
583
|
-
|
|
584
|
-
float horizonAngle = 0.0;
|
|
585
|
-
if (length(screenDelta) > 0.001) {
|
|
586
|
-
vec2 screenDir = normalize(screenDelta);
|
|
587
|
-
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
642
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
643
|
+
|
|
644
|
+
// Compute clip-boundary fade BEFORE smartProject
|
|
645
|
+
// (Stellarium culls entire constellations near the boundary;
|
|
646
|
+
// we fade per-vertex for smoother transitions)
|
|
647
|
+
vec3 viewDir = normalize(mvPosition.xyz);
|
|
648
|
+
float clipZ;
|
|
649
|
+
if (uProjectionType == 0) {
|
|
650
|
+
clipZ = -0.1;
|
|
651
|
+
} else if (uProjectionType == 1) {
|
|
652
|
+
clipZ = 0.1;
|
|
653
|
+
} else {
|
|
654
|
+
clipZ = mix(-0.1, 0.1, uBlend);
|
|
588
655
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// 5. Billboard Offset
|
|
594
|
-
vec2 offset = position.xy;
|
|
595
|
-
|
|
596
|
-
float cr = cos(finalAngle);
|
|
597
|
-
float sr = sin(finalAngle);
|
|
598
|
-
vec2 rotated = vec2(
|
|
599
|
-
offset.x * cr - offset.y * sr,
|
|
600
|
-
offset.x * sr + offset.y * cr
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
rotated.x *= uImgAspect;
|
|
604
|
-
|
|
605
|
-
float dist = length(mvCenter.xyz);
|
|
606
|
-
float scale = (uSize / dist) * uScale;
|
|
607
|
-
|
|
608
|
-
rotated *= scale;
|
|
609
|
-
rotated.x /= uAspect;
|
|
610
|
-
|
|
611
|
-
gl_Position = clipCenter;
|
|
612
|
-
gl_Position.xy += rotated * clipCenter.w;
|
|
613
|
-
|
|
656
|
+
// Smooth fade over 0.3 radians before the clip threshold
|
|
657
|
+
vClipFade = smoothstep(clipZ, clipZ - 0.3, viewDir.z);
|
|
658
|
+
|
|
659
|
+
gl_Position = smartProject(mvPosition);
|
|
614
660
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
615
661
|
}
|
|
616
662
|
`,
|
|
@@ -621,30 +667,50 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
621
667
|
uniform sampler2D uMap;
|
|
622
668
|
uniform float uOpacity;
|
|
623
669
|
varying vec2 vUv;
|
|
670
|
+
varying float vClipFade;
|
|
624
671
|
void main() {
|
|
625
672
|
float mask = getMaskAlpha();
|
|
626
673
|
if (mask < 0.01) discard;
|
|
627
674
|
vec4 tex = texture2D(uMap, vUv);
|
|
628
|
-
|
|
675
|
+
|
|
676
|
+
// Apply a slight blue tinge to the artwork
|
|
677
|
+
vec3 color = tex.rgb * vec3(0.8, 0.9, 1.0);
|
|
678
|
+
|
|
679
|
+
// vClipFade smoothly hides vertices near the projection
|
|
680
|
+
// clip boundary, preventing mesh distortion from escape positions
|
|
681
|
+
gl_FragColor = vec4(color, tex.a * uOpacity * mask * vClipFade);
|
|
629
682
|
}
|
|
630
683
|
`,
|
|
631
684
|
transparent: true,
|
|
632
685
|
depthWrite: false,
|
|
633
686
|
depthTest: true,
|
|
634
687
|
blending,
|
|
635
|
-
side:
|
|
688
|
+
side: THREE6__namespace.DoubleSide
|
|
636
689
|
});
|
|
637
690
|
material.uniforms.uMap.value = this.textureLoader.load(
|
|
638
691
|
texPath,
|
|
639
692
|
(tex) => {
|
|
640
|
-
tex.minFilter =
|
|
641
|
-
tex.magFilter =
|
|
693
|
+
tex.minFilter = THREE6__namespace.LinearFilter;
|
|
694
|
+
tex.magFilter = THREE6__namespace.LinearFilter;
|
|
642
695
|
tex.generateMipmaps = false;
|
|
643
696
|
tex.needsUpdate = true;
|
|
644
697
|
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
645
698
|
const natAspect = tex.image.width / tex.image.height;
|
|
646
|
-
|
|
699
|
+
const sqrtNatAspect = Math.sqrt(natAspect);
|
|
700
|
+
const newHalfWidth = size / 2 * sqrtNatAspect;
|
|
701
|
+
const newHalfHeight = size / 2 / sqrtNatAspect;
|
|
702
|
+
const newGeometry = buildSphereQuad(center, rightDir, upDir, newHalfWidth, newHalfHeight, radius, 8);
|
|
703
|
+
const item2 = this.items.find((i) => i.config.id === c.id);
|
|
704
|
+
if (item2) {
|
|
705
|
+
item2.mesh.geometry.dispose();
|
|
706
|
+
item2.mesh.geometry = newGeometry;
|
|
707
|
+
item2.halfWidth = newHalfWidth;
|
|
708
|
+
item2.halfHeight = newHalfHeight;
|
|
709
|
+
item2.imageLoadedFader.target = true;
|
|
710
|
+
}
|
|
647
711
|
}
|
|
712
|
+
const item = this.items.find((i) => i.config.id === c.id);
|
|
713
|
+
if (item) item.imageLoadedFader.target = true;
|
|
648
714
|
console.log(`[Constellation] Loaded: ${c.id} (${tex.image.width}x${tex.image.height})`);
|
|
649
715
|
},
|
|
650
716
|
(progress) => {
|
|
@@ -657,23 +723,55 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
657
723
|
material.polygonOffset = true;
|
|
658
724
|
material.polygonOffsetFactor = -c.zBias;
|
|
659
725
|
}
|
|
660
|
-
const mesh = new
|
|
726
|
+
const mesh = new THREE6__namespace.Mesh(geometry, material);
|
|
661
727
|
mesh.frustumCulled = false;
|
|
662
728
|
mesh.userData = { id: c.id, type: "constellation" };
|
|
663
|
-
mesh.position.copy(center);
|
|
664
729
|
this.root.add(mesh);
|
|
665
|
-
this.items.push({
|
|
730
|
+
this.items.push({
|
|
731
|
+
config: c,
|
|
732
|
+
mesh,
|
|
733
|
+
material,
|
|
734
|
+
baseOpacity: c.opacity,
|
|
735
|
+
center: center.clone(),
|
|
736
|
+
rightDir: rightDir.clone(),
|
|
737
|
+
upDir: upDir.clone(),
|
|
738
|
+
halfWidth,
|
|
739
|
+
halfHeight,
|
|
740
|
+
domeRadius: radius,
|
|
741
|
+
visibleFader: new Fader(0.35),
|
|
742
|
+
imageLoadedFader: new Fader(0.5)
|
|
743
|
+
});
|
|
666
744
|
});
|
|
667
745
|
}
|
|
746
|
+
_defaultTangentFrame(centerNorm, rightDir, upDir) {
|
|
747
|
+
const worldUp = new THREE6__namespace.Vector3(0, 1, 0);
|
|
748
|
+
if (Math.abs(centerNorm.dot(worldUp)) > 0.99) {
|
|
749
|
+
rightDir.crossVectors(new THREE6__namespace.Vector3(1, 0, 0), centerNorm).normalize();
|
|
750
|
+
} else {
|
|
751
|
+
rightDir.crossVectors(worldUp, centerNorm).normalize();
|
|
752
|
+
}
|
|
753
|
+
upDir.crossVectors(centerNorm, rightDir).normalize();
|
|
754
|
+
rightDir.crossVectors(upDir, centerNorm).normalize();
|
|
755
|
+
}
|
|
668
756
|
_globalOpacity = 1;
|
|
669
757
|
setGlobalOpacity(v) {
|
|
670
758
|
this._globalOpacity = v;
|
|
671
759
|
}
|
|
672
|
-
|
|
760
|
+
/**
|
|
761
|
+
* Update visibility and opacity.
|
|
762
|
+
* Accepts an optional camera for Stellarium-style visibility culling:
|
|
763
|
+
* constellations whose center is near or past the projection clip
|
|
764
|
+
* boundary are hidden to prevent mesh distortion from escape positions.
|
|
765
|
+
*/
|
|
766
|
+
update(fov, showArt, camera, dt = 0.016) {
|
|
673
767
|
this.root.visible = showArt;
|
|
674
768
|
if (!showArt) {
|
|
675
769
|
return;
|
|
676
770
|
}
|
|
771
|
+
let cameraForward = null;
|
|
772
|
+
if (camera) {
|
|
773
|
+
cameraForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
774
|
+
}
|
|
677
775
|
for (const item of this.items) {
|
|
678
776
|
const { fade } = item.config;
|
|
679
777
|
let opacity = fade.maxOpacity;
|
|
@@ -683,10 +781,30 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
683
781
|
opacity = fade.minOpacity;
|
|
684
782
|
} else {
|
|
685
783
|
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
686
|
-
opacity =
|
|
784
|
+
opacity = THREE6__namespace.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
687
785
|
}
|
|
688
|
-
|
|
786
|
+
const halfAngleX = Math.atan2(item.halfWidth, item.domeRadius);
|
|
787
|
+
const halfAngleY = Math.atan2(item.halfHeight, item.domeRadius);
|
|
788
|
+
const diameterDeg = Math.max(halfAngleX, halfAngleY) * 2 * THREE6__namespace.MathUtils.RAD2DEG;
|
|
789
|
+
const zoomFade = THREE6__namespace.MathUtils.smoothstep(fov, diameterDeg / 5, diameterDeg / 2);
|
|
790
|
+
opacity *= zoomFade;
|
|
791
|
+
opacity = Math.min(Math.max(opacity, 0), 1) * this._globalOpacity * item.baseOpacity;
|
|
792
|
+
if (cameraForward) {
|
|
793
|
+
const centerDir = item.center.clone().normalize();
|
|
794
|
+
const dot = cameraForward.dot(centerDir);
|
|
795
|
+
const angle = Math.acos(THREE6__namespace.MathUtils.clamp(dot, -1, 1));
|
|
796
|
+
const angularRadius = Math.max(halfAngleX, halfAngleY);
|
|
797
|
+
const margin = THREE6__namespace.MathUtils.degToRad(8);
|
|
798
|
+
item.visibleFader.target = angle <= Math.PI * 0.5 + angularRadius + margin;
|
|
799
|
+
} else {
|
|
800
|
+
item.visibleFader.target = true;
|
|
801
|
+
}
|
|
802
|
+
item.visibleFader.update(dt);
|
|
803
|
+
opacity *= item.visibleFader.eased;
|
|
804
|
+
item.imageLoadedFader.update(dt);
|
|
805
|
+
opacity *= item.imageLoadedFader.eased;
|
|
689
806
|
item.material.uniforms.uOpacity.value = opacity;
|
|
807
|
+
item.mesh.visible = opacity > 1e-3;
|
|
690
808
|
}
|
|
691
809
|
}
|
|
692
810
|
setHovered(id) {
|
|
@@ -783,6 +901,7 @@ var init_projections = __esm({
|
|
|
783
901
|
blendEnd;
|
|
784
902
|
/** Current blend factor, updated via setFov() */
|
|
785
903
|
blend = 0;
|
|
904
|
+
blendOverride = null;
|
|
786
905
|
constructor(blendStart = 40, blendEnd = 100) {
|
|
787
906
|
this.blendStart = blendStart;
|
|
788
907
|
this.blendEnd = blendEnd;
|
|
@@ -801,14 +920,22 @@ var init_projections = __esm({
|
|
|
801
920
|
this.blend = t * t * (3 - 2 * t);
|
|
802
921
|
}
|
|
803
922
|
getBlend() {
|
|
804
|
-
return this.blend;
|
|
923
|
+
return this.blendOverride ?? this.blend;
|
|
924
|
+
}
|
|
925
|
+
setBlendOverride(value) {
|
|
926
|
+
if (value === null || value === void 0 || Number.isNaN(value)) {
|
|
927
|
+
this.blendOverride = null;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
this.blendOverride = Math.max(0, Math.min(1, value));
|
|
805
931
|
}
|
|
806
932
|
forward(dir) {
|
|
807
|
-
|
|
808
|
-
if (
|
|
933
|
+
const b = this.getBlend();
|
|
934
|
+
if (b > 0.5 && dir.z > 0.4) return null;
|
|
935
|
+
if (b < 0.1 && dir.z > -0.1) return null;
|
|
809
936
|
const kLinear = 1 / Math.max(0.01, -dir.z);
|
|
810
937
|
const kStereo = 2 / (1 - dir.z);
|
|
811
|
-
const k = kLinear * (1 -
|
|
938
|
+
const k = kLinear * (1 - b) + kStereo * b;
|
|
812
939
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
813
940
|
}
|
|
814
941
|
inverse(uvX, uvY, fovRad) {
|
|
@@ -817,7 +944,8 @@ var init_projections = __esm({
|
|
|
817
944
|
const thetaLin = Math.atan(r * halfHeightLin);
|
|
818
945
|
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
819
946
|
const thetaStereo = 2 * Math.atan(r * halfHeightStereo / 2);
|
|
820
|
-
const
|
|
947
|
+
const b = this.getBlend();
|
|
948
|
+
const theta = thetaLin * (1 - b) + thetaStereo * b;
|
|
821
949
|
const phi = Math.atan2(uvY, uvX);
|
|
822
950
|
const sinT = Math.sin(theta);
|
|
823
951
|
return {
|
|
@@ -829,11 +957,13 @@ var init_projections = __esm({
|
|
|
829
957
|
getScale(fovRad) {
|
|
830
958
|
const scaleLinear = 1 / Math.tan(fovRad / 2);
|
|
831
959
|
const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
|
|
832
|
-
|
|
960
|
+
const b = this.getBlend();
|
|
961
|
+
return scaleLinear * (1 - b) + scaleStereo * b;
|
|
833
962
|
}
|
|
834
963
|
isClipped(dirZ) {
|
|
835
|
-
|
|
836
|
-
if (
|
|
964
|
+
const b = this.getBlend();
|
|
965
|
+
if (b > 0.5) return dirZ > 0.4;
|
|
966
|
+
if (b < 0.1) return dirZ > -0.1;
|
|
837
967
|
return false;
|
|
838
968
|
}
|
|
839
969
|
};
|
|
@@ -843,30 +973,366 @@ var init_projections = __esm({
|
|
|
843
973
|
};
|
|
844
974
|
}
|
|
845
975
|
});
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
"
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
976
|
+
function levelToClass(level) {
|
|
977
|
+
if (level === 1) return "division";
|
|
978
|
+
if (level === 2) return "book";
|
|
979
|
+
if (level === 2.5) return "group";
|
|
980
|
+
return "chapter";
|
|
981
|
+
}
|
|
982
|
+
function isClassEnabled(classKey, toggles) {
|
|
983
|
+
if (classKey === "division") return toggles.showDivisionLabels;
|
|
984
|
+
if (classKey === "book") return toggles.showBookLabels;
|
|
985
|
+
if (classKey === "group") return toggles.showGroupLabels;
|
|
986
|
+
return toggles.showChapterLabels;
|
|
987
|
+
}
|
|
988
|
+
function lerp(a, b, t) {
|
|
989
|
+
return a + (b - a) * t;
|
|
990
|
+
}
|
|
991
|
+
function resolveLabelBehavior(config) {
|
|
992
|
+
const classes = { ...DEFAULT_LABEL_BEHAVIOR.classes };
|
|
993
|
+
const mergeClass = (key, source) => {
|
|
994
|
+
const base = DEFAULT_LABEL_BEHAVIOR.classes[key];
|
|
995
|
+
return {
|
|
996
|
+
minFov: source?.minFov ?? base.minFov,
|
|
997
|
+
maxFov: source?.maxFov ?? base.maxFov,
|
|
998
|
+
priority: source?.priority ?? base.priority,
|
|
999
|
+
mode: source?.mode ?? base.mode,
|
|
1000
|
+
maxOverlapPx: source?.maxOverlapPx ?? base.maxOverlapPx,
|
|
1001
|
+
radialFadeStart: source?.radialFadeStart ?? base.radialFadeStart,
|
|
1002
|
+
radialFadeEnd: source?.radialFadeEnd ?? base.radialFadeEnd,
|
|
1003
|
+
fadeDuration: source?.fadeDuration ?? base.fadeDuration
|
|
1004
|
+
};
|
|
1005
|
+
};
|
|
1006
|
+
classes.division = mergeClass("division", config?.classes?.division);
|
|
1007
|
+
classes.book = mergeClass("book", config?.classes?.book);
|
|
1008
|
+
classes.group = mergeClass("group", config?.classes?.group);
|
|
1009
|
+
classes.chapter = mergeClass("chapter", config?.classes?.chapter);
|
|
1010
|
+
return {
|
|
1011
|
+
hideBackFacing: config?.hideBackFacing ?? DEFAULT_LABEL_BEHAVIOR.hideBackFacing,
|
|
1012
|
+
overlapPaddingPx: config?.overlapPaddingPx ?? DEFAULT_LABEL_BEHAVIOR.overlapPaddingPx,
|
|
1013
|
+
reappearDelayMs: config?.reappearDelayMs ?? DEFAULT_LABEL_BEHAVIOR.reappearDelayMs,
|
|
1014
|
+
classes
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function boundsOverlapDepth(a, b) {
|
|
1018
|
+
const ix0 = Math.max(a.x, b.x);
|
|
1019
|
+
const iy0 = Math.max(a.y, b.y);
|
|
1020
|
+
const ix1 = Math.min(a.x + a.w, b.x + b.w);
|
|
1021
|
+
const iy1 = Math.min(a.y + a.h, b.y + b.h);
|
|
1022
|
+
if (ix1 <= ix0 || iy1 <= iy0) return 0;
|
|
1023
|
+
return Math.min(ix1 - ix0, iy1 - iy0);
|
|
1024
|
+
}
|
|
1025
|
+
function boundsDistPoint(rect, px, py) {
|
|
1026
|
+
const cx = rect.x + rect.w * 0.5;
|
|
1027
|
+
const cy = rect.y + rect.h * 0.5;
|
|
1028
|
+
const dx = Math.max(Math.abs(px - cx) - rect.w * 0.5, 0);
|
|
1029
|
+
const dy = Math.max(Math.abs(py - cy) - rect.h * 0.5, 0);
|
|
1030
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1031
|
+
}
|
|
1032
|
+
function getLabelUniforms(obj) {
|
|
1033
|
+
const material = obj.material;
|
|
1034
|
+
if (!(material instanceof THREE6__namespace.ShaderMaterial) || !material.uniforms) return null;
|
|
1035
|
+
const uniforms = material.uniforms;
|
|
1036
|
+
const uSize = uniforms.uSize;
|
|
1037
|
+
const uAlpha = uniforms.uAlpha;
|
|
1038
|
+
const uAngle = uniforms.uAngle;
|
|
1039
|
+
if (!uSize || !(uSize.value instanceof THREE6__namespace.Vector2) || !uAlpha || typeof uAlpha.value !== "number") {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
uSize: { value: uSize.value },
|
|
1044
|
+
uAlpha: { value: uAlpha.value },
|
|
1045
|
+
uAngle: uAngle && typeof uAngle.value === "number" ? { value: uAngle.value } : void 0
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function applyUniformAlpha(obj, alpha, angle) {
|
|
1049
|
+
const material = obj.material;
|
|
1050
|
+
if (!(material instanceof THREE6__namespace.ShaderMaterial) || !material.uniforms) return;
|
|
1051
|
+
const uniforms = material.uniforms;
|
|
1052
|
+
if (uniforms.uAlpha && typeof uniforms.uAlpha.value === "number") {
|
|
1053
|
+
uniforms.uAlpha.value = alpha;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var DEFAULT_LABEL_BEHAVIOR, LabelManager;
|
|
1057
|
+
var init_LabelManager = __esm({
|
|
1058
|
+
"src/engine/LabelManager.ts"() {
|
|
1059
|
+
init_fader();
|
|
1060
|
+
DEFAULT_LABEL_BEHAVIOR = {
|
|
1061
|
+
hideBackFacing: true,
|
|
1062
|
+
overlapPaddingPx: 2,
|
|
1063
|
+
reappearDelayMs: 60,
|
|
1064
|
+
classes: {
|
|
1065
|
+
division: {
|
|
1066
|
+
minFov: 55,
|
|
1067
|
+
maxFov: 180,
|
|
1068
|
+
priority: 70,
|
|
1069
|
+
mode: "floating",
|
|
1070
|
+
maxOverlapPx: 10,
|
|
1071
|
+
radialFadeStart: 1,
|
|
1072
|
+
radialFadeEnd: 1.2,
|
|
1073
|
+
fadeDuration: 0.28
|
|
1074
|
+
},
|
|
1075
|
+
book: {
|
|
1076
|
+
minFov: 0,
|
|
1077
|
+
maxFov: 22,
|
|
1078
|
+
priority: 60,
|
|
1079
|
+
mode: "pinned",
|
|
1080
|
+
maxOverlapPx: 999,
|
|
1081
|
+
radialFadeStart: 1,
|
|
1082
|
+
radialFadeEnd: 1.2,
|
|
1083
|
+
fadeDuration: 0.22
|
|
1084
|
+
},
|
|
1085
|
+
group: {
|
|
1086
|
+
minFov: 0,
|
|
1087
|
+
maxFov: 22,
|
|
1088
|
+
priority: 42,
|
|
1089
|
+
mode: "pinned",
|
|
1090
|
+
maxOverlapPx: 999,
|
|
1091
|
+
radialFadeStart: 1,
|
|
1092
|
+
radialFadeEnd: 1.2,
|
|
1093
|
+
fadeDuration: 0.22
|
|
1094
|
+
},
|
|
1095
|
+
chapter: {
|
|
1096
|
+
minFov: 0,
|
|
1097
|
+
maxFov: 22,
|
|
1098
|
+
priority: 30,
|
|
1099
|
+
mode: "pinned",
|
|
1100
|
+
maxOverlapPx: 999,
|
|
1101
|
+
radialFadeStart: 0.55,
|
|
1102
|
+
radialFadeEnd: 0.95,
|
|
1103
|
+
fadeDuration: 0.16
|
|
1104
|
+
}
|
|
857
1105
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
|
|
1106
|
+
};
|
|
1107
|
+
LabelManager = class {
|
|
1108
|
+
records = /* @__PURE__ */ new Map();
|
|
1109
|
+
setLabels(labels) {
|
|
1110
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
1111
|
+
for (const label of labels) {
|
|
1112
|
+
activeIds.add(label.node.id);
|
|
1113
|
+
const existing = this.records.get(label.node.id);
|
|
1114
|
+
if (existing) {
|
|
1115
|
+
existing.label = label;
|
|
1116
|
+
existing.classKey = levelToClass(label.node.level);
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
this.records.set(label.node.id, {
|
|
1120
|
+
id: label.node.id,
|
|
1121
|
+
label,
|
|
1122
|
+
fader: new Fader(0.2),
|
|
1123
|
+
classKey: levelToClass(label.node.level),
|
|
1124
|
+
lastRejectedAtMs: 0,
|
|
1125
|
+
lastAccepted: false,
|
|
1126
|
+
targetAlpha: 0
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
for (const [id] of this.records) {
|
|
1130
|
+
if (!activeIds.has(id)) {
|
|
1131
|
+
this.records.delete(id);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
865
1134
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1135
|
+
clear() {
|
|
1136
|
+
this.records.clear();
|
|
1137
|
+
}
|
|
1138
|
+
update(ctx) {
|
|
1139
|
+
const behavior = resolveLabelBehavior(ctx.config);
|
|
1140
|
+
const candidates = [];
|
|
1141
|
+
const cameraForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(ctx.camera.quaternion);
|
|
1142
|
+
for (const record of this.records.values()) {
|
|
1143
|
+
const classBehavior = behavior.classes[record.classKey];
|
|
1144
|
+
record.fader.duration = classBehavior.fadeDuration;
|
|
1145
|
+
const isEnabled = isClassEnabled(record.classKey, ctx.toggles);
|
|
1146
|
+
const isSpecial = record.id === ctx.selectedId || record.id === ctx.hoverId || record.id === ctx.focusedId;
|
|
1147
|
+
let targetAlpha = 0;
|
|
1148
|
+
let angleTarget;
|
|
1149
|
+
if (isEnabled) {
|
|
1150
|
+
const maxFov = classBehavior.maxFov + (record.label.maxFovBias ?? 0);
|
|
1151
|
+
const inFovRange = ctx.fov >= classBehavior.minFov && ctx.fov <= maxFov;
|
|
1152
|
+
if (inFovRange || isSpecial) {
|
|
1153
|
+
const pWorld = record.label.obj.position;
|
|
1154
|
+
const pProj = ctx.project(pWorld);
|
|
1155
|
+
const frontVisible = pProj.z <= 0.2;
|
|
1156
|
+
let backFacing = false;
|
|
1157
|
+
if (behavior.hideBackFacing) {
|
|
1158
|
+
const worldDir = pWorld.clone().normalize();
|
|
1159
|
+
backFacing = worldDir.dot(cameraForward) < -0.2;
|
|
1160
|
+
}
|
|
1161
|
+
if (frontVisible && !backFacing) {
|
|
1162
|
+
const ndcX = pProj.x * ctx.globalScale / ctx.aspect;
|
|
1163
|
+
const ndcY = pProj.y * ctx.globalScale;
|
|
1164
|
+
const sX = (ndcX * 0.5 + 0.5) * ctx.screenW;
|
|
1165
|
+
const sY = (-ndcY * 0.5 + 0.5) * ctx.screenH;
|
|
1166
|
+
const uniforms = getLabelUniforms(record.label.obj);
|
|
1167
|
+
if (uniforms) {
|
|
1168
|
+
const pixelH = uniforms.uSize.value.y * ctx.screenH * 0.8;
|
|
1169
|
+
const pixelW = uniforms.uSize.value.x * ctx.screenH * 0.8;
|
|
1170
|
+
targetAlpha = 1;
|
|
1171
|
+
if (targetAlpha > 0 && record.classKey === "chapter" && !isSpecial) {
|
|
1172
|
+
const dist = Math.sqrt(ndcX * ndcX + ndcY * ndcY);
|
|
1173
|
+
const fovWeight = THREE6__namespace.MathUtils.smoothstep(ctx.fov, 10, 40);
|
|
1174
|
+
const focusOuter = THREE6__namespace.MathUtils.lerp(0.82, 0.62, fovWeight);
|
|
1175
|
+
const focusInner = THREE6__namespace.MathUtils.lerp(0.24, 0.16, fovWeight);
|
|
1176
|
+
const centerFocus = 1 - THREE6__namespace.MathUtils.smoothstep(dist, focusInner, focusOuter);
|
|
1177
|
+
const chapterVisibility = THREE6__namespace.MathUtils.lerp(1, centerFocus, fovWeight);
|
|
1178
|
+
targetAlpha *= chapterVisibility;
|
|
1179
|
+
if (dist > focusOuter && ctx.fov > 12) {
|
|
1180
|
+
targetAlpha = 0;
|
|
1181
|
+
}
|
|
1182
|
+
if (dist < 0.18 && ctx.fov < 58) {
|
|
1183
|
+
targetAlpha = Math.max(targetAlpha, 0.55);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (targetAlpha > 0 && (record.classKey === "book" || record.classKey === "group") && !isSpecial) {
|
|
1187
|
+
const dist = Math.sqrt(ndcX * ndcX + ndcY * ndcY);
|
|
1188
|
+
const fovWeight = THREE6__namespace.MathUtils.smoothstep(ctx.fov, 15, 58);
|
|
1189
|
+
const focusOuter = THREE6__namespace.MathUtils.lerp(0.95, 0.7, fovWeight);
|
|
1190
|
+
const focusInner = THREE6__namespace.MathUtils.lerp(0.35, 0.22, fovWeight);
|
|
1191
|
+
const centerFocus = 1 - THREE6__namespace.MathUtils.smoothstep(dist, focusInner, focusOuter);
|
|
1192
|
+
const bookVisibility = THREE6__namespace.MathUtils.lerp(1, centerFocus, fovWeight);
|
|
1193
|
+
targetAlpha *= bookVisibility;
|
|
1194
|
+
if (dist > focusOuter && ctx.fov > 20) {
|
|
1195
|
+
targetAlpha = 0;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (targetAlpha > 0 && ctx.shouldFilter) {
|
|
1199
|
+
const node = record.label.node;
|
|
1200
|
+
if (node.level === 3) {
|
|
1201
|
+
targetAlpha = 0;
|
|
1202
|
+
} else if (node.level === 2 || node.level === 2.5) {
|
|
1203
|
+
if (ctx.isNodeFiltered(node)) targetAlpha = 0;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (targetAlpha > 0 && record.classKey === "chapter" && record.label.chapterStarWorldPos) {
|
|
1207
|
+
const starProj = ctx.project(record.label.chapterStarWorldPos);
|
|
1208
|
+
if (starProj.z <= 0.2) {
|
|
1209
|
+
const starNdcX = starProj.x * ctx.globalScale / ctx.aspect;
|
|
1210
|
+
const starNdcY = starProj.y * ctx.globalScale;
|
|
1211
|
+
const starSX = (starNdcX * 0.5 + 0.5) * ctx.screenW;
|
|
1212
|
+
const starSY = (-starNdcY * 0.5 + 0.5) * ctx.screenH;
|
|
1213
|
+
const rect = {
|
|
1214
|
+
x: sX - pixelW / 2,
|
|
1215
|
+
y: sY - pixelH / 2,
|
|
1216
|
+
w: pixelW,
|
|
1217
|
+
h: pixelH,
|
|
1218
|
+
priority: classBehavior.priority
|
|
1219
|
+
};
|
|
1220
|
+
const glowRadiusPx = record.label.chapterGlowRadiusPx ?? 18;
|
|
1221
|
+
const clearancePx = Math.max(1, glowRadiusPx * 0.02);
|
|
1222
|
+
const distToLabel = boundsDistPoint(rect, starSX, starSY);
|
|
1223
|
+
if (glowRadiusPx >= 70) {
|
|
1224
|
+
const threshold = glowRadiusPx + clearancePx;
|
|
1225
|
+
const visibility = THREE6__namespace.MathUtils.smoothstep(distToLabel, threshold - 4, threshold + 2);
|
|
1226
|
+
const lowFovRelief = 1 - THREE6__namespace.MathUtils.smoothstep(ctx.fov, 8, 18);
|
|
1227
|
+
const boostedVisibility = isSpecial ? Math.max(visibility, 0.92) : Math.max(visibility, lowFovRelief * 0.85);
|
|
1228
|
+
targetAlpha *= boostedVisibility;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (record.classKey === "division" && uniforms.uAngle) {
|
|
1233
|
+
angleTarget = 0;
|
|
1234
|
+
if (ctx.projectionId !== "perspective") {
|
|
1235
|
+
const dx = sX - ctx.screenW / 2;
|
|
1236
|
+
const dy = sY - ctx.screenH / 2;
|
|
1237
|
+
angleTarget = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (targetAlpha > 0) {
|
|
1241
|
+
const priorityBoost = isSpecial ? record.id === ctx.selectedId ? 400 : 300 : 0;
|
|
1242
|
+
candidates.push({
|
|
1243
|
+
record,
|
|
1244
|
+
behavior: classBehavior,
|
|
1245
|
+
uniforms,
|
|
1246
|
+
sX,
|
|
1247
|
+
sY,
|
|
1248
|
+
w: pixelW,
|
|
1249
|
+
h: pixelH,
|
|
1250
|
+
ndcX,
|
|
1251
|
+
ndcY,
|
|
1252
|
+
priority: classBehavior.priority + priorityBoost,
|
|
1253
|
+
isPinned: isSpecial || classBehavior.mode === "pinned",
|
|
1254
|
+
isSpecial,
|
|
1255
|
+
centerDist: Math.sqrt((sX - ctx.screenW * 0.5) ** 2 + (sY - ctx.screenH * 0.5) ** 2)
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (typeof angleTarget === "number") {
|
|
1263
|
+
const material = record.label.obj.material;
|
|
1264
|
+
if (material instanceof THREE6__namespace.ShaderMaterial && material.uniforms.uAngle && typeof material.uniforms.uAngle.value === "number") {
|
|
1265
|
+
const current = material.uniforms.uAngle.value;
|
|
1266
|
+
material.uniforms.uAngle.value = lerp(current, angleTarget, 0.1);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
record.targetAlpha = targetAlpha;
|
|
1270
|
+
}
|
|
1271
|
+
candidates.sort((a, b) => {
|
|
1272
|
+
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
1273
|
+
if (a.record.lastAccepted !== b.record.lastAccepted) return a.record.lastAccepted ? -1 : 1;
|
|
1274
|
+
return a.centerDist - b.centerDist;
|
|
1275
|
+
});
|
|
1276
|
+
let guaranteedCenterChapterId = null;
|
|
1277
|
+
if (ctx.toggles.showChapterLabels && ctx.fov <= behavior.classes.chapter.maxFov + 15) {
|
|
1278
|
+
const centerChapter = candidates.filter((c) => c.record.classKey === "chapter" && c.record.targetAlpha > 0).sort((a, b) => a.centerDist - b.centerDist)[0];
|
|
1279
|
+
if (centerChapter) {
|
|
1280
|
+
guaranteedCenterChapterId = centerChapter.record.id;
|
|
1281
|
+
centerChapter.record.targetAlpha = Math.max(centerChapter.record.targetAlpha, 0.85);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const occupied = [];
|
|
1285
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const c of candidates) {
|
|
1287
|
+
if (c.record.targetAlpha <= 0) continue;
|
|
1288
|
+
const rect = {
|
|
1289
|
+
x: c.sX - c.w / 2 - behavior.overlapPaddingPx,
|
|
1290
|
+
y: c.sY - c.h / 2 - behavior.overlapPaddingPx,
|
|
1291
|
+
w: c.w + behavior.overlapPaddingPx * 2,
|
|
1292
|
+
h: c.h + behavior.overlapPaddingPx * 2,
|
|
1293
|
+
priority: c.priority
|
|
1294
|
+
};
|
|
1295
|
+
let rejected = false;
|
|
1296
|
+
const isGuaranteedCenterChapter = c.record.id === guaranteedCenterChapterId;
|
|
1297
|
+
if (!c.isPinned && !isGuaranteedCenterChapter) {
|
|
1298
|
+
if (!c.record.lastAccepted && !c.isSpecial && c.record.lastRejectedAtMs > 0) {
|
|
1299
|
+
const sinceReject = ctx.nowMs - c.record.lastRejectedAtMs;
|
|
1300
|
+
if (sinceReject < behavior.reappearDelayMs) {
|
|
1301
|
+
rejected = true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (!rejected) {
|
|
1305
|
+
for (const other of occupied) {
|
|
1306
|
+
if (other.priority < c.priority) continue;
|
|
1307
|
+
const overlapDepth = boundsOverlapDepth(rect, other);
|
|
1308
|
+
if (overlapDepth > c.behavior.maxOverlapPx) {
|
|
1309
|
+
rejected = true;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (rejected) {
|
|
1316
|
+
c.record.lastAccepted = false;
|
|
1317
|
+
c.record.lastRejectedAtMs = ctx.nowMs;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
occupied.push(rect);
|
|
1321
|
+
accepted.add(c.record.id);
|
|
1322
|
+
c.record.lastAccepted = true;
|
|
1323
|
+
}
|
|
1324
|
+
for (const record of this.records.values()) {
|
|
1325
|
+
const acceptedThisFrame = accepted.has(record.id) && record.targetAlpha > 0;
|
|
1326
|
+
record.fader.target = acceptedThisFrame;
|
|
1327
|
+
record.fader.update(ctx.dt);
|
|
1328
|
+
const baseAlpha = acceptedThisFrame ? record.targetAlpha : record.targetAlpha > 0 ? 1 : 0;
|
|
1329
|
+
const alpha = record.fader.eased * baseAlpha;
|
|
1330
|
+
applyUniformAlpha(record.label.obj, alpha);
|
|
1331
|
+
record.label.obj.visible = alpha > 0.01;
|
|
1332
|
+
if (!acceptedThisFrame) {
|
|
1333
|
+
record.lastAccepted = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
870
1336
|
}
|
|
871
1337
|
};
|
|
872
1338
|
}
|
|
@@ -893,6 +1359,7 @@ function createEngine({
|
|
|
893
1359
|
}) {
|
|
894
1360
|
let hoveredBookId = null;
|
|
895
1361
|
let focusedBookId = null;
|
|
1362
|
+
let focusedNodeId = null;
|
|
896
1363
|
let orderRevealEnabled = true;
|
|
897
1364
|
let activeBookIndex = -1;
|
|
898
1365
|
let orderRevealStrength = 0;
|
|
@@ -911,26 +1378,15 @@ function createEngine({
|
|
|
911
1378
|
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
912
1379
|
const testamentToIndex = /* @__PURE__ */ new Map();
|
|
913
1380
|
const divisionToIndex = /* @__PURE__ */ new Map();
|
|
914
|
-
const renderer = new
|
|
1381
|
+
const renderer = new THREE6__namespace.WebGLRenderer({ antialias: true, alpha: false });
|
|
915
1382
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
916
1383
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
917
1384
|
container.appendChild(renderer.domElement);
|
|
918
|
-
const scene = new
|
|
919
|
-
scene.background =
|
|
920
|
-
const camera = new
|
|
1385
|
+
const scene = new THREE6__namespace.Scene();
|
|
1386
|
+
scene.background = null;
|
|
1387
|
+
const camera = new THREE6__namespace.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
921
1388
|
camera.position.set(0, 0, 0);
|
|
922
1389
|
camera.up.set(0, 1, 0);
|
|
923
|
-
function setHoveredBook(id) {
|
|
924
|
-
if (id === hoveredBookId) return;
|
|
925
|
-
const now = performance.now();
|
|
926
|
-
if (hoveredBookId) {
|
|
927
|
-
hoverCooldowns.set(hoveredBookId, now);
|
|
928
|
-
}
|
|
929
|
-
if (id) {
|
|
930
|
-
hoverCooldowns.get(id) || 0;
|
|
931
|
-
}
|
|
932
|
-
hoveredBookId = id;
|
|
933
|
-
}
|
|
934
1390
|
let running = false;
|
|
935
1391
|
let raf = 0;
|
|
936
1392
|
const state = {
|
|
@@ -968,21 +1424,120 @@ function createEngine({
|
|
|
968
1424
|
longPressTimer: null,
|
|
969
1425
|
longPressTriggered: false
|
|
970
1426
|
};
|
|
971
|
-
const mouseNDC = new
|
|
1427
|
+
const mouseNDC = new THREE6__namespace.Vector2();
|
|
972
1428
|
let isMouseInWindow = false;
|
|
973
1429
|
let isTouchDevice = false;
|
|
974
1430
|
let edgeHoverStart = 0;
|
|
975
1431
|
let handlers = { onSelect, onHover, onArrangementChange, onFovChange, onLongPress };
|
|
976
1432
|
let currentConfig;
|
|
1433
|
+
function getSceneDebug() {
|
|
1434
|
+
return currentConfig?.debug?.sceneMechanics;
|
|
1435
|
+
}
|
|
1436
|
+
function getFreezeBand() {
|
|
1437
|
+
const dbg = getSceneDebug();
|
|
1438
|
+
const startRaw = dbg?.freezeBandStartFov ?? ENGINE_CONFIG.freezeBandStartFov;
|
|
1439
|
+
const endRaw = dbg?.freezeBandEndFov ?? ENGINE_CONFIG.freezeBandEndFov;
|
|
1440
|
+
const start2 = Math.min(startRaw, endRaw);
|
|
1441
|
+
const end = Math.max(startRaw, endRaw);
|
|
1442
|
+
return { start: start2, end };
|
|
1443
|
+
}
|
|
1444
|
+
function isInTransitionFreezeBand(fov) {
|
|
1445
|
+
const band = getFreezeBand();
|
|
1446
|
+
return fov >= band.start && fov <= band.end;
|
|
1447
|
+
}
|
|
1448
|
+
function getZenithBiasStartFov() {
|
|
1449
|
+
return getSceneDebug()?.zenithBiasStartFov ?? ENGINE_CONFIG.zenithBiasStartFov;
|
|
1450
|
+
}
|
|
1451
|
+
function getVerticalPanDampConfig() {
|
|
1452
|
+
const dbg = getSceneDebug();
|
|
1453
|
+
const fovStartRaw = dbg?.verticalPanDampStartFov ?? ENGINE_CONFIG.verticalPanDampStartFov;
|
|
1454
|
+
const fovEndRaw = dbg?.verticalPanDampEndFov ?? ENGINE_CONFIG.verticalPanDampEndFov;
|
|
1455
|
+
const latStartRaw = dbg?.verticalPanDampLatStartDeg ?? ENGINE_CONFIG.verticalPanDampLatStartDeg;
|
|
1456
|
+
const latEndRaw = dbg?.verticalPanDampLatEndDeg ?? ENGINE_CONFIG.verticalPanDampLatEndDeg;
|
|
1457
|
+
return {
|
|
1458
|
+
fovStart: Math.min(fovStartRaw, fovEndRaw),
|
|
1459
|
+
fovEnd: Math.max(fovStartRaw, fovEndRaw),
|
|
1460
|
+
latStartDeg: Math.min(latStartRaw, latEndRaw),
|
|
1461
|
+
latEndDeg: Math.max(latStartRaw, latEndRaw)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function getVerticalPanFactor(fov, lat) {
|
|
1465
|
+
if (zenithProjectionLockActive) return 0;
|
|
1466
|
+
const cfg = getVerticalPanDampConfig();
|
|
1467
|
+
const fovT = THREE6__namespace.MathUtils.smoothstep(fov, cfg.fovStart, cfg.fovEnd);
|
|
1468
|
+
const zenithT = THREE6__namespace.MathUtils.smoothstep(
|
|
1469
|
+
Math.max(lat, 0),
|
|
1470
|
+
THREE6__namespace.MathUtils.degToRad(cfg.latStartDeg),
|
|
1471
|
+
THREE6__namespace.MathUtils.degToRad(cfg.latEndDeg)
|
|
1472
|
+
);
|
|
1473
|
+
const lock = Math.max(fovT * 0.65, fovT * zenithT);
|
|
1474
|
+
return THREE6__namespace.MathUtils.clamp(1 - lock, 0, 1);
|
|
1475
|
+
}
|
|
1476
|
+
function getMovementMassFactor(fov, wideFovFactor = ENGINE_CONFIG.movementMassWideFov) {
|
|
1477
|
+
const t = THREE6__namespace.MathUtils.smoothstep(fov, 24, 96);
|
|
1478
|
+
return THREE6__namespace.MathUtils.lerp(1, wideFovFactor, t);
|
|
1479
|
+
}
|
|
1480
|
+
function compressInputDelta(delta) {
|
|
1481
|
+
const absDelta = Math.abs(delta);
|
|
1482
|
+
if (absDelta < 1e-4) return 0;
|
|
1483
|
+
return Math.sign(delta) * (absDelta / (1 + absDelta * ENGINE_CONFIG.inputCompression));
|
|
1484
|
+
}
|
|
977
1485
|
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
978
1486
|
function mix(a, b, t) {
|
|
979
1487
|
return a * (1 - t) + b * t;
|
|
980
1488
|
}
|
|
981
1489
|
let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
1490
|
+
let zenithProjectionLockActive = false;
|
|
1491
|
+
function getZenithLockBlendThresholds() {
|
|
1492
|
+
const enterRaw = ENGINE_CONFIG.zenithLockBlendEnter;
|
|
1493
|
+
const exitRaw = ENGINE_CONFIG.zenithLockBlendExit;
|
|
1494
|
+
return {
|
|
1495
|
+
enter: Math.max(0, Math.min(1, enterRaw)),
|
|
1496
|
+
exit: Math.max(0, Math.min(1, Math.min(exitRaw, enterRaw)))
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function getZenithLockLat() {
|
|
1500
|
+
return Math.PI / 2 - 1e-3;
|
|
1501
|
+
}
|
|
1502
|
+
function getBlendForZenithControl() {
|
|
1503
|
+
if (currentProjection instanceof BlendedProjection) return currentProjection.getBlend();
|
|
1504
|
+
return 0;
|
|
1505
|
+
}
|
|
1506
|
+
function applyZenithAutoCenter() {
|
|
1507
|
+
const zenithLat = getZenithLockLat();
|
|
1508
|
+
const blend = getBlendForZenithControl();
|
|
1509
|
+
let pullT = THREE6__namespace.MathUtils.smoothstep(
|
|
1510
|
+
blend,
|
|
1511
|
+
ENGINE_CONFIG.zenithAutoCenterBlendStart,
|
|
1512
|
+
ENGINE_CONFIG.zenithAutoCenterBlendEnd
|
|
1513
|
+
);
|
|
1514
|
+
if (zenithProjectionLockActive) pullT = 1;
|
|
1515
|
+
if (pullT <= 1e-4) return;
|
|
1516
|
+
const pullLerp = THREE6__namespace.MathUtils.lerp(
|
|
1517
|
+
ENGINE_CONFIG.zenithAutoCenterMinLerp,
|
|
1518
|
+
ENGINE_CONFIG.zenithAutoCenterMaxLerp,
|
|
1519
|
+
pullT
|
|
1520
|
+
);
|
|
1521
|
+
state.lat = THREE6__namespace.MathUtils.lerp(state.lat, zenithLat, pullLerp);
|
|
1522
|
+
state.targetLat = THREE6__namespace.MathUtils.lerp(state.targetLat, zenithLat, Math.min(1, pullLerp * 1.15));
|
|
1523
|
+
state.velocityY *= 1 - 0.85 * pullT;
|
|
1524
|
+
if (zenithProjectionLockActive && Math.abs(state.lat - zenithLat) < 25e-5) {
|
|
1525
|
+
state.lat = zenithLat;
|
|
1526
|
+
state.targetLat = zenithLat;
|
|
1527
|
+
state.velocityY = 0;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
982
1530
|
function syncProjectionState() {
|
|
983
1531
|
if (currentProjection instanceof BlendedProjection) {
|
|
984
1532
|
currentProjection.setFov(state.fov);
|
|
1533
|
+
currentProjection.setBlendOverride(getSceneDebug()?.projectionBlendOverride ?? null);
|
|
985
1534
|
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
1535
|
+
const blend = currentProjection.getBlend();
|
|
1536
|
+
const th = getZenithLockBlendThresholds();
|
|
1537
|
+
if (!zenithProjectionLockActive && blend >= th.enter) zenithProjectionLockActive = true;
|
|
1538
|
+
else if (zenithProjectionLockActive && blend <= th.exit) zenithProjectionLockActive = false;
|
|
1539
|
+
} else {
|
|
1540
|
+
zenithProjectionLockActive = false;
|
|
986
1541
|
}
|
|
987
1542
|
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
988
1543
|
}
|
|
@@ -1009,7 +1564,7 @@ function createEngine({
|
|
|
1009
1564
|
const uvX = mouseNDC.x * aspectRatio;
|
|
1010
1565
|
const uvY = mouseNDC.y;
|
|
1011
1566
|
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
1012
|
-
return new
|
|
1567
|
+
return new THREE6__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
1013
1568
|
}
|
|
1014
1569
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
1015
1570
|
const aspect = width / height;
|
|
@@ -1018,7 +1573,7 @@ function createEngine({
|
|
|
1018
1573
|
syncProjectionState();
|
|
1019
1574
|
const fovRad = state.fov * Math.PI / 180;
|
|
1020
1575
|
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
1021
|
-
const vView = new
|
|
1576
|
+
const vView = new THREE6__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
1022
1577
|
return vView.applyQuaternion(camera.quaternion);
|
|
1023
1578
|
}
|
|
1024
1579
|
function smartProjectJS(worldPos) {
|
|
@@ -1028,135 +1583,871 @@ function createEngine({
|
|
|
1028
1583
|
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
1029
1584
|
return result;
|
|
1030
1585
|
}
|
|
1031
|
-
const groundGroup = new
|
|
1586
|
+
const groundGroup = new THREE6__namespace.Group();
|
|
1032
1587
|
scene.add(groundGroup);
|
|
1588
|
+
const MAX_HORIZON_POINTS = 64;
|
|
1589
|
+
let groundMaterial = null;
|
|
1590
|
+
let horizonLine = null;
|
|
1591
|
+
let activeHorizonProfile = {
|
|
1592
|
+
mode: 0,
|
|
1593
|
+
pointCount: 0,
|
|
1594
|
+
azDeg: [],
|
|
1595
|
+
altDeg: [],
|
|
1596
|
+
rotateRad: 0,
|
|
1597
|
+
baseAltDeg: 3
|
|
1598
|
+
};
|
|
1599
|
+
let lastHorizonDiagTs = 0;
|
|
1600
|
+
function toColor(input, fallbackHex) {
|
|
1601
|
+
if (!input) return new THREE6__namespace.Color(fallbackHex);
|
|
1602
|
+
try {
|
|
1603
|
+
return new THREE6__namespace.Color(input);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return new THREE6__namespace.Color(fallbackHex);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function applyGroundTheme(cfg) {
|
|
1609
|
+
if (!groundMaterial) return;
|
|
1610
|
+
const theme = getSceneDebug()?.disableHorizonTheme ? void 0 : cfg?.horizonTheme;
|
|
1611
|
+
const uniforms = groundMaterial.uniforms;
|
|
1612
|
+
const atmo = theme?.atmosphere;
|
|
1613
|
+
const mode = theme?.source === "polygonal" && (theme.profile?.points?.length ?? 0) >= 2 ? 1 : 0;
|
|
1614
|
+
const groundColor = toColor(theme?.groundColor, 65794);
|
|
1615
|
+
const fogColor = toColor(theme?.horizonLineColor, 663098);
|
|
1616
|
+
const fogIntensity = THREE6__namespace.MathUtils.clamp(atmo?.fogIntensity ?? 0.6, 0, 1.5);
|
|
1617
|
+
const fogVisible = atmo?.fogVisible === false ? 0 : 1;
|
|
1618
|
+
const minBrightness = THREE6__namespace.MathUtils.clamp(atmo?.minimalBrightness ?? 0, 0, 1);
|
|
1619
|
+
const rotateRad = (theme?.profile?.angleRotateZDeg ?? 0) * Math.PI / 180;
|
|
1620
|
+
const azSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1621
|
+
const altSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1622
|
+
let pointCount = 0;
|
|
1623
|
+
let sortedPoints = [];
|
|
1624
|
+
if (mode === 1 && theme?.profile?.points) {
|
|
1625
|
+
sortedPoints = [...theme.profile.points].map((p) => ({
|
|
1626
|
+
azDeg: (p.azDeg % 360 + 360) % 360,
|
|
1627
|
+
altDeg: THREE6__namespace.MathUtils.clamp(p.altDeg, -30, 35)
|
|
1628
|
+
})).sort((a, b) => a.azDeg - b.azDeg);
|
|
1629
|
+
pointCount = Math.min(sortedPoints.length, MAX_HORIZON_POINTS);
|
|
1630
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1631
|
+
azSamples[i] = sortedPoints[i].azDeg;
|
|
1632
|
+
altSamples[i] = sortedPoints[i].altDeg;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const baseAltDeg = pointCount > 0 ? altSamples.slice(0, pointCount).reduce((sum, v) => sum + v, 0) / pointCount : 3;
|
|
1636
|
+
activeHorizonProfile = {
|
|
1637
|
+
mode,
|
|
1638
|
+
pointCount,
|
|
1639
|
+
azDeg: azSamples.slice(0, pointCount),
|
|
1640
|
+
altDeg: altSamples.slice(0, pointCount),
|
|
1641
|
+
rotateRad,
|
|
1642
|
+
baseAltDeg
|
|
1643
|
+
};
|
|
1644
|
+
uniforms.color.value = groundColor;
|
|
1645
|
+
uniforms.fogColor.value = fogColor;
|
|
1646
|
+
uniforms.uFogIntensity.value = fogIntensity;
|
|
1647
|
+
uniforms.uFogVisible.value = fogVisible;
|
|
1648
|
+
uniforms.uMinBrightness.value = minBrightness;
|
|
1649
|
+
uniforms.uHorizonMode.value = mode;
|
|
1650
|
+
uniforms.uHorizonPointCount.value = pointCount;
|
|
1651
|
+
uniforms.uHorizonAzDeg.value = azSamples;
|
|
1652
|
+
uniforms.uHorizonAltDeg.value = altSamples;
|
|
1653
|
+
uniforms.uHorizonRotateRad.value = rotateRad;
|
|
1654
|
+
uniforms.uBaseAltDeg.value = baseAltDeg;
|
|
1655
|
+
groundMaterial.uniformsNeedUpdate = true;
|
|
1656
|
+
if (atmosphereMesh && atmosphereMesh.material instanceof THREE6__namespace.ShaderMaterial) {
|
|
1657
|
+
const atmUniforms = atmosphereMesh.material.uniforms;
|
|
1658
|
+
const topAltDeg = atmo?.fogBandTopAltDeg ?? 90;
|
|
1659
|
+
const bottomAltDeg = atmo?.fogBandBottomAltDeg ?? -90;
|
|
1660
|
+
atmUniforms.uThemeFogVisible.value = fogVisible;
|
|
1661
|
+
atmUniforms.uThemeFogIntensity.value = fogIntensity;
|
|
1662
|
+
atmUniforms.uThemeFogTopSin.value = Math.sin(THREE6__namespace.MathUtils.degToRad(topAltDeg));
|
|
1663
|
+
atmUniforms.uThemeFogBottomSin.value = Math.sin(THREE6__namespace.MathUtils.degToRad(bottomAltDeg));
|
|
1664
|
+
atmUniforms.uThemeMinBrightness.value = minBrightness;
|
|
1665
|
+
atmosphereMesh.material.uniformsNeedUpdate = true;
|
|
1666
|
+
}
|
|
1667
|
+
if (horizonLine) {
|
|
1668
|
+
groundGroup.remove(horizonLine);
|
|
1669
|
+
horizonLine.geometry.dispose();
|
|
1670
|
+
horizonLine.material.dispose();
|
|
1671
|
+
horizonLine = null;
|
|
1672
|
+
}
|
|
1673
|
+
const lineThickness = THREE6__namespace.MathUtils.clamp(theme?.horizonLineThickness ?? 0, 0, 8);
|
|
1674
|
+
const shouldDrawLine = mode === 1 && pointCount >= 2 && lineThickness > 0;
|
|
1675
|
+
if (!shouldDrawLine) return;
|
|
1676
|
+
const lineColor = toColor(theme?.horizonLineColor, 5601177);
|
|
1677
|
+
const lineRadius = 997;
|
|
1678
|
+
const pts = [];
|
|
1679
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1680
|
+
const sample = sortedPoints[i];
|
|
1681
|
+
const angleDeg = sample.azDeg - (theme?.profile?.angleRotateZDeg ?? 0);
|
|
1682
|
+
const a = THREE6__namespace.MathUtils.degToRad(angleDeg);
|
|
1683
|
+
const alt = THREE6__namespace.MathUtils.degToRad(sample.altDeg);
|
|
1684
|
+
const rc = Math.cos(alt);
|
|
1685
|
+
pts.push(new THREE6__namespace.Vector3(
|
|
1686
|
+
lineRadius * rc * Math.cos(a),
|
|
1687
|
+
lineRadius * Math.sin(alt),
|
|
1688
|
+
lineRadius * rc * Math.sin(a)
|
|
1689
|
+
));
|
|
1690
|
+
}
|
|
1691
|
+
if (pts.length > 0) pts.push(pts[0].clone());
|
|
1692
|
+
const geo = new THREE6__namespace.BufferGeometry().setFromPoints(pts);
|
|
1693
|
+
const mat = createSmartMaterial({
|
|
1694
|
+
uniforms: {
|
|
1695
|
+
color: { value: lineColor },
|
|
1696
|
+
alpha: { value: 0.95 }
|
|
1697
|
+
},
|
|
1698
|
+
vertexShaderBody: `
|
|
1699
|
+
uniform vec3 color;
|
|
1700
|
+
varying vec3 vColor;
|
|
1701
|
+
void main() {
|
|
1702
|
+
vColor = color;
|
|
1703
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1704
|
+
gl_Position = smartProject(mvPosition);
|
|
1705
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1706
|
+
}
|
|
1707
|
+
`,
|
|
1708
|
+
fragmentShader: `
|
|
1709
|
+
uniform float alpha;
|
|
1710
|
+
varying vec3 vColor;
|
|
1711
|
+
void main() {
|
|
1712
|
+
float alphaMask = getMaskAlpha();
|
|
1713
|
+
if (alphaMask < 0.01) discard;
|
|
1714
|
+
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
1715
|
+
}
|
|
1716
|
+
`,
|
|
1717
|
+
transparent: true,
|
|
1718
|
+
depthWrite: false,
|
|
1719
|
+
depthTest: true
|
|
1720
|
+
});
|
|
1721
|
+
const line = new THREE6__namespace.Line(geo, mat);
|
|
1722
|
+
line.material.linewidth = lineThickness;
|
|
1723
|
+
line.frustumCulled = false;
|
|
1724
|
+
line.renderOrder = 3;
|
|
1725
|
+
horizonLine = line;
|
|
1726
|
+
groundGroup.add(line);
|
|
1727
|
+
}
|
|
1728
|
+
function sampleActiveHorizonAltDeg(azDeg) {
|
|
1729
|
+
const profile = activeHorizonProfile;
|
|
1730
|
+
if (profile.mode !== 1 || profile.pointCount < 2) return profile.baseAltDeg;
|
|
1731
|
+
const query = ((azDeg + THREE6__namespace.MathUtils.radToDeg(profile.rotateRad)) % 360 + 360) % 360;
|
|
1732
|
+
const n = profile.pointCount;
|
|
1733
|
+
const firstAz = profile.azDeg[0];
|
|
1734
|
+
const firstAlt = profile.altDeg[0];
|
|
1735
|
+
for (let i = 1; i < n; i++) {
|
|
1736
|
+
const prevAz2 = profile.azDeg[i - 1];
|
|
1737
|
+
const prevAlt2 = profile.altDeg[i - 1];
|
|
1738
|
+
const curAz = profile.azDeg[i];
|
|
1739
|
+
const curAlt = profile.altDeg[i];
|
|
1740
|
+
if (query >= prevAz2 && query <= curAz) {
|
|
1741
|
+
const t2 = (query - prevAz2) / Math.max(1e-4, curAz - prevAz2);
|
|
1742
|
+
return mix(prevAlt2, curAlt, t2);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
const prevAz = profile.azDeg[n - 1];
|
|
1746
|
+
const prevAlt = profile.altDeg[n - 1];
|
|
1747
|
+
const wrappedQuery = query < firstAz ? query + 360 : query;
|
|
1748
|
+
const t = (wrappedQuery - prevAz) / Math.max(1e-4, firstAz + 360 - prevAz);
|
|
1749
|
+
return mix(prevAlt, firstAlt, t);
|
|
1750
|
+
}
|
|
1751
|
+
function runHorizonDiagnostics(nowMs) {
|
|
1752
|
+
if (nowMs - lastHorizonDiagTs < 1200) return;
|
|
1753
|
+
lastHorizonDiagTs = nowMs;
|
|
1754
|
+
const points = [];
|
|
1755
|
+
const r = 997;
|
|
1756
|
+
const scale = globalUniforms.uScale.value;
|
|
1757
|
+
const aspect = Math.max(1e-4, globalUniforms.uAspect.value);
|
|
1758
|
+
for (let az = 0; az < 360; az += 2) {
|
|
1759
|
+
const altDeg = sampleActiveHorizonAltDeg(az);
|
|
1760
|
+
const azRad = THREE6__namespace.MathUtils.degToRad(az);
|
|
1761
|
+
const altRad = THREE6__namespace.MathUtils.degToRad(altDeg);
|
|
1762
|
+
const rc = Math.cos(altRad);
|
|
1763
|
+
const worldPos = new THREE6__namespace.Vector3(
|
|
1764
|
+
r * rc * Math.cos(azRad),
|
|
1765
|
+
r * Math.sin(altRad),
|
|
1766
|
+
r * rc * Math.sin(azRad)
|
|
1767
|
+
);
|
|
1768
|
+
const p = smartProjectJS(worldPos);
|
|
1769
|
+
if (currentProjection.isClipped(p.z)) continue;
|
|
1770
|
+
const x = p.x * scale / aspect;
|
|
1771
|
+
const y = p.y * scale;
|
|
1772
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
1773
|
+
if (Math.abs(x) > 1) continue;
|
|
1774
|
+
points.push({ x, y });
|
|
1775
|
+
}
|
|
1776
|
+
if (points.length < 16) {
|
|
1777
|
+
console.debug(`[HorizonDiag] insufficient visible horizon samples at fov=${state.fov.toFixed(1)}`);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
const binCount = 12;
|
|
1781
|
+
const maxY = new Array(binCount).fill(-Infinity);
|
|
1782
|
+
for (const p of points) {
|
|
1783
|
+
const ax = Math.min(0.999, Math.abs(p.x));
|
|
1784
|
+
const idx = Math.floor(ax * binCount);
|
|
1785
|
+
maxY[idx] = Math.max(maxY[idx], p.y);
|
|
1786
|
+
}
|
|
1787
|
+
const compact = maxY.map((v) => Number.isFinite(v) ? Number(v.toFixed(3)) : null);
|
|
1788
|
+
let dropCount = 0;
|
|
1789
|
+
for (let i = 1; i < binCount; i++) {
|
|
1790
|
+
const prev = maxY[i - 1];
|
|
1791
|
+
const cur = maxY[i];
|
|
1792
|
+
if (!Number.isFinite(prev) || !Number.isFinite(cur)) continue;
|
|
1793
|
+
if (cur < prev - 0.02) dropCount++;
|
|
1794
|
+
}
|
|
1795
|
+
const flatten = groundMaterial?.uniforms?.uZenithFlatten?.value;
|
|
1796
|
+
const blend = currentProjection instanceof BlendedProjection ? currentProjection.getBlend() : -1;
|
|
1797
|
+
const freeze = isInTransitionFreezeBand(state.fov) ? 1 : 0;
|
|
1798
|
+
const zenithBiasStart = getZenithBiasStartFov();
|
|
1799
|
+
const vPanCfg = getVerticalPanDampConfig();
|
|
1800
|
+
const vPan = getVerticalPanFactor(state.fov, state.lat);
|
|
1801
|
+
const moveMass = getMovementMassFactor(state.fov);
|
|
1802
|
+
console.debug(
|
|
1803
|
+
`[HorizonDiag] fov=${state.fov.toFixed(1)} latDeg=${THREE6__namespace.MathUtils.radToDeg(state.lat).toFixed(1)} mode=${activeHorizonProfile.mode} blend=${blend.toFixed(3)} freeze=${freeze} zLock=${zenithProjectionLockActive ? 1 : 0} biasStart=${zenithBiasStart.toFixed(1)} vPan=${vPan.toFixed(3)} moveMass=${moveMass.toFixed(3)} vPanFov=${vPanCfg.fovStart.toFixed(1)}-${vPanCfg.fovEnd.toFixed(1)} vPanLat=${vPanCfg.latStartDeg.toFixed(1)}-${vPanCfg.latEndDeg.toFixed(1)} flatten=${Number(flatten ?? 0).toFixed(3)} drops=${dropCount} bins=${JSON.stringify(compact)}`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1033
1806
|
function createGround() {
|
|
1034
1807
|
groundGroup.clear();
|
|
1035
1808
|
const radius = 995;
|
|
1036
|
-
const geometry = new
|
|
1809
|
+
const geometry = new THREE6__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
1037
1810
|
const material = createSmartMaterial({
|
|
1038
1811
|
uniforms: {
|
|
1039
|
-
color: { value: new
|
|
1040
|
-
fogColor: { value: new
|
|
1812
|
+
color: { value: new THREE6__namespace.Color(65794) },
|
|
1813
|
+
fogColor: { value: new THREE6__namespace.Color(663098) },
|
|
1814
|
+
uFogIntensity: { value: 0.6 },
|
|
1815
|
+
uFogVisible: { value: 1 },
|
|
1816
|
+
uMinBrightness: { value: 0 },
|
|
1817
|
+
uHorizonMode: { value: 0 },
|
|
1818
|
+
uHorizonPointCount: { value: 0 },
|
|
1819
|
+
uHorizonAzDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1820
|
+
uHorizonAltDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1821
|
+
uHorizonRotateRad: { value: 0 },
|
|
1822
|
+
uHorizonRadius: { value: radius },
|
|
1823
|
+
uBaseAltDeg: { value: 3 },
|
|
1824
|
+
uZenithFlatten: { value: 0 }
|
|
1041
1825
|
},
|
|
1042
1826
|
vertexShaderBody: `
|
|
1043
|
-
varying vec3 vPos;
|
|
1044
|
-
varying vec3 vWorldPos;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1827
|
+
varying vec3 vPos;
|
|
1828
|
+
varying vec3 vWorldPos;
|
|
1829
|
+
varying float vViewDirZ;
|
|
1830
|
+
void main() {
|
|
1831
|
+
vPos = position;
|
|
1832
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1833
|
+
gl_Position = smartProject(mvPosition);
|
|
1834
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1835
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
1836
|
+
vViewDirZ = normalize(mvPosition.xyz).z;
|
|
1837
|
+
}
|
|
1838
|
+
`,
|
|
1839
|
+
fragmentShader: `
|
|
1840
|
+
uniform vec3 color;
|
|
1841
|
+
uniform vec3 fogColor;
|
|
1842
|
+
uniform float uFogIntensity;
|
|
1843
|
+
uniform float uFogVisible;
|
|
1844
|
+
uniform float uMinBrightness;
|
|
1845
|
+
uniform int uHorizonMode;
|
|
1846
|
+
uniform int uHorizonPointCount;
|
|
1847
|
+
uniform float uHorizonAzDeg[64];
|
|
1848
|
+
uniform float uHorizonAltDeg[64];
|
|
1849
|
+
uniform float uHorizonRotateRad;
|
|
1850
|
+
uniform float uHorizonRadius;
|
|
1851
|
+
uniform float uBaseAltDeg;
|
|
1852
|
+
uniform float uZenithFlatten;
|
|
1853
|
+
varying vec3 vPos;
|
|
1854
|
+
varying vec3 vWorldPos;
|
|
1855
|
+
varying float vViewDirZ;
|
|
1856
|
+
|
|
1857
|
+
float samplePolygonalAltDeg(float azDeg) {
|
|
1858
|
+
if (uHorizonPointCount < 2) return 0.0;
|
|
1859
|
+
float z = mod(azDeg, 360.0);
|
|
1860
|
+
if (z < 0.0) z += 360.0;
|
|
1861
|
+
|
|
1862
|
+
float prevAz = uHorizonAzDeg[0];
|
|
1863
|
+
float prevAlt = uHorizonAltDeg[0];
|
|
1864
|
+
for (int i = 1; i < 64; i++) {
|
|
1865
|
+
if (i >= uHorizonPointCount) break;
|
|
1866
|
+
float curAz = uHorizonAzDeg[i];
|
|
1867
|
+
float curAlt = uHorizonAltDeg[i];
|
|
1868
|
+
if (z >= prevAz && z <= curAz) {
|
|
1869
|
+
float t = (z - prevAz) / max(0.0001, curAz - prevAz);
|
|
1870
|
+
return mix(prevAlt, curAlt, t);
|
|
1871
|
+
}
|
|
1872
|
+
prevAz = curAz;
|
|
1873
|
+
prevAlt = curAlt;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
float firstAz = uHorizonAzDeg[0] + 360.0;
|
|
1877
|
+
float firstAlt = uHorizonAltDeg[0];
|
|
1878
|
+
float zw = z;
|
|
1879
|
+
if (zw < uHorizonAzDeg[0]) zw += 360.0;
|
|
1880
|
+
float t = (zw - prevAz) / max(0.0001, firstAz - prevAz);
|
|
1881
|
+
return mix(prevAlt, firstAlt, t);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
void main() {
|
|
1885
|
+
float alphaMask = getMaskAlpha();
|
|
1886
|
+
if (alphaMask < 0.01) discard;
|
|
1887
|
+
|
|
1888
|
+
// Keep ground visibility aligned with the active projection clip.
|
|
1889
|
+
float clipZ = -0.1;
|
|
1890
|
+
if (uProjectionType == 1) {
|
|
1891
|
+
clipZ = 0.1;
|
|
1892
|
+
} else if (uProjectionType == 2) {
|
|
1893
|
+
clipZ = mix(-0.1, 0.1, clamp(uBlend, 0.0, 1.0));
|
|
1894
|
+
}
|
|
1895
|
+
if (vViewDirZ > clipZ) discard;
|
|
1896
|
+
|
|
1897
|
+
float angle = atan(vPos.z, vPos.x);
|
|
1898
|
+
float terrainHeight;
|
|
1899
|
+
|
|
1900
|
+
if (uHorizonMode == 1 && uHorizonPointCount >= 2) {
|
|
1901
|
+
float azDeg = mod(degrees(angle) + 360.0 + degrees(uHorizonRotateRad), 360.0);
|
|
1902
|
+
float altDeg = samplePolygonalAltDeg(azDeg);
|
|
1903
|
+
terrainHeight = uHorizonRadius * sin(radians(altDeg));
|
|
1904
|
+
} else {
|
|
1905
|
+
// Procedural Horizon (Mountains)
|
|
1906
|
+
float h = 0.0;
|
|
1907
|
+
h += sin(angle * 6.0) * 35.0;
|
|
1908
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1909
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1910
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1911
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1912
|
+
terrainHeight = h + 12.0;
|
|
1913
|
+
}
|
|
1914
|
+
float circularHeight = uHorizonRadius * sin(radians(uBaseAltDeg));
|
|
1915
|
+
terrainHeight = mix(terrainHeight, circularHeight, clamp(uZenithFlatten, 0.0, 1.0));
|
|
1916
|
+
|
|
1917
|
+
if (vPos.y > terrainHeight) discard;
|
|
1918
|
+
|
|
1919
|
+
// Atmospheric rim glow just below terrain peaks
|
|
1920
|
+
float rimDist = terrainHeight - vPos.y;
|
|
1921
|
+
float rim = exp(-rimDist * 0.15) * 0.4 * uFogVisible;
|
|
1922
|
+
vec3 rimColor = fogColor * 1.5;
|
|
1923
|
+
|
|
1924
|
+
// Atmospheric haze \u2014 stronger near horizon
|
|
1925
|
+
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1926
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * uFogIntensity * uFogVisible);
|
|
1927
|
+
|
|
1928
|
+
// Add rim glow near terrain peaks
|
|
1929
|
+
finalCol += rimColor * rim;
|
|
1930
|
+
finalCol = max(finalCol, color * uMinBrightness);
|
|
1931
|
+
|
|
1932
|
+
gl_FragColor = vec4(finalCol, 1.0);
|
|
1933
|
+
}
|
|
1934
|
+
`,
|
|
1935
|
+
side: THREE6__namespace.BackSide,
|
|
1936
|
+
transparent: false,
|
|
1937
|
+
depthWrite: true,
|
|
1938
|
+
depthTest: true
|
|
1939
|
+
});
|
|
1940
|
+
groundMaterial = material;
|
|
1941
|
+
const ground = new THREE6__namespace.Mesh(geometry, material);
|
|
1942
|
+
groundGroup.add(ground);
|
|
1943
|
+
applyGroundTheme(currentConfig);
|
|
1944
|
+
}
|
|
1945
|
+
let skyBackgroundMesh = null;
|
|
1946
|
+
let atmosphereMesh = null;
|
|
1947
|
+
let moonMesh = null;
|
|
1948
|
+
let moonGlowMesh = null;
|
|
1949
|
+
let sunDiscMesh = null;
|
|
1950
|
+
let sunHaloMesh = null;
|
|
1951
|
+
let milkyWayMesh = null;
|
|
1952
|
+
function createSkyBackground() {
|
|
1953
|
+
const geo = new THREE6__namespace.SphereGeometry(2400, 32, 32);
|
|
1954
|
+
const mat = createSmartMaterial({
|
|
1955
|
+
uniforms: {},
|
|
1956
|
+
vertexShaderBody: `
|
|
1957
|
+
varying vec3 vWorldNormal;
|
|
1958
|
+
void main() {
|
|
1959
|
+
vWorldNormal = normalize(position);
|
|
1960
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
1961
|
+
gl_Position = smartProject(mv);
|
|
1962
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1963
|
+
}
|
|
1964
|
+
`,
|
|
1965
|
+
fragmentShader: `
|
|
1966
|
+
varying vec3 vWorldNormal;
|
|
1967
|
+
void main() {
|
|
1968
|
+
float h = clamp(normalize(vWorldNormal).y, -1.0, 1.0);
|
|
1969
|
+
|
|
1970
|
+
// Scotopic-inspired 5-stop gradient.
|
|
1971
|
+
// Night sky: blue channel ~2.6x red, derived from CIE (x=0.25, y=0.25).
|
|
1972
|
+
vec3 cZenith = vec3(0.010, 0.022, 0.055);
|
|
1973
|
+
vec3 cUpper = vec3(0.015, 0.033, 0.080);
|
|
1974
|
+
vec3 cMid = vec3(0.022, 0.048, 0.108);
|
|
1975
|
+
vec3 cLower = vec3(0.035, 0.072, 0.148);
|
|
1976
|
+
vec3 cHorizon = vec3(0.052, 0.100, 0.190);
|
|
1977
|
+
|
|
1978
|
+
float t1 = smoothstep(0.0, 0.30, h);
|
|
1979
|
+
float t2 = smoothstep(0.3, 0.60, h);
|
|
1980
|
+
float t3 = smoothstep(0.6, 0.85, h);
|
|
1981
|
+
float t4 = smoothstep(0.85, 1.00, h);
|
|
1982
|
+
|
|
1983
|
+
vec3 col = cHorizon;
|
|
1984
|
+
col = mix(col, cLower, t1);
|
|
1985
|
+
col = mix(col, cMid, t2);
|
|
1986
|
+
col = mix(col, cUpper, t3);
|
|
1987
|
+
col = mix(col, cZenith, t4);
|
|
1988
|
+
|
|
1989
|
+
// Rayleigh limb brightening at horizon
|
|
1990
|
+
float limb = exp(-18.0 * abs(h)) * smoothstep(-0.05, 0.06, h);
|
|
1991
|
+
col += vec3(0.012, 0.024, 0.050) * limb;
|
|
1992
|
+
|
|
1993
|
+
// Below ground: fade to near-black
|
|
1994
|
+
float below = smoothstep(-0.04, -0.18, h);
|
|
1995
|
+
col = mix(col, vec3(0.002, 0.003, 0.006), below);
|
|
1996
|
+
|
|
1997
|
+
gl_FragColor = vec4(col, 1.0);
|
|
1998
|
+
}
|
|
1999
|
+
`,
|
|
2000
|
+
transparent: false,
|
|
2001
|
+
depthWrite: false,
|
|
2002
|
+
depthTest: false,
|
|
2003
|
+
side: THREE6__namespace.BackSide
|
|
2004
|
+
});
|
|
2005
|
+
skyBackgroundMesh = new THREE6__namespace.Mesh(geo, mat);
|
|
2006
|
+
skyBackgroundMesh.renderOrder = -2;
|
|
2007
|
+
skyBackgroundMesh.frustumCulled = false;
|
|
2008
|
+
scene.add(skyBackgroundMesh);
|
|
2009
|
+
}
|
|
2010
|
+
function createAtmosphere() {
|
|
2011
|
+
const geometry = new THREE6__namespace.SphereGeometry(990, 64, 64);
|
|
2012
|
+
const material = createSmartMaterial({
|
|
2013
|
+
uniforms: {
|
|
2014
|
+
uThemeFogVisible: { value: 1 },
|
|
2015
|
+
uThemeFogTopSin: { value: 0.95 },
|
|
2016
|
+
uThemeFogBottomSin: { value: -1 },
|
|
2017
|
+
uThemeFogIntensity: { value: 1 },
|
|
2018
|
+
uThemeMinBrightness: { value: 0 }
|
|
2019
|
+
},
|
|
2020
|
+
vertexShaderBody: `
|
|
2021
|
+
varying vec3 vWorldNormal;
|
|
2022
|
+
void main() {
|
|
2023
|
+
vWorldNormal = normalize(position);
|
|
2024
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
2025
|
+
gl_Position = smartProject(mv);
|
|
2026
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
2027
|
+
}`,
|
|
2028
|
+
fragmentShader: `
|
|
2029
|
+
varying vec3 vWorldNormal;
|
|
2030
|
+
|
|
2031
|
+
uniform float uAtmGlow;
|
|
2032
|
+
uniform float uAtmDark;
|
|
2033
|
+
uniform vec3 uColorHorizon;
|
|
2034
|
+
uniform vec3 uColorZenith;
|
|
2035
|
+
uniform float uThemeFogVisible;
|
|
2036
|
+
uniform float uThemeFogTopSin;
|
|
2037
|
+
uniform float uThemeFogBottomSin;
|
|
2038
|
+
uniform float uThemeFogIntensity;
|
|
2039
|
+
uniform float uThemeMinBrightness;
|
|
2040
|
+
|
|
2041
|
+
void main() {
|
|
2042
|
+
float alphaMask = getMaskAlpha();
|
|
2043
|
+
if (alphaMask < 0.01) discard;
|
|
2044
|
+
|
|
2045
|
+
// Altitude angle (Y is up)
|
|
2046
|
+
float h = normalize(vWorldNormal).y;
|
|
2047
|
+
|
|
2048
|
+
// 1. Base gradient from Horizon to Zenith (wider range)
|
|
2049
|
+
float t = smoothstep(-0.15, 0.7, h);
|
|
2050
|
+
|
|
2051
|
+
// Non-linear mix for realistic sky falloff
|
|
2052
|
+
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
2053
|
+
float hazeBand = smoothstep(uThemeFogBottomSin, uThemeFogTopSin, h);
|
|
2054
|
+
float hazeFadeEnd = max(uThemeFogTopSin + 0.001, min(1.0, uThemeFogTopSin + 0.25));
|
|
2055
|
+
hazeBand *= (1.0 - smoothstep(uThemeFogTopSin, hazeFadeEnd, h));
|
|
2056
|
+
float fogTheme = uThemeFogVisible * uThemeFogIntensity;
|
|
2057
|
+
|
|
2058
|
+
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
2059
|
+
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
2060
|
+
skyColor += vec3(0.05, 0.12, 0.15) * midBand * uAtmGlow;
|
|
2061
|
+
|
|
2062
|
+
// 3. Primary horizon glow band (wider than before)
|
|
2063
|
+
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
2064
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
2065
|
+
|
|
2066
|
+
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
2067
|
+
float warmGlow = exp(-8.0 * abs(h));
|
|
2068
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
2069
|
+
skyColor = max(skyColor, uColorZenith * (0.2 * uThemeMinBrightness));
|
|
2070
|
+
|
|
2071
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
2072
|
+
}
|
|
2073
|
+
`,
|
|
2074
|
+
side: THREE6__namespace.BackSide,
|
|
2075
|
+
depthWrite: false,
|
|
2076
|
+
depthTest: true
|
|
2077
|
+
});
|
|
2078
|
+
const atm = new THREE6__namespace.Mesh(geometry, material);
|
|
2079
|
+
atmosphereMesh = atm;
|
|
2080
|
+
groundGroup.add(atm);
|
|
2081
|
+
}
|
|
2082
|
+
function createMoon() {
|
|
2083
|
+
const moonDir = new THREE6__namespace.Vector3(-0.38, 0.62, -0.68).normalize();
|
|
2084
|
+
const moonWorldPos = moonDir.clone().multiplyScalar(2e3);
|
|
2085
|
+
const glowGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2086
|
+
const glowMat = createSmartMaterial({
|
|
2087
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
2088
|
+
vertexShaderBody: `
|
|
2089
|
+
uniform float uMoonSize;
|
|
2090
|
+
varying vec2 vUv;
|
|
2091
|
+
void main() {
|
|
2092
|
+
vUv = uv;
|
|
2093
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2094
|
+
vec4 projected = smartProject(mvPos);
|
|
2095
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2096
|
+
vec2 offset = position.xy * uMoonSize * uScale * 2.4;
|
|
2097
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2098
|
+
vScreenPos = projected.xy / projected.w;
|
|
2099
|
+
gl_Position = projected;
|
|
2100
|
+
}
|
|
2101
|
+
`,
|
|
2102
|
+
fragmentShader: `
|
|
2103
|
+
varying vec2 vUv;
|
|
2104
|
+
void main() {
|
|
2105
|
+
float alphaMask = getMaskAlpha();
|
|
2106
|
+
if (alphaMask < 0.01) discard;
|
|
2107
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2108
|
+
float d = length(p);
|
|
2109
|
+
if (d > 1.0) discard;
|
|
2110
|
+
float halo = exp(-5.0 * d * d) * 0.07;
|
|
2111
|
+
halo += exp(-2.5 * max(0.0, d - 0.42)) * 0.045;
|
|
2112
|
+
if (halo < 0.003) discard;
|
|
2113
|
+
gl_FragColor = vec4(vec3(0.78, 0.88, 1.0) * halo, halo * alphaMask);
|
|
2114
|
+
}
|
|
2115
|
+
`,
|
|
2116
|
+
transparent: true,
|
|
2117
|
+
depthWrite: false,
|
|
2118
|
+
depthTest: true,
|
|
2119
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2120
|
+
});
|
|
2121
|
+
moonGlowMesh = new THREE6__namespace.Mesh(glowGeo, glowMat);
|
|
2122
|
+
moonGlowMesh.position.copy(moonWorldPos);
|
|
2123
|
+
moonGlowMesh.frustumCulled = false;
|
|
2124
|
+
moonGlowMesh.renderOrder = 2;
|
|
2125
|
+
scene.add(moonGlowMesh);
|
|
2126
|
+
const discGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2127
|
+
const discMat = createSmartMaterial({
|
|
2128
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
2129
|
+
vertexShaderBody: `
|
|
2130
|
+
uniform float uMoonSize;
|
|
2131
|
+
varying vec2 vUv;
|
|
2132
|
+
void main() {
|
|
2133
|
+
vUv = uv;
|
|
2134
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2135
|
+
vec4 projected = smartProject(mvPos);
|
|
2136
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2137
|
+
vec2 offset = position.xy * uMoonSize * uScale;
|
|
2138
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2139
|
+
vScreenPos = projected.xy / projected.w;
|
|
2140
|
+
gl_Position = projected;
|
|
2141
|
+
}
|
|
2142
|
+
`,
|
|
2143
|
+
fragmentShader: `
|
|
2144
|
+
varying vec2 vUv;
|
|
2145
|
+
void main() {
|
|
2146
|
+
float alphaMask = getMaskAlpha();
|
|
2147
|
+
if (alphaMask < 0.01) discard;
|
|
2148
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2149
|
+
float d = length(p);
|
|
2150
|
+
if (d > 1.0) discard;
|
|
2151
|
+
|
|
2152
|
+
float edge = smoothstep(1.0, 0.90, d);
|
|
2153
|
+
|
|
2154
|
+
// Phase: sunlight from upper-right (gibbous moon)
|
|
2155
|
+
vec2 sunDir2D = normalize(vec2(0.55, 0.45));
|
|
2156
|
+
float phaseRaw = dot(normalize(p + vec2(0.0001)), sunDir2D);
|
|
2157
|
+
float lit = smoothstep(-0.18, 0.32, phaseRaw);
|
|
2158
|
+
|
|
2159
|
+
// Limb darkening (classical sqrt law)
|
|
2160
|
+
float cosTheta = sqrt(max(0.001, 1.0 - d * d));
|
|
2161
|
+
float limb = cosTheta * 0.42 + 0.58;
|
|
2162
|
+
|
|
2163
|
+
// Procedural surface texture
|
|
2164
|
+
float angle = atan(p.y, p.x);
|
|
2165
|
+
float r = d;
|
|
2166
|
+
float detail = sin(angle * 5.0 + 2.1) * sin(r * 8.3) * 0.038
|
|
2167
|
+
+ sin(angle * 11.0 - 1.3) * sin(r * 13.0) * 0.022
|
|
2168
|
+
+ sin(angle * 2.0 + 0.8) * (1.0 - r) * 0.055
|
|
2169
|
+
+ sin(angle * 17.0 + r * 6.5) * 0.014
|
|
2170
|
+
+ sin(angle * 23.0 - r * 11.0) * 0.009;
|
|
2171
|
+
|
|
2172
|
+
// Mare (dark maria) patches
|
|
2173
|
+
float mare1 = 1.0 - smoothstep(0.0, 0.30, length(p - vec2(-0.20, 0.22)));
|
|
2174
|
+
float mare2 = 1.0 - smoothstep(0.0, 0.20, length(p - vec2( 0.10, 0.30)));
|
|
2175
|
+
float mare3 = 1.0 - smoothstep(0.0, 0.24, length(p - vec2( 0.17,-0.06)));
|
|
2176
|
+
float mare4 = 1.0 - smoothstep(0.0, 0.14, length(p - vec2(-0.30,-0.20)));
|
|
2177
|
+
float totalMare = clamp(mare1*0.50 + mare2*0.38 + mare3*0.32 + mare4*0.28, 0.0, 0.58);
|
|
2178
|
+
|
|
2179
|
+
vec3 highland = vec3(0.88, 0.85, 0.80);
|
|
2180
|
+
vec3 mareColor = vec3(0.40, 0.39, 0.37);
|
|
2181
|
+
vec3 moonBase = clamp(mix(highland, mareColor, totalMare) + detail, 0.0, 1.0);
|
|
2182
|
+
|
|
2183
|
+
vec3 litSurface = moonBase * limb;
|
|
2184
|
+
vec3 earthshine = vec3(0.038, 0.052, 0.078);
|
|
2185
|
+
vec3 finalColor = mix(earthshine, litSurface, lit);
|
|
2186
|
+
|
|
2187
|
+
gl_FragColor = vec4(finalColor * edge, edge * alphaMask);
|
|
2188
|
+
}
|
|
2189
|
+
`,
|
|
2190
|
+
transparent: true,
|
|
2191
|
+
depthWrite: true,
|
|
2192
|
+
depthTest: true,
|
|
2193
|
+
blending: THREE6__namespace.NormalBlending
|
|
2194
|
+
});
|
|
2195
|
+
moonMesh = new THREE6__namespace.Mesh(discGeo, discMat);
|
|
2196
|
+
moonMesh.position.copy(moonWorldPos);
|
|
2197
|
+
moonMesh.frustumCulled = false;
|
|
2198
|
+
moonMesh.renderOrder = 3;
|
|
2199
|
+
scene.add(moonMesh);
|
|
2200
|
+
}
|
|
2201
|
+
function createSun() {
|
|
2202
|
+
const sunDir = new THREE6__namespace.Vector3(-1, -0.08, 0).normalize();
|
|
2203
|
+
const sunWorldPos = sunDir.clone().multiplyScalar(2e3);
|
|
2204
|
+
const haloGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2205
|
+
const haloMat = createSmartMaterial({
|
|
2206
|
+
uniforms: { uSunHaloSize: { value: 0.46 } },
|
|
2207
|
+
vertexShaderBody: `
|
|
2208
|
+
uniform float uSunHaloSize;
|
|
2209
|
+
varying vec2 vUv;
|
|
2210
|
+
void main() {
|
|
2211
|
+
vUv = uv;
|
|
2212
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2213
|
+
vec4 projected = smartProject(mvPos);
|
|
2214
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2215
|
+
vec2 offset = position.xy * uSunHaloSize * uScale;
|
|
2216
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2217
|
+
vScreenPos = projected.xy / projected.w;
|
|
2218
|
+
gl_Position = projected;
|
|
2219
|
+
}
|
|
2220
|
+
`,
|
|
2221
|
+
fragmentShader: `
|
|
2222
|
+
varying vec2 vUv;
|
|
2223
|
+
void main() {
|
|
2224
|
+
float alphaMask = getMaskAlpha();
|
|
2225
|
+
if (alphaMask < 0.01) discard;
|
|
2226
|
+
|
|
2227
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2228
|
+
float d = length(p);
|
|
2229
|
+
if (d > 1.0) discard;
|
|
2230
|
+
|
|
2231
|
+
// Asymmetric falloff: spread wider horizontally than vertically
|
|
2232
|
+
float asymDist = length(vec2(p.x * 0.55, p.y));
|
|
2233
|
+
|
|
2234
|
+
// Radial glow: warm near centre, fading outward
|
|
2235
|
+
float glow = exp(-2.8 * asymDist * asymDist) * 1.0;
|
|
2236
|
+
glow += exp(-1.0 * asymDist) * 0.35;
|
|
2237
|
+
|
|
2238
|
+
// Crepuscular rays: fan out from bottom, visible above sun centre
|
|
2239
|
+
float rayMask = smoothstep(-0.05, 0.35, p.y);
|
|
2240
|
+
float rayFade = max(0.0, 1.0 - d) * (1.0 - d);
|
|
2241
|
+
float rayAngle = atan(p.x, max(0.0001, p.y)); // angle from vertical
|
|
2242
|
+
float rays = pow(abs(sin(rayAngle * 7.0 + 0.30)), 9.0) * 0.10
|
|
2243
|
+
+ pow(abs(sin(rayAngle * 13.0 - 1.10)), 14.0) * 0.07
|
|
2244
|
+
+ pow(abs(sin(rayAngle * 19.0 + 2.30)), 11.0) * 0.05;
|
|
2245
|
+
rays *= rayMask * rayFade;
|
|
2246
|
+
|
|
2247
|
+
// Colour: white-yellow \u2192 orange \u2192 hot-pink \u2192 purple
|
|
2248
|
+
vec3 cYellow = vec3(1.00, 0.88, 0.52);
|
|
2249
|
+
vec3 cOrange = vec3(1.00, 0.42, 0.10);
|
|
2250
|
+
vec3 cPink = vec3(0.90, 0.22, 0.52);
|
|
2251
|
+
vec3 cPurple = vec3(0.38, 0.12, 0.48);
|
|
2252
|
+
vec3 col = mix(cYellow, cOrange, smoothstep(0.00, 0.40, asymDist));
|
|
2253
|
+
col = mix(col, cPink, smoothstep(0.35, 0.72, asymDist));
|
|
2254
|
+
col = mix(col, cPurple, smoothstep(0.65, 1.00, asymDist));
|
|
2255
|
+
|
|
2256
|
+
float total = (glow + rays) * alphaMask;
|
|
2257
|
+
if (total < 0.005) discard;
|
|
2258
|
+
gl_FragColor = vec4(col * total, total);
|
|
2259
|
+
}
|
|
2260
|
+
`,
|
|
2261
|
+
transparent: true,
|
|
2262
|
+
depthWrite: false,
|
|
2263
|
+
depthTest: true,
|
|
2264
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2265
|
+
});
|
|
2266
|
+
sunHaloMesh = new THREE6__namespace.Mesh(haloGeo, haloMat);
|
|
2267
|
+
sunHaloMesh.position.copy(sunWorldPos);
|
|
2268
|
+
sunHaloMesh.frustumCulled = false;
|
|
2269
|
+
sunHaloMesh.renderOrder = 1;
|
|
2270
|
+
scene.add(sunHaloMesh);
|
|
2271
|
+
const discGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2272
|
+
const discMat = createSmartMaterial({
|
|
2273
|
+
uniforms: { uSunSize: { value: 0.09 } },
|
|
2274
|
+
vertexShaderBody: `
|
|
2275
|
+
uniform float uSunSize;
|
|
2276
|
+
varying vec2 vUv;
|
|
2277
|
+
void main() {
|
|
2278
|
+
vUv = uv;
|
|
2279
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2280
|
+
vec4 projected = smartProject(mvPos);
|
|
2281
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2282
|
+
vec2 offset = position.xy * uSunSize * uScale;
|
|
2283
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2284
|
+
vScreenPos = projected.xy / projected.w;
|
|
2285
|
+
gl_Position = projected;
|
|
1051
2286
|
}
|
|
1052
2287
|
`,
|
|
1053
2288
|
fragmentShader: `
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
void main() {
|
|
1060
|
-
float alphaMask = getMaskAlpha();
|
|
1061
|
-
if (alphaMask < 0.01) discard;
|
|
1062
|
-
|
|
1063
|
-
// Procedural Horizon (Mountains)
|
|
1064
|
-
float angle = atan(vPos.z, vPos.x);
|
|
1065
|
-
|
|
1066
|
-
// FBM-like terrain with increased amplitude
|
|
1067
|
-
float h = 0.0;
|
|
1068
|
-
h += sin(angle * 6.0) * 35.0;
|
|
1069
|
-
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1070
|
-
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1071
|
-
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1072
|
-
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
2289
|
+
varying vec2 vUv;
|
|
2290
|
+
void main() {
|
|
2291
|
+
float alphaMask = getMaskAlpha();
|
|
2292
|
+
if (alphaMask < 0.01) discard;
|
|
1073
2293
|
|
|
1074
|
-
|
|
2294
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2295
|
+
float d = length(p);
|
|
2296
|
+
if (d > 1.0) discard;
|
|
1075
2297
|
|
|
1076
|
-
|
|
2298
|
+
float edge = smoothstep(1.0, 0.86, d);
|
|
1077
2299
|
|
|
1078
|
-
//
|
|
1079
|
-
float
|
|
1080
|
-
float
|
|
1081
|
-
|
|
2300
|
+
// Photosphere limb darkening: bright white core \u2192 orange limb
|
|
2301
|
+
float core = smoothstep(0.28, 0.00, d);
|
|
2302
|
+
float mid = smoothstep(0.68, 0.22, d) * (1.0 - core);
|
|
2303
|
+
float limb = (1.0 - smoothstep(0.70, 1.00, d)) * (1.0 - core - mid);
|
|
1082
2304
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
vec3
|
|
2305
|
+
vec3 cCore = vec3(1.00, 0.97, 0.88); // hot white
|
|
2306
|
+
vec3 cMid = vec3(1.00, 0.80, 0.38); // yellow
|
|
2307
|
+
vec3 cLimb = vec3(1.00, 0.52, 0.08); // deep orange
|
|
1086
2308
|
|
|
1087
|
-
|
|
1088
|
-
|
|
2309
|
+
vec3 col = cCore * (core + 0.12) + cMid * mid + cLimb * limb;
|
|
2310
|
+
col = clamp(col, 0.0, 1.5); // allow slight overbright
|
|
1089
2311
|
|
|
1090
|
-
gl_FragColor = vec4(
|
|
2312
|
+
gl_FragColor = vec4(col * edge, edge * alphaMask);
|
|
1091
2313
|
}
|
|
1092
2314
|
`,
|
|
1093
|
-
|
|
1094
|
-
transparent: false,
|
|
2315
|
+
transparent: true,
|
|
1095
2316
|
depthWrite: true,
|
|
1096
|
-
depthTest: true
|
|
2317
|
+
depthTest: true,
|
|
2318
|
+
blending: THREE6__namespace.NormalBlending
|
|
1097
2319
|
});
|
|
1098
|
-
|
|
1099
|
-
|
|
2320
|
+
sunDiscMesh = new THREE6__namespace.Mesh(discGeo, discMat);
|
|
2321
|
+
sunDiscMesh.position.copy(sunWorldPos);
|
|
2322
|
+
sunDiscMesh.frustumCulled = false;
|
|
2323
|
+
sunDiscMesh.renderOrder = 3;
|
|
2324
|
+
scene.add(sunDiscMesh);
|
|
1100
2325
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
2326
|
+
function createMilkyWay() {
|
|
2327
|
+
if (milkyWayMesh) {
|
|
2328
|
+
scene.remove(milkyWayMesh);
|
|
2329
|
+
milkyWayMesh.geometry.dispose();
|
|
2330
|
+
milkyWayMesh.material.dispose();
|
|
2331
|
+
milkyWayMesh = null;
|
|
2332
|
+
}
|
|
2333
|
+
const geo = new THREE6__namespace.PlaneGeometry(1100, 380, 4, 4);
|
|
2334
|
+
const mat = createSmartMaterial({
|
|
2335
|
+
uniforms: {},
|
|
1105
2336
|
vertexShaderBody: `
|
|
1106
|
-
varying
|
|
1107
|
-
void main() {
|
|
1108
|
-
|
|
1109
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
1110
|
-
gl_Position = smartProject(mv);
|
|
2337
|
+
varying vec2 vUv;
|
|
2338
|
+
void main() {
|
|
2339
|
+
vUv = uv;
|
|
2340
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
2341
|
+
gl_Position = smartProject(mv);
|
|
1111
2342
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1112
|
-
}
|
|
2343
|
+
}
|
|
2344
|
+
`,
|
|
1113
2345
|
fragmentShader: `
|
|
1114
|
-
varying
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
2346
|
+
varying vec2 vUv;
|
|
2347
|
+
|
|
2348
|
+
// --- Noise helpers ---
|
|
2349
|
+
float hash(vec2 p) {
|
|
2350
|
+
p = fract(p * vec2(127.1, 311.7));
|
|
2351
|
+
p += dot(p, p + 19.19);
|
|
2352
|
+
return fract(p.x * p.y);
|
|
2353
|
+
}
|
|
2354
|
+
float vnoise(vec2 p) {
|
|
2355
|
+
vec2 i = floor(p); vec2 f = fract(p);
|
|
2356
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
2357
|
+
return mix(
|
|
2358
|
+
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
|
|
2359
|
+
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), f.x), f.y
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
float fbm(vec2 p) {
|
|
2363
|
+
float v = 0.0; float a = 0.5;
|
|
2364
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
2365
|
+
for (int i = 0; i < 7; i++) { v += a * vnoise(p); p = m * p; a *= 0.5; }
|
|
2366
|
+
return v;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
1121
2369
|
void main() {
|
|
1122
2370
|
float alphaMask = getMaskAlpha();
|
|
1123
2371
|
if (alphaMask < 0.01) discard;
|
|
1124
2372
|
|
|
1125
|
-
|
|
1126
|
-
float h = normalize(vWorldNormal).y;
|
|
2373
|
+
vec2 uv = vUv * 2.0 - 1.0; // -1..1 centred
|
|
1127
2374
|
|
|
1128
|
-
//
|
|
1129
|
-
float
|
|
2375
|
+
// Galactic band: tight Gaussian falloff vertically
|
|
2376
|
+
float bandMask = exp(-uv.y * uv.y * 10.0);
|
|
1130
2377
|
|
|
1131
|
-
//
|
|
1132
|
-
|
|
2378
|
+
// Warp UV for organic turbulence (two layers of distortion)
|
|
2379
|
+
vec2 q = vec2(fbm(uv * 1.5),
|
|
2380
|
+
fbm(uv * 1.5 + vec2(5.2, 1.3)));
|
|
2381
|
+
vec2 r = vec2(fbm(uv * 1.0 + 4.0 * q + vec2(1.7, 9.2)),
|
|
2382
|
+
fbm(uv * 1.0 + 4.0 * q + vec2(8.3, 2.8)));
|
|
1133
2383
|
|
|
1134
|
-
|
|
1135
|
-
float
|
|
1136
|
-
|
|
2384
|
+
float nebula = fbm(uv * 2.0 + 2.0 * r);
|
|
2385
|
+
float detail = fbm(uv * 5.0 + r * 3.0 + vec2(3.1, 2.7));
|
|
2386
|
+
float fine = fbm(uv * 10.0 + vec2(1.0, 5.0));
|
|
1137
2387
|
|
|
1138
|
-
//
|
|
1139
|
-
float
|
|
1140
|
-
|
|
2388
|
+
// Base density
|
|
2389
|
+
float density = smoothstep(0.30, 0.80, nebula) * bandMask;
|
|
2390
|
+
density += smoothstep(0.45, 0.85, detail) * bandMask * 0.35;
|
|
1141
2391
|
|
|
1142
|
-
//
|
|
1143
|
-
float
|
|
1144
|
-
|
|
2392
|
+
// Dust lanes \u2014 dark patches carved into the band
|
|
2393
|
+
float dust = fbm(uv * 3.5 + vec2(11.0, 7.0));
|
|
2394
|
+
density *= (1.0 - smoothstep(0.52, 0.62, dust) * 0.7 * bandMask);
|
|
1145
2395
|
|
|
1146
|
-
|
|
2396
|
+
// Galactic core boost toward horizontal centre
|
|
2397
|
+
float galCore = exp(-uv.x * uv.x * 1.2) * bandMask;
|
|
2398
|
+
|
|
2399
|
+
// --- Color palette ---
|
|
2400
|
+
vec3 deepBlue = vec3(0.10, 0.15, 0.45);
|
|
2401
|
+
vec3 midBlue = vec3(0.25, 0.30, 0.65);
|
|
2402
|
+
vec3 purple = vec3(0.40, 0.20, 0.60);
|
|
2403
|
+
vec3 coreWarm = vec3(0.85, 0.80, 0.65); // warm star-cluster glow
|
|
2404
|
+
vec3 pinkNeb = vec3(0.65, 0.28, 0.50); // emission nebula pink
|
|
2405
|
+
|
|
2406
|
+
float t1 = smoothstep(0.3, 0.7, nebula);
|
|
2407
|
+
float t2 = smoothstep(0.5, 0.8, detail);
|
|
2408
|
+
float t3 = smoothstep(0.55, 0.75, fine);
|
|
2409
|
+
|
|
2410
|
+
vec3 color = mix(deepBlue, midBlue, t1);
|
|
2411
|
+
color = mix(color, purple, t2 * 0.5);
|
|
2412
|
+
color = mix(color, pinkNeb, t3 * 0.25 * bandMask);
|
|
2413
|
+
color += coreWarm * galCore * 0.45 * density;
|
|
2414
|
+
|
|
2415
|
+
// Micro-star field \u2014 denser in the band
|
|
2416
|
+
float starThresh = mix(0.975, 0.940, bandMask);
|
|
2417
|
+
float starSeed = hash(floor(vUv * 500.0));
|
|
2418
|
+
float star = step(starThresh, starSeed);
|
|
2419
|
+
float starBright = hash(floor(vUv * 500.0) + 37.0);
|
|
2420
|
+
color += vec3(0.90, 0.95, 1.0) * star * (0.4 + 0.6 * starBright);
|
|
2421
|
+
density = max(density, star * bandMask * 0.5);
|
|
2422
|
+
|
|
2423
|
+
// Soft edge vignette
|
|
2424
|
+
float ex = smoothstep(0.0, 0.12, vUv.x) * smoothstep(1.0, 0.88, vUv.x);
|
|
2425
|
+
float ey = smoothstep(0.0, 0.18, vUv.y) * smoothstep(1.0, 0.82, vUv.y);
|
|
2426
|
+
|
|
2427
|
+
float alpha = density * ex * ey * alphaMask * 0.80;
|
|
2428
|
+
if (alpha < 0.004) discard;
|
|
2429
|
+
gl_FragColor = vec4(color, alpha);
|
|
1147
2430
|
}
|
|
1148
2431
|
`,
|
|
1149
|
-
|
|
2432
|
+
transparent: true,
|
|
1150
2433
|
depthWrite: false,
|
|
1151
|
-
depthTest: true
|
|
2434
|
+
depthTest: true,
|
|
2435
|
+
side: THREE6__namespace.DoubleSide,
|
|
2436
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1152
2437
|
});
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
2438
|
+
milkyWayMesh = new THREE6__namespace.Mesh(geo, mat);
|
|
2439
|
+
const mwDir = new THREE6__namespace.Vector3(-0.62, 0.6, -0.5).normalize();
|
|
2440
|
+
milkyWayMesh.position.copy(mwDir.clone().multiplyScalar(920));
|
|
2441
|
+
milkyWayMesh.lookAt(0, 0, 0);
|
|
2442
|
+
milkyWayMesh.rotateY(Math.PI);
|
|
2443
|
+
milkyWayMesh.frustumCulled = false;
|
|
2444
|
+
milkyWayMesh.renderOrder = 1;
|
|
2445
|
+
scene.add(milkyWayMesh);
|
|
1156
2446
|
}
|
|
1157
|
-
const backdropGroup = new
|
|
2447
|
+
const backdropGroup = new THREE6__namespace.Group();
|
|
1158
2448
|
scene.add(backdropGroup);
|
|
1159
|
-
|
|
2449
|
+
let backdropStarsMaterial = null;
|
|
2450
|
+
function createBackdropStars(count = 5e3) {
|
|
1160
2451
|
backdropGroup.clear();
|
|
1161
2452
|
while (backdropGroup.children.length > 0) {
|
|
1162
2453
|
const c = backdropGroup.children[0];
|
|
@@ -1164,7 +2455,7 @@ function createEngine({
|
|
|
1164
2455
|
if (c.geometry) c.geometry.dispose();
|
|
1165
2456
|
if (c.material) c.material.dispose();
|
|
1166
2457
|
}
|
|
1167
|
-
const geometry = new
|
|
2458
|
+
const geometry = new THREE6__namespace.BufferGeometry();
|
|
1168
2459
|
const positions = [];
|
|
1169
2460
|
const sizes = [];
|
|
1170
2461
|
const colors = [];
|
|
@@ -1199,14 +2490,18 @@ function createEngine({
|
|
|
1199
2490
|
}
|
|
1200
2491
|
colors.push(cr, cg, cb);
|
|
1201
2492
|
}
|
|
1202
|
-
geometry.setAttribute("position", new
|
|
1203
|
-
geometry.setAttribute("size", new
|
|
1204
|
-
geometry.setAttribute("color", new
|
|
2493
|
+
geometry.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(positions, 3));
|
|
2494
|
+
geometry.setAttribute("size", new THREE6__namespace.Float32BufferAttribute(sizes, 1));
|
|
2495
|
+
geometry.setAttribute("color", new THREE6__namespace.Float32BufferAttribute(colors, 3));
|
|
1205
2496
|
const material = createSmartMaterial({
|
|
1206
2497
|
uniforms: {
|
|
1207
2498
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1208
2499
|
uScale: globalUniforms.uScale,
|
|
1209
|
-
uTime: globalUniforms.uTime
|
|
2500
|
+
uTime: globalUniforms.uTime,
|
|
2501
|
+
uBackdropGain: { value: 1 },
|
|
2502
|
+
uBackdropEnergy: { value: 2.2 },
|
|
2503
|
+
uBackdropSizeExp: { value: 0.9 },
|
|
2504
|
+
uRevealZoom: { value: 0 }
|
|
1210
2505
|
},
|
|
1211
2506
|
vertexShaderBody: `
|
|
1212
2507
|
attribute float size;
|
|
@@ -1217,6 +2512,10 @@ function createEngine({
|
|
|
1217
2512
|
uniform float uAtmExtinction;
|
|
1218
2513
|
uniform float uAtmTwinkle;
|
|
1219
2514
|
uniform float uTime;
|
|
2515
|
+
uniform float uBackdropGain;
|
|
2516
|
+
uniform float uBackdropEnergy;
|
|
2517
|
+
uniform float uBackdropSizeExp;
|
|
2518
|
+
uniform float uRevealZoom;
|
|
1220
2519
|
|
|
1221
2520
|
void main() {
|
|
1222
2521
|
vec3 nPos = normalize(position);
|
|
@@ -1232,15 +2531,21 @@ function createEngine({
|
|
|
1232
2531
|
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1233
2532
|
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
1234
2533
|
|
|
1235
|
-
|
|
2534
|
+
// Backdrop appears latest \u2014 fully hidden at wide FOV, emerges when zoomed in.
|
|
2535
|
+
// Thresholds come from ZOOM_REVEAL_CONFIG (baked at startup).
|
|
2536
|
+
float mappedZoom = pow(uRevealZoom, ${ZOOM_REVEAL_CONFIG.zoomCurveExp});
|
|
2537
|
+
float backdropReveal = smoothstep(${ZOOM_REVEAL_CONFIG.backdropRevealStart}, ${ZOOM_REVEAL_CONFIG.backdropRevealEnd}, mappedZoom);
|
|
2538
|
+
|
|
2539
|
+
vColor = color * uBackdropEnergy * extinction * horizonFade * scintillation * uBackdropGain * backdropReveal;
|
|
1236
2540
|
|
|
1237
2541
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1238
2542
|
gl_Position = smartProject(mvPosition);
|
|
1239
2543
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1240
2544
|
|
|
1241
|
-
float zoomScale = pow(uScale, 0.
|
|
2545
|
+
float zoomScale = pow(max(uScale, 0.0001), uBackdropSizeExp);
|
|
1242
2546
|
float perceptualSize = pow(size, 0.55);
|
|
1243
|
-
|
|
2547
|
+
float sizeGain = mix(0.78, 1.0, uBackdropGain);
|
|
2548
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * sizeGain * 0.5 * pixelRatio * (800.0 / length(mvPosition.xyz)) * horizonFade, 0.5, 20.0);
|
|
1244
2549
|
}
|
|
1245
2550
|
`,
|
|
1246
2551
|
fragmentShader: `
|
|
@@ -1264,27 +2569,34 @@ function createEngine({
|
|
|
1264
2569
|
transparent: true,
|
|
1265
2570
|
depthWrite: false,
|
|
1266
2571
|
depthTest: true,
|
|
1267
|
-
blending:
|
|
2572
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1268
2573
|
});
|
|
1269
|
-
|
|
2574
|
+
backdropStarsMaterial = material;
|
|
2575
|
+
const points = new THREE6__namespace.Points(geometry, material);
|
|
1270
2576
|
points.frustumCulled = false;
|
|
1271
2577
|
backdropGroup.add(points);
|
|
1272
2578
|
}
|
|
2579
|
+
createSkyBackground();
|
|
1273
2580
|
createGround();
|
|
1274
2581
|
createAtmosphere();
|
|
2582
|
+
createMoon();
|
|
2583
|
+
createSun();
|
|
2584
|
+
createMilkyWay();
|
|
1275
2585
|
createBackdropStars();
|
|
1276
|
-
const raycaster = new
|
|
2586
|
+
const raycaster = new THREE6__namespace.Raycaster();
|
|
1277
2587
|
raycaster.params.Points.threshold = 5;
|
|
1278
|
-
new
|
|
1279
|
-
const root = new
|
|
2588
|
+
new THREE6__namespace.Vector2();
|
|
2589
|
+
const root = new THREE6__namespace.Group();
|
|
1280
2590
|
scene.add(root);
|
|
1281
2591
|
const nodeById = /* @__PURE__ */ new Map();
|
|
1282
2592
|
const starIndexToId = [];
|
|
2593
|
+
const starIdToIndex = /* @__PURE__ */ new Map();
|
|
1283
2594
|
const dynamicLabels = [];
|
|
2595
|
+
const labelManager = new LabelManager();
|
|
1284
2596
|
const hoverLabelMat = createSmartMaterial({
|
|
1285
2597
|
uniforms: {
|
|
1286
2598
|
uMap: { value: null },
|
|
1287
|
-
uSize: { value: new
|
|
2599
|
+
uSize: { value: new THREE6__namespace.Vector2(1, 1) },
|
|
1288
2600
|
uAlpha: { value: 0 },
|
|
1289
2601
|
uAngle: { value: 0 }
|
|
1290
2602
|
},
|
|
@@ -1322,7 +2634,7 @@ function createEngine({
|
|
|
1322
2634
|
depthTest: false
|
|
1323
2635
|
// Always on top of stars
|
|
1324
2636
|
});
|
|
1325
|
-
const hoverLabelMesh = new
|
|
2637
|
+
const hoverLabelMesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), hoverLabelMat);
|
|
1326
2638
|
hoverLabelMesh.visible = false;
|
|
1327
2639
|
hoverLabelMesh.renderOrder = 999;
|
|
1328
2640
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -1346,7 +2658,9 @@ function createEngine({
|
|
|
1346
2658
|
}
|
|
1347
2659
|
nodeById.clear();
|
|
1348
2660
|
starIndexToId.length = 0;
|
|
2661
|
+
starIdToIndex.clear();
|
|
1349
2662
|
dynamicLabels.length = 0;
|
|
2663
|
+
labelManager.clear();
|
|
1350
2664
|
constellationLines = null;
|
|
1351
2665
|
boundaryLines = null;
|
|
1352
2666
|
starPoints = null;
|
|
@@ -1368,49 +2682,132 @@ function createEngine({
|
|
|
1368
2682
|
ctx.textAlign = "center";
|
|
1369
2683
|
ctx.textBaseline = "middle";
|
|
1370
2684
|
ctx.fillText(text, w / 2, h / 2);
|
|
1371
|
-
const tex = new
|
|
1372
|
-
tex.minFilter =
|
|
2685
|
+
const tex = new THREE6__namespace.CanvasTexture(canvas);
|
|
2686
|
+
tex.minFilter = THREE6__namespace.LinearFilter;
|
|
1373
2687
|
return { tex, aspect: w / h };
|
|
1374
2688
|
}
|
|
1375
2689
|
function getPosition(n) {
|
|
1376
2690
|
if (currentConfig?.arrangement) {
|
|
1377
2691
|
const arr = currentConfig.arrangement[n.id];
|
|
1378
|
-
if (arr) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const y = arr.position[1];
|
|
2692
|
+
if (arr?.position) {
|
|
2693
|
+
const [px, py, pz] = arr.position;
|
|
2694
|
+
if (pz === 0) {
|
|
1382
2695
|
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
2696
|
+
const len3d = Math.sqrt(px * px + py * py);
|
|
2697
|
+
if (len3d < radius * 0.99) {
|
|
2698
|
+
const r_norm = Math.min(1, len3d / radius);
|
|
2699
|
+
const phi = Math.atan2(py, px);
|
|
2700
|
+
const theta = r_norm * (Math.PI / 2);
|
|
2701
|
+
return new THREE6__namespace.Vector3(
|
|
2702
|
+
Math.sin(theta) * Math.cos(phi),
|
|
2703
|
+
Math.cos(theta),
|
|
2704
|
+
Math.sin(theta) * Math.sin(phi)
|
|
2705
|
+
).multiplyScalar(radius);
|
|
2706
|
+
}
|
|
1391
2707
|
}
|
|
1392
|
-
return new
|
|
2708
|
+
return new THREE6__namespace.Vector3(px, py, pz);
|
|
1393
2709
|
}
|
|
1394
2710
|
}
|
|
1395
|
-
return new
|
|
2711
|
+
return new THREE6__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
1396
2712
|
}
|
|
1397
2713
|
function getBoundaryPoint(angle, t, radius) {
|
|
1398
2714
|
const y = 0.05 + t * (1 - 0.05);
|
|
1399
2715
|
const rY = Math.sqrt(1 - y * y);
|
|
1400
2716
|
const x = Math.cos(angle) * rY;
|
|
1401
2717
|
const z = Math.sin(angle) * rY;
|
|
1402
|
-
return new
|
|
2718
|
+
return new THREE6__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
2719
|
+
}
|
|
2720
|
+
function updateChapterLabelAnchors() {
|
|
2721
|
+
if (!starPoints) return;
|
|
2722
|
+
const attr = starPoints.geometry.attributes.position;
|
|
2723
|
+
if (!attr) return;
|
|
2724
|
+
const cameraUpWorld = new THREE6__namespace.Vector3(0, 1, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2725
|
+
const cameraRightWorld = new THREE6__namespace.Vector3(1, 0, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2726
|
+
for (const item of dynamicLabels) {
|
|
2727
|
+
if (item.node.level !== 3) continue;
|
|
2728
|
+
const idx = starIdToIndex.get(item.node.id);
|
|
2729
|
+
if (idx === void 0) continue;
|
|
2730
|
+
const starPos = new THREE6__namespace.Vector3(attr.getX(idx), attr.getY(idx), attr.getZ(idx));
|
|
2731
|
+
const normal = starPos.clone().normalize();
|
|
2732
|
+
const tangent = cameraUpWorld.clone().sub(normal.clone().multiplyScalar(cameraUpWorld.dot(normal)));
|
|
2733
|
+
if (tangent.lengthSq() < 1e-6) {
|
|
2734
|
+
tangent.copy(cameraRightWorld).sub(normal.clone().multiplyScalar(cameraRightWorld.dot(normal)));
|
|
2735
|
+
}
|
|
2736
|
+
if (tangent.lengthSq() < 1e-6) continue;
|
|
2737
|
+
tangent.normalize();
|
|
2738
|
+
const starNorm = item.chapterStarSizeNorm ?? 0.5;
|
|
2739
|
+
const baseSize = item.chapterStarBaseSize ?? 3.5;
|
|
2740
|
+
const altitude = normal.y;
|
|
2741
|
+
const horizonFade = THREE6__namespace.MathUtils.smoothstep(altitude, -0.1, 0.05);
|
|
2742
|
+
const mvPos = starPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
2743
|
+
const dist = Math.max(1, mvPos.length());
|
|
2744
|
+
const perceptualSize = Math.pow(baseSize, 0.7);
|
|
2745
|
+
const sizeBoost = 1 + Math.pow(baseSize, 0.5) * 0.08;
|
|
2746
|
+
const pointSize = THREE6__namespace.MathUtils.clamp(
|
|
2747
|
+
perceptualSize * sizeBoost * 20 * globalUniforms.uScale.value * renderer.getPixelRatio() * (2e3 / dist) * horizonFade,
|
|
2748
|
+
1,
|
|
2749
|
+
600
|
|
2750
|
+
);
|
|
2751
|
+
item.chapterGlowRadiusPx = pointSize * 0.6;
|
|
2752
|
+
const viewportH = Math.max(1, renderer.domElement.clientHeight);
|
|
2753
|
+
const fovRad = state.fov * Math.PI / 180;
|
|
2754
|
+
const worldPerPixel = 2 * dist * Math.tan(fovRad * 0.5) / viewportH;
|
|
2755
|
+
let labelHalfDiagPx = 18;
|
|
2756
|
+
const mat = item.obj.material;
|
|
2757
|
+
if (mat instanceof THREE6__namespace.ShaderMaterial && mat.uniforms?.uSize?.value instanceof THREE6__namespace.Vector2) {
|
|
2758
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2759
|
+
const revealT = THREE6__namespace.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2760
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2761
|
+
const fadeOutScale = 1 + (1 - revealT) * 0.06;
|
|
2762
|
+
const zoomTextBoost = THREE6__namespace.MathUtils.lerp(1.4, 0.55, THREE6__namespace.MathUtils.smoothstep(state.fov, 8, 46));
|
|
2763
|
+
const starTextBoost = THREE6__namespace.MathUtils.lerp(0.9, 1.35, starNorm);
|
|
2764
|
+
const scaleMul = zoomTextBoost * starTextBoost * revealScale * fadeOutScale;
|
|
2765
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2766
|
+
const targetX = item.initialScale.x * scaleMul;
|
|
2767
|
+
const targetY = item.initialScale.y * scaleMul;
|
|
2768
|
+
uSize.x = THREE6__namespace.MathUtils.lerp(uSize.x, targetX, 0.2);
|
|
2769
|
+
uSize.y = THREE6__namespace.MathUtils.lerp(uSize.y, targetY, 0.2);
|
|
2770
|
+
const size = mat.uniforms.uSize.value;
|
|
2771
|
+
const pixelH = size.y * viewportH * 0.8;
|
|
2772
|
+
const pixelW = size.x * viewportH * 0.8;
|
|
2773
|
+
labelHalfDiagPx = Math.max(6, Math.max(pixelH, pixelW * 0.45) * 0.5);
|
|
2774
|
+
}
|
|
2775
|
+
const edgeMarginPx = THREE6__namespace.MathUtils.lerp(1, 3, starNorm);
|
|
2776
|
+
const requiredPx = item.chapterGlowRadiusPx + edgeMarginPx + labelHalfDiagPx;
|
|
2777
|
+
const zoomPush = 1 + (1 - THREE6__namespace.MathUtils.smoothstep(state.fov, 8, 30)) * 0.8;
|
|
2778
|
+
const starPush = THREE6__namespace.MathUtils.lerp(0.95, 1.2, starNorm);
|
|
2779
|
+
const offset = THREE6__namespace.MathUtils.clamp(requiredPx * worldPerPixel * zoomPush * starPush, 3, 76);
|
|
2780
|
+
item.obj.position.copy(starPos);
|
|
2781
|
+
item.obj.position.addScaledVector(tangent, offset);
|
|
2782
|
+
item.obj.position.addScaledVector(normal, 2.5);
|
|
2783
|
+
item.chapterStarWorldPos = starPos.clone();
|
|
2784
|
+
}
|
|
2785
|
+
for (const item of dynamicLabels) {
|
|
2786
|
+
const level = item.node.level;
|
|
2787
|
+
if (level !== 2 && level !== 2.5) continue;
|
|
2788
|
+
const mat = item.obj.material;
|
|
2789
|
+
if (!(mat instanceof THREE6__namespace.ShaderMaterial) || !(mat.uniforms?.uSize?.value instanceof THREE6__namespace.Vector2)) continue;
|
|
2790
|
+
const entryFov = 22;
|
|
2791
|
+
const zoomBoost = THREE6__namespace.MathUtils.lerp(1.3, 0.5, THREE6__namespace.MathUtils.smoothstep(state.fov, 8, entryFov));
|
|
2792
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2793
|
+
const revealT = THREE6__namespace.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2794
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2795
|
+
const scaleMul = zoomBoost * revealScale;
|
|
2796
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2797
|
+
uSize.x = THREE6__namespace.MathUtils.lerp(uSize.x, item.initialScale.x * scaleMul, 0.2);
|
|
2798
|
+
uSize.y = THREE6__namespace.MathUtils.lerp(uSize.y, item.initialScale.y * scaleMul, 0.2);
|
|
2799
|
+
}
|
|
1403
2800
|
}
|
|
1404
2801
|
function buildFromModel(model, cfg) {
|
|
1405
2802
|
clearRoot();
|
|
1406
2803
|
bookIdToIndex.clear();
|
|
1407
2804
|
testamentToIndex.clear();
|
|
1408
2805
|
divisionToIndex.clear();
|
|
1409
|
-
scene.background = cfg.background && cfg.background !== "transparent" ? new
|
|
2806
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE6__namespace.Color(cfg.background) : null;
|
|
1410
2807
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
1411
2808
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
1412
2809
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
1413
|
-
|
|
2810
|
+
{
|
|
1414
2811
|
const divMap = /* @__PURE__ */ new Map();
|
|
1415
2812
|
for (const n of laidOut.nodes) {
|
|
1416
2813
|
if (n.level === 2 && n.parent) {
|
|
@@ -1420,7 +2817,7 @@ function createEngine({
|
|
|
1420
2817
|
}
|
|
1421
2818
|
}
|
|
1422
2819
|
for (const [divId, books] of divMap.entries()) {
|
|
1423
|
-
const centroid = new
|
|
2820
|
+
const centroid = new THREE6__namespace.Vector3();
|
|
1424
2821
|
let count = 0;
|
|
1425
2822
|
for (const b of books) {
|
|
1426
2823
|
const p = getPosition(b);
|
|
@@ -1441,20 +2838,26 @@ function createEngine({
|
|
|
1441
2838
|
const starChapterIndices = [];
|
|
1442
2839
|
const starTestamentIndices = [];
|
|
1443
2840
|
const starDivisionIndices = [];
|
|
2841
|
+
const starRevealThresholds = [];
|
|
2842
|
+
const chapterLineCutById = /* @__PURE__ */ new Map();
|
|
2843
|
+
const chapterStarSizeById = /* @__PURE__ */ new Map();
|
|
2844
|
+
const chapterWeightNormById = /* @__PURE__ */ new Map();
|
|
2845
|
+
let minChapterStarSize = Infinity;
|
|
2846
|
+
let maxChapterStarSize = -Infinity;
|
|
1444
2847
|
const SPECTRAL_COLORS = [
|
|
1445
|
-
new
|
|
2848
|
+
new THREE6__namespace.Color(14544639),
|
|
1446
2849
|
// O - Blueish White
|
|
1447
|
-
new
|
|
2850
|
+
new THREE6__namespace.Color(15660287),
|
|
1448
2851
|
// B - White
|
|
1449
|
-
new
|
|
2852
|
+
new THREE6__namespace.Color(16317695),
|
|
1450
2853
|
// A - White
|
|
1451
|
-
new
|
|
2854
|
+
new THREE6__namespace.Color(16777208),
|
|
1452
2855
|
// F - White
|
|
1453
|
-
new
|
|
2856
|
+
new THREE6__namespace.Color(16775406),
|
|
1454
2857
|
// G - Yellowish White
|
|
1455
|
-
new
|
|
2858
|
+
new THREE6__namespace.Color(16773085),
|
|
1456
2859
|
// K - Pale Orange
|
|
1457
|
-
new
|
|
2860
|
+
new THREE6__namespace.Color(16771788)
|
|
1458
2861
|
// M - Light Orange
|
|
1459
2862
|
];
|
|
1460
2863
|
let minWeight = Infinity;
|
|
@@ -1472,17 +2875,59 @@ function createEngine({
|
|
|
1472
2875
|
} else if (minWeight === maxWeight) {
|
|
1473
2876
|
maxWeight = minWeight + 1;
|
|
1474
2877
|
}
|
|
2878
|
+
{
|
|
2879
|
+
const pctCap = THREE6__namespace.MathUtils.clamp(cfg.starSizeWeightPercentile ?? 0.95, 0.5, 1);
|
|
2880
|
+
const allWeights = [];
|
|
2881
|
+
for (const n of laidOut.nodes) {
|
|
2882
|
+
if (n.level === 3 && typeof n.weight === "number") allWeights.push(n.weight);
|
|
2883
|
+
}
|
|
2884
|
+
allWeights.sort((a, b) => a - b);
|
|
2885
|
+
const capIdx = Math.min(Math.floor(pctCap * allWeights.length), allWeights.length - 1);
|
|
2886
|
+
const cappedMax = allWeights[capIdx];
|
|
2887
|
+
if (cappedMax !== void 0 && cappedMax > minWeight) maxWeight = cappedMax;
|
|
2888
|
+
}
|
|
1475
2889
|
for (const n of laidOut.nodes) {
|
|
1476
2890
|
if (n.level === 3) {
|
|
1477
|
-
const p = getPosition(n);
|
|
1478
|
-
starPositions.push(p.x, p.y, p.z);
|
|
1479
|
-
starIndexToId.push(n.id);
|
|
1480
2891
|
let baseSize = 3.5;
|
|
2892
|
+
let weightNorm = 0;
|
|
1481
2893
|
if (typeof n.weight === "number") {
|
|
1482
|
-
|
|
1483
|
-
|
|
2894
|
+
weightNorm = THREE6__namespace.MathUtils.clamp((n.weight - minWeight) / (maxWeight - minWeight), 0, 1);
|
|
2895
|
+
const sizeExp = cfg.starSizeExponent ?? 4;
|
|
2896
|
+
const sizeScale = cfg.starSizeScale ?? 6;
|
|
2897
|
+
baseSize = Math.pow(weightNorm, sizeExp) * 22 * sizeScale;
|
|
1484
2898
|
}
|
|
2899
|
+
chapterStarSizeById.set(n.id, baseSize);
|
|
2900
|
+
chapterWeightNormById.set(n.id, weightNorm);
|
|
2901
|
+
minChapterStarSize = Math.min(minChapterStarSize, baseSize);
|
|
2902
|
+
maxChapterStarSize = Math.max(maxChapterStarSize, baseSize);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (!Number.isFinite(minChapterStarSize)) {
|
|
2906
|
+
minChapterStarSize = 1;
|
|
2907
|
+
maxChapterStarSize = 2;
|
|
2908
|
+
} else if (minChapterStarSize === maxChapterStarSize) {
|
|
2909
|
+
maxChapterStarSize = minChapterStarSize + 1;
|
|
2910
|
+
}
|
|
2911
|
+
for (const n of laidOut.nodes) {
|
|
2912
|
+
if (n.level === 3) {
|
|
2913
|
+
const p = getPosition(n);
|
|
2914
|
+
starPositions.push(p.x, p.y, p.z);
|
|
2915
|
+
starIdToIndex.set(n.id, starIndexToId.length);
|
|
2916
|
+
starIndexToId.push(n.id);
|
|
2917
|
+
const baseSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
1485
2918
|
starSizes.push(baseSize);
|
|
2919
|
+
{
|
|
2920
|
+
const wn = chapterWeightNormById.get(n.id) ?? 0;
|
|
2921
|
+
starRevealThresholds.push(THREE6__namespace.MathUtils.lerp(
|
|
2922
|
+
-ZOOM_REVEAL_CONFIG.chapterFeather,
|
|
2923
|
+
ZOOM_REVEAL_CONFIG.chapterRevealMax,
|
|
2924
|
+
1 - wn
|
|
2925
|
+
));
|
|
2926
|
+
}
|
|
2927
|
+
chapterLineCutById.set(
|
|
2928
|
+
n.id,
|
|
2929
|
+
THREE6__namespace.MathUtils.clamp(2.5 + baseSize * 0.45, 3, 40)
|
|
2930
|
+
);
|
|
1486
2931
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
1487
2932
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
1488
2933
|
starColors.push(c.r, c.g, c.b);
|
|
@@ -1533,8 +2978,11 @@ function createEngine({
|
|
|
1533
2978
|
let baseScale = 0.05;
|
|
1534
2979
|
if (n.level === 1) baseScale = 0.08;
|
|
1535
2980
|
else if (n.level === 2) baseScale = 0.04;
|
|
1536
|
-
else if (n.level === 3)
|
|
1537
|
-
|
|
2981
|
+
else if (n.level === 3) {
|
|
2982
|
+
const wn2 = chapterWeightNormById.get(n.id) ?? 0;
|
|
2983
|
+
baseScale = THREE6__namespace.MathUtils.lerp(0.019, 0.039, wn2);
|
|
2984
|
+
}
|
|
2985
|
+
const size = new THREE6__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1538
2986
|
const mat = createSmartMaterial({
|
|
1539
2987
|
uniforms: {
|
|
1540
2988
|
uMap: { value: texRes.tex },
|
|
@@ -1573,39 +3021,60 @@ function createEngine({
|
|
|
1573
3021
|
`,
|
|
1574
3022
|
transparent: true,
|
|
1575
3023
|
depthWrite: false,
|
|
1576
|
-
depthTest: true
|
|
3024
|
+
depthTest: n.level === 3 ? false : true
|
|
1577
3025
|
});
|
|
1578
|
-
const mesh = new
|
|
3026
|
+
const mesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), mat);
|
|
1579
3027
|
let p = getPosition(n);
|
|
1580
3028
|
if (n.level === 1) {
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
3029
|
+
if (cfg.arrangement?.[n.id]) {
|
|
3030
|
+
const arr = cfg.arrangement[n.id];
|
|
3031
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
3032
|
+
} else {
|
|
3033
|
+
if (divisionPositions.has(n.id)) {
|
|
3034
|
+
p.copy(divisionPositions.get(n.id));
|
|
3035
|
+
}
|
|
3036
|
+
const r = layoutCfg.radius * 0.95;
|
|
3037
|
+
const angle = Math.atan2(p.z, p.x);
|
|
3038
|
+
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1583
3039
|
}
|
|
1584
|
-
const r = layoutCfg.radius * 0.95;
|
|
1585
|
-
const angle = Math.atan2(p.z, p.x);
|
|
1586
|
-
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1587
3040
|
} else if (n.level === 3) {
|
|
1588
|
-
|
|
1589
|
-
|
|
3041
|
+
const starSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
3042
|
+
const starNorm = THREE6__namespace.MathUtils.clamp(
|
|
3043
|
+
(starSize - minChapterStarSize) / (maxChapterStarSize - minChapterStarSize),
|
|
3044
|
+
0,
|
|
3045
|
+
1
|
|
3046
|
+
);
|
|
3047
|
+
const radialOffset = THREE6__namespace.MathUtils.lerp(16, 46, starNorm);
|
|
3048
|
+
p.addScaledVector(p.clone().normalize(), radialOffset);
|
|
1590
3049
|
}
|
|
1591
3050
|
mesh.position.set(p.x, p.y, p.z);
|
|
1592
3051
|
mesh.scale.set(size.x, size.y, 1);
|
|
1593
3052
|
mesh.frustumCulled = false;
|
|
1594
3053
|
mesh.userData = { id: n.id };
|
|
1595
3054
|
root.add(mesh);
|
|
1596
|
-
|
|
3055
|
+
const wn = n.level === 3 ? chapterWeightNormById.get(n.id) ?? 0 : 0;
|
|
3056
|
+
const chapterMaxFovBias = n.level === 3 ? THREE6__namespace.MathUtils.lerp(-4, 8, wn) : 0;
|
|
3057
|
+
dynamicLabels.push({
|
|
3058
|
+
obj: mesh,
|
|
3059
|
+
node: n,
|
|
3060
|
+
initialScale: size.clone(),
|
|
3061
|
+
maxFovBias: chapterMaxFovBias,
|
|
3062
|
+
chapterStarSizeNorm: n.level === 3 ? wn : void 0,
|
|
3063
|
+
chapterStarBaseSize: n.level === 3 ? chapterStarSizeById.get(n.id) ?? 3.5 : void 0
|
|
3064
|
+
});
|
|
1597
3065
|
}
|
|
1598
3066
|
}
|
|
1599
3067
|
}
|
|
1600
|
-
const starGeo = new
|
|
1601
|
-
starGeo.setAttribute("position", new
|
|
1602
|
-
starGeo.setAttribute("size", new
|
|
1603
|
-
starGeo.setAttribute("color", new
|
|
1604
|
-
starGeo.setAttribute("phase", new
|
|
1605
|
-
starGeo.setAttribute("bookIndex", new
|
|
1606
|
-
starGeo.setAttribute("chapterIndex", new
|
|
1607
|
-
starGeo.setAttribute("testamentIndex", new
|
|
1608
|
-
starGeo.setAttribute("divisionIndex", new
|
|
3068
|
+
const starGeo = new THREE6__namespace.BufferGeometry();
|
|
3069
|
+
starGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(starPositions, 3));
|
|
3070
|
+
starGeo.setAttribute("size", new THREE6__namespace.Float32BufferAttribute(starSizes, 1));
|
|
3071
|
+
starGeo.setAttribute("color", new THREE6__namespace.Float32BufferAttribute(starColors, 3));
|
|
3072
|
+
starGeo.setAttribute("phase", new THREE6__namespace.Float32BufferAttribute(starPhases, 1));
|
|
3073
|
+
starGeo.setAttribute("bookIndex", new THREE6__namespace.Float32BufferAttribute(starBookIndices, 1));
|
|
3074
|
+
starGeo.setAttribute("chapterIndex", new THREE6__namespace.Float32BufferAttribute(starChapterIndices, 1));
|
|
3075
|
+
starGeo.setAttribute("testamentIndex", new THREE6__namespace.Float32BufferAttribute(starTestamentIndices, 1));
|
|
3076
|
+
starGeo.setAttribute("divisionIndex", new THREE6__namespace.Float32BufferAttribute(starDivisionIndices, 1));
|
|
3077
|
+
starGeo.setAttribute("revealThreshold", new THREE6__namespace.Float32BufferAttribute(starRevealThresholds, 1));
|
|
1609
3078
|
const starMat = createSmartMaterial({
|
|
1610
3079
|
uniforms: {
|
|
1611
3080
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -1614,7 +3083,7 @@ function createEngine({
|
|
|
1614
3083
|
uActiveBookIndex: { value: -1 },
|
|
1615
3084
|
uOrderRevealStrength: { value: 0 },
|
|
1616
3085
|
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1617
|
-
uPulseParams: { value: new
|
|
3086
|
+
uPulseParams: { value: new THREE6__namespace.Vector3(
|
|
1618
3087
|
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1619
3088
|
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1620
3089
|
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
@@ -1623,18 +3092,22 @@ function createEngine({
|
|
|
1623
3092
|
uFilterDivisionIndex: { value: -1 },
|
|
1624
3093
|
uFilterBookIndex: { value: -1 },
|
|
1625
3094
|
uFilterStrength: { value: 0 },
|
|
1626
|
-
uFilterDimFactor: { value: 0.08 }
|
|
3095
|
+
uFilterDimFactor: { value: 0.08 },
|
|
3096
|
+
uRevealZoom: { value: 0 }
|
|
1627
3097
|
},
|
|
1628
3098
|
vertexShaderBody: `
|
|
1629
|
-
attribute float size;
|
|
1630
|
-
attribute vec3 color;
|
|
3099
|
+
attribute float size;
|
|
3100
|
+
attribute vec3 color;
|
|
1631
3101
|
attribute float phase;
|
|
1632
3102
|
attribute float bookIndex;
|
|
1633
3103
|
attribute float chapterIndex;
|
|
1634
3104
|
attribute float testamentIndex;
|
|
1635
3105
|
attribute float divisionIndex;
|
|
3106
|
+
attribute float revealThreshold;
|
|
1636
3107
|
|
|
1637
3108
|
varying vec3 vColor;
|
|
3109
|
+
varying float vSize;
|
|
3110
|
+
varying float vReveal;
|
|
1638
3111
|
uniform float pixelRatio;
|
|
1639
3112
|
|
|
1640
3113
|
uniform float uTime;
|
|
@@ -1651,6 +3124,7 @@ function createEngine({
|
|
|
1651
3124
|
uniform float uFilterBookIndex;
|
|
1652
3125
|
uniform float uFilterStrength;
|
|
1653
3126
|
uniform float uFilterDimFactor;
|
|
3127
|
+
uniform float uRevealZoom;
|
|
1654
3128
|
|
|
1655
3129
|
void main() {
|
|
1656
3130
|
vec3 nPos = normalize(position);
|
|
@@ -1707,41 +3181,166 @@ function createEngine({
|
|
|
1707
3181
|
gl_Position = smartProject(mvPosition);
|
|
1708
3182
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1709
3183
|
|
|
1710
|
-
float sizeBoost = 1.0 + activePulse * 0.
|
|
1711
|
-
|
|
1712
|
-
|
|
3184
|
+
float sizeBoost = 1.0 + activePulse * 0.15;
|
|
3185
|
+
// pow(size, 0.7) is gentler compression than 0.55 \u2014 preserves more of
|
|
3186
|
+
// the aggressive JS curve so large stars stay visually dominant.
|
|
3187
|
+
float perceptualSize = pow(size, 0.7);
|
|
3188
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 20.0) * uScale * pixelRatio * (2000.0 / length(mvPosition.xyz)) * horizonFade, 1.0, 600.0);
|
|
3189
|
+
vSize = gl_PointSize;
|
|
3190
|
+
|
|
3191
|
+
// Zoom-based reveal: faint stars hide at wide FOV, fade in as user zooms.
|
|
3192
|
+
// Exponent and feather baked from ZOOM_REVEAL_CONFIG at startup.
|
|
3193
|
+
float mappedZoom = pow(uRevealZoom, ${ZOOM_REVEAL_CONFIG.zoomCurveExp});
|
|
3194
|
+
vReveal = smoothstep(revealThreshold, revealThreshold + ${ZOOM_REVEAL_CONFIG.chapterFeather}, mappedZoom);
|
|
1713
3195
|
}
|
|
1714
3196
|
`,
|
|
1715
3197
|
fragmentShader: `
|
|
1716
|
-
varying vec3 vColor;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
3198
|
+
varying vec3 vColor;
|
|
3199
|
+
varying float vSize;
|
|
3200
|
+
varying float vReveal;
|
|
3201
|
+
void main() {
|
|
3202
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
3203
|
+
float d = length(coord) * 2.0;
|
|
3204
|
+
if (d > 1.0) discard;
|
|
3205
|
+
|
|
3206
|
+
float alphaMask = getMaskAlpha();
|
|
3207
|
+
if (alphaMask < 0.01) discard;
|
|
3208
|
+
|
|
3209
|
+
// --- Multi-layer Gaussian star model ---
|
|
3210
|
+
// Tight white-hot core
|
|
3211
|
+
float core = exp(-d * d * 9.0);
|
|
3212
|
+
// Broader coloured inner halo
|
|
3213
|
+
float innerGlow = exp(-d * d * 3.0) * 0.45;
|
|
3214
|
+
// Wide faint bloom that fades smoothly to the disc edge
|
|
3215
|
+
float outerBloom = max(0.0, 1.0 - d * d) * 0.10;
|
|
3216
|
+
|
|
3217
|
+
float k = core + innerGlow + outerBloom;
|
|
1729
3218
|
|
|
1730
|
-
// White-hot
|
|
1731
|
-
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.
|
|
1732
|
-
|
|
3219
|
+
// White-hot centre \u2192 spectral colour at the halo
|
|
3220
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.88);
|
|
3221
|
+
|
|
3222
|
+
// --- Size-dependent diffraction spikes ---
|
|
3223
|
+
// Only appear on larger (brighter) stars, matching real optics.
|
|
3224
|
+
float spikeFactor = smoothstep(10.0, 24.0, vSize);
|
|
3225
|
+
float spikeH = exp(-coord.y * coord.y * 180.0) * exp(-abs(coord.x) * 6.0);
|
|
3226
|
+
float spikeV = exp(-coord.x * coord.x * 180.0) * exp(-abs(coord.y) * 6.0);
|
|
3227
|
+
float spikes = (spikeH + spikeV) * 0.18 * spikeFactor;
|
|
3228
|
+
|
|
3229
|
+
// vReveal drives the additive contribution (AdditiveBlending uses SRC_ALPHA).
|
|
3230
|
+
gl_FragColor = vec4(finalColor * (k + spikes) * alphaMask, vReveal);
|
|
1733
3231
|
}
|
|
1734
3232
|
`,
|
|
1735
3233
|
transparent: true,
|
|
1736
3234
|
depthWrite: false,
|
|
1737
3235
|
depthTest: true,
|
|
1738
|
-
blending:
|
|
3236
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1739
3237
|
});
|
|
1740
|
-
starPoints = new
|
|
3238
|
+
starPoints = new THREE6__namespace.Points(starGeo, starMat);
|
|
1741
3239
|
starPoints.frustumCulled = false;
|
|
1742
3240
|
root.add(starPoints);
|
|
1743
3241
|
const linePoints = [];
|
|
3242
|
+
const lineWeights = [];
|
|
3243
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1744
3244
|
const bookMap = /* @__PURE__ */ new Map();
|
|
3245
|
+
const parseBookKeyFromChapterId = (id) => {
|
|
3246
|
+
if (!id) return null;
|
|
3247
|
+
const parts = id.split(":");
|
|
3248
|
+
if (parts.length < 3 || parts[0] !== "C") return null;
|
|
3249
|
+
return parts[1] || null;
|
|
3250
|
+
};
|
|
3251
|
+
const weightScaleFromLabel = (weight) => {
|
|
3252
|
+
if (weight === "thin") return 0.65;
|
|
3253
|
+
if (weight === "bold") return 1.6;
|
|
3254
|
+
return 1;
|
|
3255
|
+
};
|
|
3256
|
+
const edgeKey = (aNodeId, bNodeId) => aNodeId < bNodeId ? `${aNodeId}|${bNodeId}` : `${bNodeId}|${aNodeId}`;
|
|
3257
|
+
const addTruncatedSegment = (aNodeId, bNodeId, weightScale) => {
|
|
3258
|
+
if (aNodeId === bNodeId) return;
|
|
3259
|
+
const k = edgeKey(aNodeId, bNodeId);
|
|
3260
|
+
if (seenEdges.has(k)) return;
|
|
3261
|
+
seenEdges.add(k);
|
|
3262
|
+
const aNode = nodeById.get(aNodeId);
|
|
3263
|
+
const bNode = nodeById.get(bNodeId);
|
|
3264
|
+
if (!aNode || !bNode) return;
|
|
3265
|
+
const p1 = getPosition(aNode);
|
|
3266
|
+
const p2 = getPosition(bNode);
|
|
3267
|
+
const dir = new THREE6__namespace.Vector3().subVectors(p2, p1);
|
|
3268
|
+
const len = dir.length();
|
|
3269
|
+
if (len < 1e-3) return;
|
|
3270
|
+
dir.divideScalar(len);
|
|
3271
|
+
let cutA = chapterLineCutById.get(aNodeId) ?? 4;
|
|
3272
|
+
let cutB = chapterLineCutById.get(bNodeId) ?? 4;
|
|
3273
|
+
const maxTotalCut = len * 0.8;
|
|
3274
|
+
const totalCut = cutA + cutB;
|
|
3275
|
+
if (totalCut > maxTotalCut && totalCut > 0) {
|
|
3276
|
+
const scale = maxTotalCut / totalCut;
|
|
3277
|
+
cutA *= scale;
|
|
3278
|
+
cutB *= scale;
|
|
3279
|
+
}
|
|
3280
|
+
const a = p1.clone().addScaledVector(dir, cutA);
|
|
3281
|
+
const b = p2.clone().addScaledVector(dir, -cutB);
|
|
3282
|
+
linePoints.push(a.x, a.y, a.z);
|
|
3283
|
+
linePoints.push(b.x, b.y, b.z);
|
|
3284
|
+
lineWeights.push(weightScale);
|
|
3285
|
+
};
|
|
3286
|
+
const customBooks = /* @__PURE__ */ new Set();
|
|
3287
|
+
const rawConstellations = cfg.constellations && Array.isArray(cfg.constellations.constellations) ? cfg.constellations.constellations : [];
|
|
3288
|
+
for (const c of rawConstellations) {
|
|
3289
|
+
const linePaths = Array.isArray(c?.linePaths) ? c.linePaths : [];
|
|
3290
|
+
const lineSegments = Array.isArray(c?.lineSegments) ? c.lineSegments : [];
|
|
3291
|
+
if (linePaths.length === 0 && lineSegments.length === 0) continue;
|
|
3292
|
+
const anchorBookKey = parseBookKeyFromChapterId(c?.anchors?.[0]);
|
|
3293
|
+
if (anchorBookKey) customBooks.add(anchorBookKey);
|
|
3294
|
+
for (const segDef of lineSegments) {
|
|
3295
|
+
let from;
|
|
3296
|
+
let to;
|
|
3297
|
+
let weightLabel;
|
|
3298
|
+
if (Array.isArray(segDef)) {
|
|
3299
|
+
const raw = segDef;
|
|
3300
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3301
|
+
weightLabel = raw[0];
|
|
3302
|
+
from = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3303
|
+
to = typeof raw[2] === "string" ? raw[2] : void 0;
|
|
3304
|
+
} else {
|
|
3305
|
+
from = typeof raw[0] === "string" ? raw[0] : void 0;
|
|
3306
|
+
to = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3307
|
+
}
|
|
3308
|
+
} else if (segDef) {
|
|
3309
|
+
from = typeof segDef.from === "string" ? segDef.from : void 0;
|
|
3310
|
+
to = typeof segDef.to === "string" ? segDef.to : void 0;
|
|
3311
|
+
weightLabel = typeof segDef.weight === "string" ? segDef.weight : void 0;
|
|
3312
|
+
}
|
|
3313
|
+
if (!from || !to) continue;
|
|
3314
|
+
const k1 = parseBookKeyFromChapterId(from);
|
|
3315
|
+
const k2 = parseBookKeyFromChapterId(to);
|
|
3316
|
+
if (k1) customBooks.add(k1);
|
|
3317
|
+
if (k2) customBooks.add(k2);
|
|
3318
|
+
addTruncatedSegment(from, to, weightScaleFromLabel(weightLabel));
|
|
3319
|
+
}
|
|
3320
|
+
for (const pathDef of linePaths) {
|
|
3321
|
+
let nodes = [];
|
|
3322
|
+
let weightLabel = void 0;
|
|
3323
|
+
if (Array.isArray(pathDef)) {
|
|
3324
|
+
const raw = pathDef;
|
|
3325
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3326
|
+
weightLabel = raw[0];
|
|
3327
|
+
nodes = raw.slice(1).filter((v) => typeof v === "string");
|
|
3328
|
+
} else {
|
|
3329
|
+
nodes = raw.filter((v) => typeof v === "string");
|
|
3330
|
+
}
|
|
3331
|
+
} else if (pathDef && Array.isArray(pathDef.nodes)) {
|
|
3332
|
+
nodes = pathDef.nodes.filter((v) => typeof v === "string");
|
|
3333
|
+
weightLabel = typeof pathDef.weight === "string" ? pathDef.weight : void 0;
|
|
3334
|
+
}
|
|
3335
|
+
if (nodes.length < 2) continue;
|
|
3336
|
+
const inferredBookKey = parseBookKeyFromChapterId(nodes[0]);
|
|
3337
|
+
if (inferredBookKey) customBooks.add(inferredBookKey);
|
|
3338
|
+
const w = weightScaleFromLabel(weightLabel);
|
|
3339
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
3340
|
+
addTruncatedSegment(nodes[i], nodes[i + 1], w);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
1745
3344
|
for (const n of laidOut.nodes) {
|
|
1746
3345
|
if (n.level === 3 && n.parent) {
|
|
1747
3346
|
const list = bookMap.get(n.parent) ?? [];
|
|
@@ -1752,24 +3351,27 @@ function createEngine({
|
|
|
1752
3351
|
for (const chapters of bookMap.values()) {
|
|
1753
3352
|
chapters.sort((a, b) => (a.meta?.chapter || 0) - (b.meta?.chapter || 0));
|
|
1754
3353
|
if (chapters.length < 2) continue;
|
|
3354
|
+
const bookKey = chapters[0]?.meta?.bookKey ?? null;
|
|
3355
|
+
if (bookKey && customBooks.has(bookKey)) continue;
|
|
1755
3356
|
for (let i = 0; i < chapters.length - 1; i++) {
|
|
1756
3357
|
const c1 = chapters[i];
|
|
1757
3358
|
const c2 = chapters[i + 1];
|
|
1758
3359
|
if (!c1 || !c2) continue;
|
|
1759
|
-
|
|
1760
|
-
const p2 = getPosition(c2);
|
|
1761
|
-
linePoints.push(p1.x, p1.y, p1.z);
|
|
1762
|
-
linePoints.push(p2.x, p2.y, p2.z);
|
|
3360
|
+
addTruncatedSegment(c1.id, c2.id, 1);
|
|
1763
3361
|
}
|
|
1764
3362
|
}
|
|
1765
3363
|
if (linePoints.length > 0) {
|
|
1766
3364
|
const quadPositions = [];
|
|
1767
3365
|
const quadUvs = [];
|
|
3366
|
+
const quadLineWeight = [];
|
|
3367
|
+
const quadSegmentIndex = [];
|
|
1768
3368
|
const quadIndices = [];
|
|
1769
3369
|
const lineWidth = 8;
|
|
3370
|
+
const segmentCount = linePoints.length / 6;
|
|
1770
3371
|
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1771
3372
|
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1772
3373
|
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
3374
|
+
const segIndex = i / 6;
|
|
1773
3375
|
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1774
3376
|
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1775
3377
|
if (len < 1e-3) continue;
|
|
@@ -1794,23 +3396,36 @@ function createEngine({
|
|
|
1794
3396
|
quadUvs.push(1, -1);
|
|
1795
3397
|
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1796
3398
|
quadUvs.push(1, 1);
|
|
3399
|
+
const w = lineWeights[segIndex] ?? 1;
|
|
3400
|
+
quadLineWeight.push(w, w, w, w);
|
|
3401
|
+
quadSegmentIndex.push(segIndex, segIndex, segIndex, segIndex);
|
|
1797
3402
|
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1798
3403
|
}
|
|
1799
|
-
const lineGeo = new
|
|
1800
|
-
lineGeo.setAttribute("position", new
|
|
1801
|
-
lineGeo.setAttribute("lineUv", new
|
|
3404
|
+
const lineGeo = new THREE6__namespace.BufferGeometry();
|
|
3405
|
+
lineGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(quadPositions, 3));
|
|
3406
|
+
lineGeo.setAttribute("lineUv", new THREE6__namespace.Float32BufferAttribute(quadUvs, 2));
|
|
3407
|
+
lineGeo.setAttribute("lineWeight", new THREE6__namespace.Float32BufferAttribute(quadLineWeight, 1));
|
|
3408
|
+
lineGeo.setAttribute("segmentIndex", new THREE6__namespace.Float32BufferAttribute(quadSegmentIndex, 1));
|
|
1802
3409
|
lineGeo.setIndex(quadIndices);
|
|
1803
3410
|
const lineMat = createSmartMaterial({
|
|
1804
3411
|
uniforms: {
|
|
1805
|
-
color: { value: new
|
|
3412
|
+
color: { value: new THREE6__namespace.Color(11193599) },
|
|
1806
3413
|
uLineWidth: { value: 1.5 },
|
|
1807
|
-
uGlowIntensity: { value: 0.3 }
|
|
3414
|
+
uGlowIntensity: { value: 0.3 },
|
|
3415
|
+
uReveal: { value: 0 },
|
|
3416
|
+
uSegmentCount: { value: Math.max(1, segmentCount) }
|
|
1808
3417
|
},
|
|
1809
3418
|
vertexShaderBody: `
|
|
1810
3419
|
attribute vec2 lineUv;
|
|
3420
|
+
attribute float lineWeight;
|
|
3421
|
+
attribute float segmentIndex;
|
|
1811
3422
|
varying vec2 vLineUv;
|
|
3423
|
+
varying float vLineWeight;
|
|
3424
|
+
varying float vSegmentIndex;
|
|
1812
3425
|
void main() {
|
|
1813
3426
|
vLineUv = lineUv;
|
|
3427
|
+
vLineWeight = lineWeight;
|
|
3428
|
+
vSegmentIndex = segmentIndex;
|
|
1814
3429
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1815
3430
|
gl_Position = smartProject(mvPosition);
|
|
1816
3431
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
@@ -1820,32 +3435,53 @@ function createEngine({
|
|
|
1820
3435
|
uniform vec3 color;
|
|
1821
3436
|
uniform float uLineWidth;
|
|
1822
3437
|
uniform float uGlowIntensity;
|
|
3438
|
+
uniform float uReveal;
|
|
3439
|
+
uniform float uSegmentCount;
|
|
1823
3440
|
varying vec2 vLineUv;
|
|
3441
|
+
varying float vLineWeight;
|
|
3442
|
+
varying float vSegmentIndex;
|
|
1824
3443
|
void main() {
|
|
1825
3444
|
float alphaMask = getMaskAlpha();
|
|
1826
3445
|
if (alphaMask < 0.01) discard;
|
|
1827
3446
|
|
|
3447
|
+
// Progressive line draw tuned closer to Stellarium feel:
|
|
3448
|
+
// - eased global reveal
|
|
3449
|
+
// - sequential segment staggering with slight overlap
|
|
3450
|
+
// - smooth growth of each segment endpoint
|
|
3451
|
+
float reveal = smoothstep(0.0, 1.0, uReveal);
|
|
3452
|
+
float segCount = max(uSegmentCount, 1.0);
|
|
3453
|
+
float segStart = vSegmentIndex / segCount;
|
|
3454
|
+
float segSpan = (1.25 / segCount) + 0.04;
|
|
3455
|
+
float localReveal = clamp((reveal - segStart) / segSpan, 0.0, 1.0);
|
|
3456
|
+
localReveal = smoothstep(0.0, 1.0, localReveal);
|
|
3457
|
+
|
|
3458
|
+
// Keep fragment only when x is before the animated endpoint.
|
|
3459
|
+
float endpointMask = 1.0 - smoothstep(localReveal - 0.03, localReveal + 0.02, vLineUv.x);
|
|
3460
|
+
// Fade in segment brightness as it begins drawing.
|
|
3461
|
+
float drawMask = endpointMask * smoothstep(0.0, 0.08, localReveal);
|
|
3462
|
+
if (drawMask < 0.001) discard;
|
|
3463
|
+
|
|
1828
3464
|
float dist = abs(vLineUv.y);
|
|
1829
3465
|
|
|
1830
3466
|
// Anti-aliased core line
|
|
1831
|
-
float hw = uLineWidth * 0.05;
|
|
3467
|
+
float hw = (uLineWidth * vLineWeight) * 0.05;
|
|
1832
3468
|
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1833
3469
|
|
|
1834
3470
|
// Soft glow extending outward
|
|
1835
|
-
float glow = (1.0 - dist) * uGlowIntensity;
|
|
3471
|
+
float glow = (1.0 - dist) * uGlowIntensity * vLineWeight;
|
|
1836
3472
|
|
|
1837
3473
|
float alpha = max(glow, base);
|
|
1838
3474
|
if (alpha < 0.005) discard;
|
|
1839
3475
|
|
|
1840
|
-
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
3476
|
+
gl_FragColor = vec4(color, alpha * alphaMask * drawMask);
|
|
1841
3477
|
}
|
|
1842
3478
|
`,
|
|
1843
3479
|
transparent: true,
|
|
1844
3480
|
depthWrite: false,
|
|
1845
|
-
blending:
|
|
1846
|
-
side:
|
|
3481
|
+
blending: THREE6__namespace.AdditiveBlending,
|
|
3482
|
+
side: THREE6__namespace.DoubleSide
|
|
1847
3483
|
});
|
|
1848
|
-
constellationLines = new
|
|
3484
|
+
constellationLines = new THREE6__namespace.Mesh(lineGeo, lineMat);
|
|
1849
3485
|
constellationLines.frustumCulled = false;
|
|
1850
3486
|
root.add(constellationLines);
|
|
1851
3487
|
}
|
|
@@ -1858,7 +3494,7 @@ function createEngine({
|
|
|
1858
3494
|
if (groupList) {
|
|
1859
3495
|
groupList.forEach((g, idx) => {
|
|
1860
3496
|
const groupId = `G:${bookId}:${idx}`;
|
|
1861
|
-
let p = new
|
|
3497
|
+
let p = new THREE6__namespace.Vector3();
|
|
1862
3498
|
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1863
3499
|
const arr = cfg.arrangement[groupId];
|
|
1864
3500
|
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
@@ -1877,7 +3513,7 @@ function createEngine({
|
|
|
1877
3513
|
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1878
3514
|
if (texRes) {
|
|
1879
3515
|
const baseScale = 0.036;
|
|
1880
|
-
const size = new
|
|
3516
|
+
const size = new THREE6__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1881
3517
|
const mat = createSmartMaterial({
|
|
1882
3518
|
uniforms: {
|
|
1883
3519
|
uMap: { value: texRes.tex },
|
|
@@ -1918,7 +3554,7 @@ function createEngine({
|
|
|
1918
3554
|
depthWrite: false,
|
|
1919
3555
|
depthTest: true
|
|
1920
3556
|
});
|
|
1921
|
-
const mesh = new
|
|
3557
|
+
const mesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), mat);
|
|
1922
3558
|
mesh.position.copy(p);
|
|
1923
3559
|
mesh.scale.set(size.x, size.y, 1);
|
|
1924
3560
|
mesh.frustumCulled = false;
|
|
@@ -1940,14 +3576,14 @@ function createEngine({
|
|
|
1940
3576
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1941
3577
|
if (boundaries.length > 0) {
|
|
1942
3578
|
const boundaryMat = createSmartMaterial({
|
|
1943
|
-
uniforms: { color: { value: new
|
|
3579
|
+
uniforms: { color: { value: new THREE6__namespace.Color(5601177) } },
|
|
1944
3580
|
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; }`,
|
|
1945
3581
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1946
3582
|
transparent: true,
|
|
1947
3583
|
depthWrite: false,
|
|
1948
|
-
blending:
|
|
3584
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1949
3585
|
});
|
|
1950
|
-
const boundaryGeo = new
|
|
3586
|
+
const boundaryGeo = new THREE6__namespace.BufferGeometry();
|
|
1951
3587
|
const bPoints = [];
|
|
1952
3588
|
boundaries.forEach((angle) => {
|
|
1953
3589
|
const steps = 32;
|
|
@@ -1960,8 +3596,8 @@ function createEngine({
|
|
|
1960
3596
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1961
3597
|
}
|
|
1962
3598
|
});
|
|
1963
|
-
boundaryGeo.setAttribute("position", new
|
|
1964
|
-
boundaryLines = new
|
|
3599
|
+
boundaryGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(bPoints, 3));
|
|
3600
|
+
boundaryLines = new THREE6__namespace.LineSegments(boundaryGeo, boundaryMat);
|
|
1965
3601
|
boundaryLines.frustumCulled = false;
|
|
1966
3602
|
root.add(boundaryLines);
|
|
1967
3603
|
}
|
|
@@ -1980,7 +3616,7 @@ function createEngine({
|
|
|
1980
3616
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1981
3617
|
const phi = Math.atan2(y, x);
|
|
1982
3618
|
const theta = r_norm * (Math.PI / 2);
|
|
1983
|
-
return new
|
|
3619
|
+
return new THREE6__namespace.Vector3(
|
|
1984
3620
|
Math.sin(theta) * Math.cos(phi),
|
|
1985
3621
|
Math.cos(theta),
|
|
1986
3622
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1993,22 +3629,23 @@ function createEngine({
|
|
|
1993
3629
|
}
|
|
1994
3630
|
}
|
|
1995
3631
|
if (polyPoints.length > 0) {
|
|
1996
|
-
const polyGeo = new
|
|
1997
|
-
polyGeo.setAttribute("position", new
|
|
3632
|
+
const polyGeo = new THREE6__namespace.BufferGeometry();
|
|
3633
|
+
polyGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(polyPoints, 3));
|
|
1998
3634
|
const polyMat = createSmartMaterial({
|
|
1999
|
-
uniforms: { color: { value: new
|
|
3635
|
+
uniforms: { color: { value: new THREE6__namespace.Color(3718648) } },
|
|
2000
3636
|
// Cyan-ish
|
|
2001
3637
|
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; }`,
|
|
2002
3638
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
2003
3639
|
transparent: true,
|
|
2004
3640
|
depthWrite: false,
|
|
2005
|
-
blending:
|
|
3641
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2006
3642
|
});
|
|
2007
|
-
const polyLines = new
|
|
3643
|
+
const polyLines = new THREE6__namespace.LineSegments(polyGeo, polyMat);
|
|
2008
3644
|
polyLines.frustumCulled = false;
|
|
2009
3645
|
root.add(polyLines);
|
|
2010
3646
|
}
|
|
2011
3647
|
}
|
|
3648
|
+
labelManager.setLabels(dynamicLabels);
|
|
2012
3649
|
resize();
|
|
2013
3650
|
}
|
|
2014
3651
|
let lastData = void 0;
|
|
@@ -2029,6 +3666,10 @@ function createEngine({
|
|
|
2029
3666
|
}
|
|
2030
3667
|
function setConfig(cfg) {
|
|
2031
3668
|
currentConfig = cfg;
|
|
3669
|
+
applyGroundTheme(cfg);
|
|
3670
|
+
const externalFocusId = cfg.focus?.nodeId;
|
|
3671
|
+
if (typeof externalFocusId === "string") focusedNodeId = externalFocusId;
|
|
3672
|
+
if (externalFocusId === null) focusedNodeId = null;
|
|
2032
3673
|
if (cfg.projection) setProjection(cfg.projection);
|
|
2033
3674
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
2034
3675
|
state.lon = cfg.camera.lon;
|
|
@@ -2069,27 +3710,48 @@ function createEngine({
|
|
|
2069
3710
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
2070
3711
|
}
|
|
2071
3712
|
if (cfg.constellations) {
|
|
3713
|
+
const getLayoutPosition = (id) => {
|
|
3714
|
+
const n = nodeById.get(id);
|
|
3715
|
+
if (!n) return null;
|
|
3716
|
+
const x = n.meta?.x ?? 0;
|
|
3717
|
+
const y = n.meta?.y ?? 0;
|
|
3718
|
+
const z = n.meta?.z ?? 0;
|
|
3719
|
+
if (z === 0) {
|
|
3720
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
3721
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
3722
|
+
const phi = Math.atan2(y, x);
|
|
3723
|
+
const theta = r_norm * (Math.PI / 2);
|
|
3724
|
+
return new THREE6__namespace.Vector3(
|
|
3725
|
+
Math.sin(theta) * Math.cos(phi),
|
|
3726
|
+
Math.cos(theta),
|
|
3727
|
+
Math.sin(theta) * Math.sin(phi)
|
|
3728
|
+
).multiplyScalar(radius);
|
|
3729
|
+
}
|
|
3730
|
+
return new THREE6__namespace.Vector3(x, y, z);
|
|
3731
|
+
};
|
|
2072
3732
|
constellationLayer.load(cfg.constellations, (id) => {
|
|
2073
3733
|
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
2074
3734
|
const arr = cfg.arrangement[id];
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
3735
|
+
const coords = arr.center ?? arr.position;
|
|
3736
|
+
if (coords) {
|
|
3737
|
+
if (coords[2] === 0) {
|
|
3738
|
+
const x = coords[0];
|
|
3739
|
+
const y = coords[1];
|
|
3740
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
3741
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
3742
|
+
const phi = Math.atan2(y, x);
|
|
3743
|
+
const theta = r_norm * (Math.PI / 2);
|
|
3744
|
+
return new THREE6__namespace.Vector3(
|
|
3745
|
+
Math.sin(theta) * Math.cos(phi),
|
|
3746
|
+
Math.cos(theta),
|
|
3747
|
+
Math.sin(theta) * Math.sin(phi)
|
|
3748
|
+
).multiplyScalar(radius);
|
|
3749
|
+
}
|
|
3750
|
+
return new THREE6__namespace.Vector3(coords[0], coords[1], coords[2]);
|
|
2087
3751
|
}
|
|
2088
|
-
return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
2089
3752
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
});
|
|
3753
|
+
return getLayoutPosition(id);
|
|
3754
|
+
}, getLayoutPosition);
|
|
2093
3755
|
}
|
|
2094
3756
|
}
|
|
2095
3757
|
function setHandlers(next) {
|
|
@@ -2114,7 +3776,7 @@ function createEngine({
|
|
|
2114
3776
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
2115
3777
|
}
|
|
2116
3778
|
for (const item of constellationLayer.getItems()) {
|
|
2117
|
-
arr[item.config.id] = {
|
|
3779
|
+
arr[item.config.id] = { center: [item.center.x, item.center.y, item.center.z] };
|
|
2118
3780
|
}
|
|
2119
3781
|
Object.assign(arr, state.tempArrangement);
|
|
2120
3782
|
return arr;
|
|
@@ -2138,60 +3800,70 @@ function createEngine({
|
|
|
2138
3800
|
const uAspect = camera.aspect;
|
|
2139
3801
|
const w = rect.width;
|
|
2140
3802
|
const h = rect.height;
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3803
|
+
const isEditMode = currentConfig?.editable ?? false;
|
|
3804
|
+
function pickLabel(threshold) {
|
|
3805
|
+
let closest = null;
|
|
3806
|
+
let minDist = threshold;
|
|
3807
|
+
for (const item of dynamicLabels) {
|
|
3808
|
+
if (!item.obj.visible) continue;
|
|
3809
|
+
if (isNodeFiltered(item.node)) continue;
|
|
3810
|
+
const labelMat = item.obj.material;
|
|
3811
|
+
if ((labelMat?.uniforms?.uAlpha?.value ?? 0) < 0.1) continue;
|
|
3812
|
+
const pWorld = item.obj.position;
|
|
3813
|
+
const pProj = smartProjectJS(pWorld);
|
|
3814
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
3815
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
3816
|
+
const yNDC = pProj.y * uScale;
|
|
3817
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
3818
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
3819
|
+
const d = Math.sqrt((mX - sX) ** 2 + (mY - sY) ** 2);
|
|
3820
|
+
if (d < minDist) {
|
|
3821
|
+
minDist = d;
|
|
3822
|
+
closest = item;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
return closest;
|
|
3826
|
+
}
|
|
3827
|
+
if (isEditMode) {
|
|
3828
|
+
if (starPoints) {
|
|
3829
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3830
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3831
|
+
raycaster.ray.direction.copy(worldDir);
|
|
3832
|
+
raycaster.params.Points.threshold = 65 * (state.fov / 60);
|
|
3833
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
3834
|
+
const pointHit = hits[0];
|
|
3835
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
3836
|
+
const id = starIndexToId[pointHit.index];
|
|
3837
|
+
if (id) {
|
|
3838
|
+
const node = nodeById.get(id);
|
|
3839
|
+
if (node && !isNodeFiltered(node)) {
|
|
3840
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3841
|
+
const starPos = new THREE6__namespace.Vector3(attr.getX(pointHit.index), attr.getY(pointHit.index), attr.getZ(pointHit.index));
|
|
3842
|
+
return { type: "star", node, index: pointHit.index, point: starPos, object: void 0 };
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
const editLabel = pickLabel(isTouchDevice ? 48 : 32);
|
|
3848
|
+
if (editLabel) {
|
|
3849
|
+
return { type: "label", node: editLabel.node, object: editLabel.obj, point: editLabel.obj.position.clone(), index: void 0 };
|
|
2160
3850
|
}
|
|
3851
|
+
return void 0;
|
|
2161
3852
|
}
|
|
3853
|
+
const closestLabel = pickLabel(isTouchDevice ? 48 : 40);
|
|
2162
3854
|
if (closestLabel) {
|
|
2163
3855
|
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
2164
3856
|
}
|
|
2165
3857
|
let closestConst = null;
|
|
2166
3858
|
let minConstDist = Infinity;
|
|
3859
|
+
const artWorldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3860
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3861
|
+
raycaster.ray.direction.copy(artWorldDir);
|
|
2167
3862
|
for (const item of constellationLayer.getItems()) {
|
|
2168
3863
|
if (!item.mesh.visible) continue;
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const uniforms = item.material.uniforms;
|
|
2173
|
-
if (!uniforms || !uniforms.uSize) continue;
|
|
2174
|
-
const uSize = uniforms.uSize.value;
|
|
2175
|
-
const uImgAspect = uniforms.uImgAspect.value;
|
|
2176
|
-
const uImgRotation = uniforms.uImgRotation.value;
|
|
2177
|
-
const dist = pWorld.length();
|
|
2178
|
-
if (dist < 1e-3) continue;
|
|
2179
|
-
const scale = uSize / dist * uScale;
|
|
2180
|
-
const halfH_px = scale / 2 * (h / 2);
|
|
2181
|
-
const halfW_px = halfH_px * uImgAspect;
|
|
2182
|
-
const xNDC = pProj.x * uScale / uAspect;
|
|
2183
|
-
const yNDC = pProj.y * uScale;
|
|
2184
|
-
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
2185
|
-
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
2186
|
-
const dx = mX - sX;
|
|
2187
|
-
const dy = mY - sY;
|
|
2188
|
-
const dy_cart = -dy;
|
|
2189
|
-
const cr = Math.cos(-uImgRotation);
|
|
2190
|
-
const sr = Math.sin(-uImgRotation);
|
|
2191
|
-
const localX = dx * cr - dy_cart * sr;
|
|
2192
|
-
const localY = dx * sr + dy_cart * cr;
|
|
2193
|
-
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
2194
|
-
const d = Math.sqrt(dx * dx + dy * dy);
|
|
3864
|
+
const hits = raycaster.intersectObject(item.mesh, false);
|
|
3865
|
+
if (hits.length > 0) {
|
|
3866
|
+
const d = hits[0].distance;
|
|
2195
3867
|
if (!closestConst || d < minConstDist) {
|
|
2196
3868
|
minConstDist = d;
|
|
2197
3869
|
closestConst = item;
|
|
@@ -2204,7 +3876,7 @@ function createEngine({
|
|
|
2204
3876
|
label: closestConst.config.title,
|
|
2205
3877
|
level: -1
|
|
2206
3878
|
};
|
|
2207
|
-
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.
|
|
3879
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.center.clone(), index: void 0 };
|
|
2208
3880
|
}
|
|
2209
3881
|
if (starPoints) {
|
|
2210
3882
|
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
@@ -2227,20 +3899,65 @@ function createEngine({
|
|
|
2227
3899
|
isMouseInWindow = false;
|
|
2228
3900
|
edgeHoverStart = 0;
|
|
2229
3901
|
}
|
|
3902
|
+
function screenSpacePickStar(mx, my, maxPx = 50) {
|
|
3903
|
+
if (!starPoints) return null;
|
|
3904
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3905
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
3906
|
+
const w = rect.width;
|
|
3907
|
+
const h = rect.height;
|
|
3908
|
+
const uScale = globalUniforms.uScale.value;
|
|
3909
|
+
const uAspect = globalUniforms.uAspect.value;
|
|
3910
|
+
let bestIdx = -1;
|
|
3911
|
+
let bestDist2 = maxPx * maxPx;
|
|
3912
|
+
const worldPos = new THREE6__namespace.Vector3();
|
|
3913
|
+
for (let i = 0; i < attr.count; i++) {
|
|
3914
|
+
worldPos.set(attr.getX(i), attr.getY(i), attr.getZ(i));
|
|
3915
|
+
const proj = smartProjectJS(worldPos);
|
|
3916
|
+
if (currentProjection.isClipped(proj.z)) continue;
|
|
3917
|
+
const sx = (proj.x * uScale / uAspect * 0.5 + 0.5) * w;
|
|
3918
|
+
const sy = (-(proj.y * uScale) * 0.5 + 0.5) * h;
|
|
3919
|
+
const dx = mx - sx;
|
|
3920
|
+
const dy = my - sy;
|
|
3921
|
+
const d2 = dx * dx + dy * dy;
|
|
3922
|
+
if (d2 < bestDist2) {
|
|
3923
|
+
bestDist2 = d2;
|
|
3924
|
+
bestIdx = i;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (bestIdx < 0) return null;
|
|
3928
|
+
return {
|
|
3929
|
+
index: bestIdx,
|
|
3930
|
+
worldPos: new THREE6__namespace.Vector3(attr.getX(bestIdx), attr.getY(bestIdx), attr.getZ(bestIdx))
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
2230
3933
|
function onMouseDown(e) {
|
|
2231
3934
|
state.lastMouseX = e.clientX;
|
|
2232
3935
|
state.lastMouseY = e.clientY;
|
|
2233
3936
|
if (currentConfig?.editable) {
|
|
3937
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
3938
|
+
const mX = e.clientX - rect.left;
|
|
3939
|
+
const mY = e.clientY - rect.top;
|
|
3940
|
+
const starHit = screenSpacePickStar(mX, mY);
|
|
3941
|
+
if (starHit) {
|
|
3942
|
+
state.dragMode = "node";
|
|
3943
|
+
state.draggedStarIndex = starHit.index;
|
|
3944
|
+
state.draggedNodeId = starIndexToId[starHit.index] ?? null;
|
|
3945
|
+
state.draggedDist = starHit.worldPos.length();
|
|
3946
|
+
state.draggedGroup = null;
|
|
3947
|
+
state.tempArrangement = {};
|
|
3948
|
+
state.velocityX = 0;
|
|
3949
|
+
state.velocityY = 0;
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
2234
3952
|
const hit = pick(e);
|
|
2235
|
-
if (hit) {
|
|
3953
|
+
if (hit && (hit.type === "label" || hit.type === "constellation")) {
|
|
2236
3954
|
state.dragMode = "node";
|
|
2237
3955
|
state.draggedNodeId = hit.node.id;
|
|
2238
3956
|
state.draggedDist = hit.point.length();
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
} else if (hit.type === "label") {
|
|
3957
|
+
state.draggedStarIndex = -1;
|
|
3958
|
+
state.velocityX = 0;
|
|
3959
|
+
state.velocityY = 0;
|
|
3960
|
+
if (hit.type === "label") {
|
|
2244
3961
|
const bookId = hit.node.id;
|
|
2245
3962
|
const children = [];
|
|
2246
3963
|
if (starPoints && starPoints.geometry.attributes.position) {
|
|
@@ -2250,17 +3967,26 @@ function createEngine({
|
|
|
2250
3967
|
if (starId) {
|
|
2251
3968
|
const starNode = nodeById.get(starId);
|
|
2252
3969
|
if (starNode && starNode.parent === bookId) {
|
|
2253
|
-
children.push({ index: i, initialPos: new
|
|
3970
|
+
children.push({ index: i, initialPos: new THREE6__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
2254
3971
|
}
|
|
2255
3972
|
}
|
|
2256
3973
|
}
|
|
2257
3974
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
3975
|
+
const constellations = [];
|
|
3976
|
+
for (const cItem of constellationLayer.getItems()) {
|
|
3977
|
+
const anchored = cItem.config.anchors.some((anchorId) => {
|
|
3978
|
+
const n = nodeById.get(anchorId);
|
|
3979
|
+
return n?.parent === bookId;
|
|
3980
|
+
});
|
|
3981
|
+
if (anchored) {
|
|
3982
|
+
constellations.push({ id: cItem.config.id, initialCenter: cItem.center.clone() });
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children, constellations };
|
|
3986
|
+
} else {
|
|
3987
|
+
state.draggedGroup = { labelInitialPos: hit.point.clone(), children: [], constellations: [] };
|
|
2263
3988
|
}
|
|
3989
|
+
return;
|
|
2264
3990
|
}
|
|
2265
3991
|
return;
|
|
2266
3992
|
}
|
|
@@ -2287,6 +4013,8 @@ function createEngine({
|
|
|
2287
4013
|
const attr = starPoints.geometry.attributes.position;
|
|
2288
4014
|
attr.setXYZ(idx, newPos.x, newPos.y, newPos.z);
|
|
2289
4015
|
attr.needsUpdate = true;
|
|
4016
|
+
const starId = starIndexToId[idx];
|
|
4017
|
+
if (starId) state.tempArrangement[starId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2290
4018
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
2291
4019
|
const group = state.draggedGroup;
|
|
2292
4020
|
const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
|
|
@@ -2296,16 +4024,19 @@ function createEngine({
|
|
|
2296
4024
|
} else if (state.draggedNodeId) {
|
|
2297
4025
|
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
2298
4026
|
if (cItem) {
|
|
2299
|
-
|
|
2300
|
-
|
|
4027
|
+
const vS = group.labelInitialPos.clone().normalize();
|
|
4028
|
+
const vE = newPos.clone().normalize();
|
|
4029
|
+
cItem.mesh.quaternion.setFromUnitVectors(vS, vE);
|
|
4030
|
+
cItem.center.copy(newPos);
|
|
4031
|
+
state.tempArrangement[state.draggedNodeId] = { center: [newPos.x, newPos.y, newPos.z] };
|
|
2301
4032
|
}
|
|
2302
4033
|
}
|
|
2303
4034
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
2304
4035
|
const vEnd = newPos.clone().normalize();
|
|
2305
|
-
const q = new
|
|
4036
|
+
const q = new THREE6__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
2306
4037
|
if (starPoints && group.children.length > 0) {
|
|
2307
4038
|
const attr = starPoints.geometry.attributes.position;
|
|
2308
|
-
const tempVec = new
|
|
4039
|
+
const tempVec = new THREE6__namespace.Vector3();
|
|
2309
4040
|
for (const child of group.children) {
|
|
2310
4041
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
2311
4042
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -2316,6 +4047,20 @@ function createEngine({
|
|
|
2316
4047
|
}
|
|
2317
4048
|
attr.needsUpdate = true;
|
|
2318
4049
|
}
|
|
4050
|
+
if (group.constellations.length > 0) {
|
|
4051
|
+
for (const { id, initialCenter } of group.constellations) {
|
|
4052
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === id);
|
|
4053
|
+
if (cItem) {
|
|
4054
|
+
const newCenter = initialCenter.clone().applyQuaternion(q);
|
|
4055
|
+
cItem.center.copy(newCenter);
|
|
4056
|
+
cItem.mesh.quaternion.setFromUnitVectors(
|
|
4057
|
+
initialCenter.clone().normalize(),
|
|
4058
|
+
newCenter.clone().normalize()
|
|
4059
|
+
);
|
|
4060
|
+
state.tempArrangement[id] = { center: [newCenter.x, newCenter.y, newCenter.z] };
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
2319
4064
|
}
|
|
2320
4065
|
} else if (state.dragMode === "camera") {
|
|
2321
4066
|
const deltaX = e.clientX - state.lastMouseX;
|
|
@@ -2323,13 +4068,15 @@ function createEngine({
|
|
|
2323
4068
|
state.lastMouseX = e.clientX;
|
|
2324
4069
|
state.lastMouseY = e.clientY;
|
|
2325
4070
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2326
|
-
const
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
4071
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4072
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4073
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4074
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4075
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4076
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2330
4077
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2331
|
-
state.velocityX =
|
|
2332
|
-
state.velocityY =
|
|
4078
|
+
state.velocityX = moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4079
|
+
state.velocityY = moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2333
4080
|
state.lon = state.targetLon;
|
|
2334
4081
|
state.lat = state.targetLat;
|
|
2335
4082
|
} else {
|
|
@@ -2341,7 +4088,7 @@ function createEngine({
|
|
|
2341
4088
|
if (res) {
|
|
2342
4089
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
2343
4090
|
const baseScale = 0.03;
|
|
2344
|
-
const size = new
|
|
4091
|
+
const size = new THREE6__namespace.Vector2(baseScale * res.aspect, baseScale);
|
|
2345
4092
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
2346
4093
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
2347
4094
|
}
|
|
@@ -2359,7 +4106,7 @@ function createEngine({
|
|
|
2359
4106
|
handlers.onHover?.(hit?.node);
|
|
2360
4107
|
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
2361
4108
|
}
|
|
2362
|
-
document.body.style.cursor = hit ? currentConfig?.editable ? "
|
|
4109
|
+
document.body.style.cursor = hit ? currentConfig?.editable && hit.type === "star" ? "grab" : "pointer" : "default";
|
|
2363
4110
|
}
|
|
2364
4111
|
}
|
|
2365
4112
|
function onMouseUp(e) {
|
|
@@ -2383,10 +4130,12 @@ function createEngine({
|
|
|
2383
4130
|
if (hit) {
|
|
2384
4131
|
handlers.onSelect?.(hit.node);
|
|
2385
4132
|
constellationLayer.setFocused(hit.node.id);
|
|
4133
|
+
focusedNodeId = hit.node.id;
|
|
2386
4134
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2387
4135
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2388
4136
|
} else {
|
|
2389
4137
|
setFocusedBook(null);
|
|
4138
|
+
focusedNodeId = null;
|
|
2390
4139
|
}
|
|
2391
4140
|
}
|
|
2392
4141
|
} else {
|
|
@@ -2394,10 +4143,12 @@ function createEngine({
|
|
|
2394
4143
|
if (hit) {
|
|
2395
4144
|
handlers.onSelect?.(hit.node);
|
|
2396
4145
|
constellationLayer.setFocused(hit.node.id);
|
|
4146
|
+
focusedNodeId = hit.node.id;
|
|
2397
4147
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2398
4148
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2399
4149
|
} else {
|
|
2400
4150
|
setFocusedBook(null);
|
|
4151
|
+
focusedNodeId = null;
|
|
2401
4152
|
}
|
|
2402
4153
|
}
|
|
2403
4154
|
}
|
|
@@ -2407,44 +4158,55 @@ function createEngine({
|
|
|
2407
4158
|
const aspect = container.clientWidth / container.clientHeight;
|
|
2408
4159
|
renderer.domElement.getBoundingClientRect();
|
|
2409
4160
|
const vBefore = getMouseViewVector(state.fov, aspect);
|
|
2410
|
-
const
|
|
4161
|
+
const zoomResistance = THREE6__namespace.MathUtils.lerp(
|
|
4162
|
+
1,
|
|
4163
|
+
ENGINE_CONFIG.zoomResistanceWideFov,
|
|
4164
|
+
THREE6__namespace.MathUtils.smoothstep(state.fov, 24, 100)
|
|
4165
|
+
);
|
|
4166
|
+
const zoomSpeed = 1e-3 * state.fov * zoomResistance;
|
|
2411
4167
|
state.fov += e.deltaY * zoomSpeed;
|
|
2412
4168
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2413
4169
|
handlers.onFovChange?.(state.fov);
|
|
2414
4170
|
updateUniforms();
|
|
2415
4171
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
2416
|
-
const quaternion = new
|
|
2417
|
-
const dampStartFov =
|
|
2418
|
-
const dampEndFov =
|
|
4172
|
+
const quaternion = new THREE6__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
4173
|
+
const dampStartFov = 32;
|
|
4174
|
+
const dampEndFov = 110;
|
|
2419
4175
|
let spinAmount = 1;
|
|
2420
4176
|
if (state.fov > dampStartFov) {
|
|
2421
4177
|
const t = Math.max(0, Math.min(1, (state.fov - dampStartFov) / (dampEndFov - dampStartFov)));
|
|
2422
|
-
spinAmount = 1 - Math.pow(t, 1.
|
|
4178
|
+
spinAmount = 1 - Math.pow(t, 1.35) * 0.92;
|
|
2423
4179
|
}
|
|
4180
|
+
const blendForSpin = getBlendForZenithControl();
|
|
4181
|
+
const blendSpinDamp = THREE6__namespace.MathUtils.smoothstep(blendForSpin, 0.58, 0.9);
|
|
4182
|
+
spinAmount *= 1 - 0.88 * blendSpinDamp;
|
|
4183
|
+
if (zenithProjectionLockActive) spinAmount = Math.min(spinAmount, 0.02);
|
|
4184
|
+
spinAmount = Math.max(0.02, Math.min(1, spinAmount));
|
|
2424
4185
|
if (spinAmount < 0.999) {
|
|
2425
|
-
const identityQuat = new
|
|
4186
|
+
const identityQuat = new THREE6__namespace.Quaternion();
|
|
2426
4187
|
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2427
4188
|
}
|
|
2428
4189
|
const y = Math.sin(state.lat);
|
|
2429
4190
|
const r = Math.cos(state.lat);
|
|
2430
4191
|
const x = r * Math.sin(state.lon);
|
|
2431
4192
|
const z = -r * Math.cos(state.lon);
|
|
2432
|
-
const currentLook = new
|
|
4193
|
+
const currentLook = new THREE6__namespace.Vector3(x, y, z);
|
|
2433
4194
|
const camForward = currentLook.clone().normalize();
|
|
2434
4195
|
const camUp = camera.up.clone();
|
|
2435
|
-
const camRight = new
|
|
2436
|
-
const camUpOrtho = new
|
|
2437
|
-
const mat = new
|
|
2438
|
-
const qOld = new
|
|
4196
|
+
const camRight = new THREE6__namespace.Vector3().crossVectors(camForward, camUp).normalize();
|
|
4197
|
+
const camUpOrtho = new THREE6__namespace.Vector3().crossVectors(camRight, camForward).normalize();
|
|
4198
|
+
const mat = new THREE6__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
4199
|
+
const qOld = new THREE6__namespace.Quaternion().setFromRotationMatrix(mat);
|
|
2439
4200
|
const qNew = qOld.clone().multiply(quaternion);
|
|
2440
|
-
const newForward = new
|
|
4201
|
+
const newForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
2441
4202
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
2442
4203
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
2443
|
-
const newUp = new
|
|
4204
|
+
const newUp = new THREE6__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
2444
4205
|
camera.up.copy(newUp);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
4206
|
+
const zenithBiasStartFov = getZenithBiasStartFov();
|
|
4207
|
+
if (!zenithProjectionLockActive && !getSceneDebug()?.disableZenithBias && !isInTransitionFreezeBand(state.fov) && e.deltaY > 0 && state.fov > zenithBiasStartFov) {
|
|
4208
|
+
const range = ENGINE_CONFIG.maxFov - zenithBiasStartFov;
|
|
4209
|
+
let t = (state.fov - zenithBiasStartFov) / range;
|
|
2448
4210
|
t = Math.max(0, Math.min(1, t));
|
|
2449
4211
|
const bias = ENGINE_CONFIG.zenithStrength * t;
|
|
2450
4212
|
const zenithLat = Math.PI / 2 - 1e-3;
|
|
@@ -2536,13 +4298,15 @@ function createEngine({
|
|
|
2536
4298
|
}
|
|
2537
4299
|
}
|
|
2538
4300
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2539
|
-
const
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
|
|
4301
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4302
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4303
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4304
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4305
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4306
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2543
4307
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2544
|
-
state.velocityX =
|
|
2545
|
-
state.velocityY =
|
|
4308
|
+
state.velocityX = moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4309
|
+
state.velocityY = moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
2546
4310
|
state.lon = state.targetLon;
|
|
2547
4311
|
state.lat = state.targetLat;
|
|
2548
4312
|
} else if (touches.length === 2) {
|
|
@@ -2554,9 +4318,10 @@ function createEngine({
|
|
|
2554
4318
|
state.fov = state.pinchStartFov / scale;
|
|
2555
4319
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2556
4320
|
handlers.onFovChange?.(state.fov);
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
4321
|
+
const zenithBiasStartFov = getZenithBiasStartFov();
|
|
4322
|
+
if (!zenithProjectionLockActive && !getSceneDebug()?.disableZenithBias && !isInTransitionFreezeBand(state.fov) && state.fov > prevFov && state.fov > zenithBiasStartFov) {
|
|
4323
|
+
const range = ENGINE_CONFIG.maxFov - zenithBiasStartFov;
|
|
4324
|
+
let t = (state.fov - zenithBiasStartFov) / range;
|
|
2560
4325
|
t = Math.max(0, Math.min(1, t));
|
|
2561
4326
|
const bias = ENGINE_CONFIG.zenithStrength * t;
|
|
2562
4327
|
const zenithLat = Math.PI / 2 - 1e-3;
|
|
@@ -2569,8 +4334,12 @@ function createEngine({
|
|
|
2569
4334
|
state.lastMouseX = center.x;
|
|
2570
4335
|
state.lastMouseY = center.y;
|
|
2571
4336
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
2572
|
-
|
|
2573
|
-
|
|
4337
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4338
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4339
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4340
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4341
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
4342
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5 * latFactor;
|
|
2574
4343
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
2575
4344
|
state.lon = state.targetLon;
|
|
2576
4345
|
state.lat = state.targetLat;
|
|
@@ -2733,7 +4502,9 @@ function createEngine({
|
|
|
2733
4502
|
if (inZoneX || inZoneY) {
|
|
2734
4503
|
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
2735
4504
|
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
2736
|
-
const
|
|
4505
|
+
const edgeMassFactor = getMovementMassFactor(state.fov, ENGINE_CONFIG.edgePanMassWideFov);
|
|
4506
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov) * edgeMassFactor;
|
|
4507
|
+
const verticalPanFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
2737
4508
|
if (mouseNDC.x < -1 + t) {
|
|
2738
4509
|
const s = (-1 + t - mouseNDC.x) / t;
|
|
2739
4510
|
panX = -s * s * speedBase;
|
|
@@ -2743,10 +4514,10 @@ function createEngine({
|
|
|
2743
4514
|
}
|
|
2744
4515
|
if (mouseNDC.y < -1 + t) {
|
|
2745
4516
|
const s = (-1 + t - mouseNDC.y) / t;
|
|
2746
|
-
panY = -s * s * speedBase;
|
|
4517
|
+
panY = -s * s * speedBase * verticalPanFactor;
|
|
2747
4518
|
} else if (mouseNDC.y > 1 - t) {
|
|
2748
4519
|
const s = (mouseNDC.y - (1 - t)) / t;
|
|
2749
|
-
panY = s * s * speedBase;
|
|
4520
|
+
panY = s * s * speedBase * verticalPanFactor;
|
|
2750
4521
|
}
|
|
2751
4522
|
}
|
|
2752
4523
|
} else {
|
|
@@ -2778,26 +4549,56 @@ function createEngine({
|
|
|
2778
4549
|
state.targetLat = state.lat;
|
|
2779
4550
|
} else if (!state.isDragging && !flyToActive) {
|
|
2780
4551
|
state.lon += state.velocityX;
|
|
4552
|
+
state.velocityY *= getVerticalPanFactor(state.fov, state.lat);
|
|
2781
4553
|
state.lat += state.velocityY;
|
|
2782
|
-
const
|
|
4554
|
+
const baseDamping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
|
|
4555
|
+
const speed = Math.hypot(state.velocityX, state.velocityY);
|
|
4556
|
+
const damping = speed < ENGINE_CONFIG.lowSpeedVelocityThreshold ? Math.min(baseDamping, ENGINE_CONFIG.lowSpeedInertiaDamping) : baseDamping;
|
|
2783
4557
|
state.velocityX *= damping;
|
|
2784
4558
|
state.velocityY *= damping;
|
|
2785
|
-
if (Math.abs(state.velocityX) <
|
|
2786
|
-
if (Math.abs(state.velocityY) <
|
|
4559
|
+
if (Math.abs(state.velocityX) < ENGINE_CONFIG.velocityStopThreshold) state.velocityX = 0;
|
|
4560
|
+
if (Math.abs(state.velocityY) < ENGINE_CONFIG.velocityStopThreshold) state.velocityY = 0;
|
|
2787
4561
|
}
|
|
2788
4562
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
4563
|
+
if (!flyToActive) {
|
|
4564
|
+
const latDeg = THREE6__namespace.MathUtils.radToDeg(state.lat);
|
|
4565
|
+
if (latDeg < HORIZON_ZOOM_CONFIG.latStartDeg) {
|
|
4566
|
+
const t = THREE6__namespace.MathUtils.clamp(latDeg / HORIZON_ZOOM_CONFIG.latStartDeg, 0, 1);
|
|
4567
|
+
const maxFov = THREE6__namespace.MathUtils.lerp(
|
|
4568
|
+
HORIZON_ZOOM_CONFIG.safeFovAtHorizon,
|
|
4569
|
+
ENGINE_CONFIG.maxFov,
|
|
4570
|
+
t
|
|
4571
|
+
);
|
|
4572
|
+
if (state.fov > maxFov) {
|
|
4573
|
+
state.fov = THREE6__namespace.MathUtils.lerp(state.fov, maxFov, HORIZON_ZOOM_CONFIG.lerpRate);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
applyZenithAutoCenter();
|
|
2789
4578
|
const y = Math.sin(state.lat);
|
|
2790
4579
|
const r = Math.cos(state.lat);
|
|
2791
4580
|
const x = r * Math.sin(state.lon);
|
|
2792
4581
|
const z = -r * Math.cos(state.lon);
|
|
2793
|
-
const target = new
|
|
2794
|
-
const idealUp = new
|
|
4582
|
+
const target = new THREE6__namespace.Vector3(x, y, z);
|
|
4583
|
+
const idealUp = new THREE6__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
2795
4584
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
2796
4585
|
camera.up.normalize();
|
|
2797
4586
|
camera.lookAt(target);
|
|
2798
4587
|
camera.updateMatrixWorld();
|
|
2799
4588
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
4589
|
+
if (groundMaterial?.uniforms?.uZenithFlatten) {
|
|
4590
|
+
const targetFlatten = getSceneDebug()?.disableZenithFlatten ? 0 : THREE6__namespace.MathUtils.smoothstep(
|
|
4591
|
+
state.lat,
|
|
4592
|
+
THREE6__namespace.MathUtils.degToRad(68),
|
|
4593
|
+
THREE6__namespace.MathUtils.degToRad(88)
|
|
4594
|
+
);
|
|
4595
|
+
const prevFlatten = Number(groundMaterial.uniforms.uZenithFlatten.value ?? 0);
|
|
4596
|
+
const flatten = isInTransitionFreezeBand(state.fov) ? THREE6__namespace.MathUtils.clamp(targetFlatten, prevFlatten - 0.01, prevFlatten + 0.01) : targetFlatten;
|
|
4597
|
+
groundMaterial.uniforms.uZenithFlatten.value = flatten;
|
|
4598
|
+
}
|
|
2800
4599
|
updateUniforms();
|
|
4600
|
+
if (getSceneDebug()?.horizonDiagnostics) runHorizonDiagnostics(now);
|
|
4601
|
+
updateChapterLabelAnchors();
|
|
2801
4602
|
const nowSec = now / 1e3;
|
|
2802
4603
|
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2803
4604
|
lastTickTime = nowSec;
|
|
@@ -2805,21 +4606,44 @@ function createEngine({
|
|
|
2805
4606
|
linesFader.update(dt);
|
|
2806
4607
|
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2807
4608
|
artFader.update(dt);
|
|
2808
|
-
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
}
|
|
4609
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01, camera, dt);
|
|
4610
|
+
const baseArtOpacity = THREE6__namespace.MathUtils.clamp(currentConfig?.constellationBaseOpacity ?? 1, 0, 300);
|
|
4611
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased * baseArtOpacity);
|
|
2812
4612
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
4613
|
+
const revealZoom = currentConfig?.starZoomReveal ?? true ? THREE6__namespace.MathUtils.clamp(
|
|
4614
|
+
(ZOOM_REVEAL_CONFIG.wideFov - state.fov) / (ZOOM_REVEAL_CONFIG.wideFov - ZOOM_REVEAL_CONFIG.narrowFov),
|
|
4615
|
+
0,
|
|
4616
|
+
1
|
|
4617
|
+
) : 1;
|
|
4618
|
+
if (backdropStarsMaterial?.uniforms) {
|
|
4619
|
+
const minGain = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropWideFovGain ?? 0.42, 0, 1);
|
|
4620
|
+
const fovT = THREE6__namespace.MathUtils.smoothstep(state.fov, 24, 100);
|
|
4621
|
+
const gain = THREE6__namespace.MathUtils.lerp(1, minGain, fovT);
|
|
4622
|
+
backdropStarsMaterial.uniforms.uBackdropGain.value = gain;
|
|
4623
|
+
backdropStarsMaterial.uniforms.uBackdropEnergy.value = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropEnergy ?? 2.2, 0.2, 5);
|
|
4624
|
+
backdropStarsMaterial.uniforms.uBackdropSizeExp.value = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropSizeExponent ?? 0.9, 0.4, 1.4);
|
|
4625
|
+
backdropStarsMaterial.uniforms.uRevealZoom.value = revealZoom;
|
|
4626
|
+
}
|
|
4627
|
+
if (starPoints?.material) {
|
|
4628
|
+
const sm = starPoints.material;
|
|
4629
|
+
if (sm.uniforms.uRevealZoom) sm.uniforms.uRevealZoom.value = revealZoom;
|
|
4630
|
+
}
|
|
4631
|
+
if (skyBackgroundMesh) skyBackgroundMesh.visible = currentConfig?.background !== "transparent";
|
|
2813
4632
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2814
|
-
|
|
2815
|
-
|
|
4633
|
+
if (moonMesh) moonMesh.visible = currentConfig?.showMoon ?? true;
|
|
4634
|
+
if (moonGlowMesh) moonGlowMesh.visible = currentConfig?.showMoon ?? true;
|
|
4635
|
+
const showSun = currentConfig?.showSunrise ?? true;
|
|
4636
|
+
if (sunDiscMesh) sunDiscMesh.visible = showSun;
|
|
4637
|
+
if (sunHaloMesh) sunHaloMesh.visible = showSun;
|
|
4638
|
+
if (milkyWayMesh) milkyWayMesh.visible = currentConfig?.showMilkyWay ?? true;
|
|
2816
4639
|
if (constellationLines) {
|
|
2817
4640
|
constellationLines.visible = linesFader.eased > 0.01;
|
|
2818
4641
|
if (constellationLines.visible && constellationLines.material) {
|
|
2819
4642
|
const mat = constellationLines.material;
|
|
2820
4643
|
if (mat.uniforms?.color) {
|
|
2821
4644
|
mat.uniforms.color.value.setHex(11193599);
|
|
2822
|
-
mat.
|
|
4645
|
+
if (mat.uniforms.uReveal) mat.uniforms.uReveal.value = linesFader.eased;
|
|
4646
|
+
mat.opacity = 1;
|
|
2823
4647
|
}
|
|
2824
4648
|
}
|
|
2825
4649
|
}
|
|
@@ -2830,116 +4654,35 @@ function createEngine({
|
|
|
2830
4654
|
const screenW = rect.width;
|
|
2831
4655
|
const screenH = rect.height;
|
|
2832
4656
|
const aspect = screenW / screenH;
|
|
2833
|
-
const
|
|
2834
|
-
const occupied = [];
|
|
2835
|
-
function isOverlapping(x2, y2, w, h) {
|
|
2836
|
-
for (const r2 of occupied) {
|
|
2837
|
-
if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
|
|
2838
|
-
}
|
|
2839
|
-
return false;
|
|
2840
|
-
}
|
|
2841
|
-
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
2842
|
-
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2843
|
-
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2844
|
-
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2845
|
-
const showChapters = state.fov < 45;
|
|
2846
|
-
for (const item of dynamicLabels) {
|
|
2847
|
-
const uniforms = item.obj.material.uniforms;
|
|
2848
|
-
const level = item.node.level;
|
|
2849
|
-
let isEnabled = false;
|
|
2850
|
-
if (level === 2 && showBookLabels) isEnabled = true;
|
|
2851
|
-
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
2852
|
-
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2853
|
-
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
2854
|
-
if (!isEnabled) {
|
|
2855
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2856
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2857
|
-
continue;
|
|
2858
|
-
}
|
|
2859
|
-
const pWorld = item.obj.position;
|
|
2860
|
-
const pProj = smartProjectJS(pWorld);
|
|
2861
|
-
if (pProj.z > 0.2) {
|
|
2862
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2863
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2864
|
-
continue;
|
|
2865
|
-
}
|
|
2866
|
-
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2867
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2868
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2869
|
-
continue;
|
|
2870
|
-
}
|
|
2871
|
-
const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
|
|
2872
|
-
const ndcY = pProj.y * globalUniforms.uScale.value;
|
|
2873
|
-
const sX = (ndcX * 0.5 + 0.5) * screenW;
|
|
2874
|
-
const sY = (-ndcY * 0.5 + 0.5) * screenH;
|
|
2875
|
-
const size = uniforms.uSize.value;
|
|
2876
|
-
const pixelH = size.y * screenH * 0.8;
|
|
2877
|
-
const pixelW = size.x * screenH * 0.8;
|
|
2878
|
-
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
2879
|
-
}
|
|
2880
|
-
const hoverId = handlers._lastHoverId;
|
|
4657
|
+
const hoverId = handlers._lastHoverId ?? null;
|
|
2881
4658
|
const selectedId = state.draggedNodeId;
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
4659
|
+
labelManager.update({
|
|
4660
|
+
nowMs: now,
|
|
4661
|
+
dt,
|
|
4662
|
+
fov: state.fov,
|
|
4663
|
+
camera,
|
|
4664
|
+
projectionId: currentProjection.id,
|
|
4665
|
+
screenW,
|
|
4666
|
+
screenH,
|
|
4667
|
+
globalScale: globalUniforms.uScale.value,
|
|
4668
|
+
aspect,
|
|
4669
|
+
hoverId,
|
|
4670
|
+
selectedId,
|
|
4671
|
+
focusedId: focusedNodeId,
|
|
4672
|
+
shouldFilter: !!currentFilter && filterStrength > 0.01,
|
|
4673
|
+
isNodeFiltered: (node) => {
|
|
4674
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
4675
|
+
return !!nodeToCheck && isNodeFiltered(nodeToCheck);
|
|
4676
|
+
},
|
|
4677
|
+
toggles: {
|
|
4678
|
+
showBookLabels: currentConfig?.showBookLabels === true,
|
|
4679
|
+
showDivisionLabels: currentConfig?.showDivisionLabels === true,
|
|
4680
|
+
showChapterLabels: currentConfig?.showChapterLabels === true,
|
|
4681
|
+
showGroupLabels: currentConfig?.showGroupLabels === true
|
|
4682
|
+
},
|
|
4683
|
+
config: currentConfig?.labelBehavior,
|
|
4684
|
+
project: smartProjectJS
|
|
2892
4685
|
});
|
|
2893
|
-
for (const l of labelsToCheck) {
|
|
2894
|
-
let target2 = 0;
|
|
2895
|
-
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2896
|
-
if (l.level === 1) {
|
|
2897
|
-
let rot = 0;
|
|
2898
|
-
const isWideAngle = currentProjection.id !== "perspective";
|
|
2899
|
-
if (isWideAngle) {
|
|
2900
|
-
const dx = l.sX - screenW / 2;
|
|
2901
|
-
const dy = l.sY - screenH / 2;
|
|
2902
|
-
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
2903
|
-
}
|
|
2904
|
-
l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2905
|
-
}
|
|
2906
|
-
if (l.level === 2) {
|
|
2907
|
-
{
|
|
2908
|
-
target2 = 1;
|
|
2909
|
-
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2910
|
-
}
|
|
2911
|
-
} else if (l.level === 1) {
|
|
2912
|
-
if (showDivisions || isSpecial) {
|
|
2913
|
-
const pad = -5;
|
|
2914
|
-
if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
|
|
2915
|
-
target2 = 1;
|
|
2916
|
-
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
} else if (l.level === 2.5 || l.level === 3) {
|
|
2920
|
-
if (showChapters || isSpecial) {
|
|
2921
|
-
target2 = 1;
|
|
2922
|
-
if (!isSpecial) {
|
|
2923
|
-
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2924
|
-
const focusFade = 1 - THREE5__namespace.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2925
|
-
target2 *= focusFade;
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
if (target2 > 0 && currentFilter && filterStrength > 0.01) {
|
|
2930
|
-
const node = l.item.node;
|
|
2931
|
-
if (node.level === 3) {
|
|
2932
|
-
target2 = 0;
|
|
2933
|
-
} else if (node.level === 2 || node.level === 2.5) {
|
|
2934
|
-
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
2935
|
-
if (nodeToCheck && isNodeFiltered(nodeToCheck)) {
|
|
2936
|
-
target2 = 0;
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2941
|
-
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
2942
|
-
}
|
|
2943
4686
|
renderer.render(scene, camera);
|
|
2944
4687
|
}
|
|
2945
4688
|
function stop() {
|
|
@@ -2964,6 +4707,42 @@ function createEngine({
|
|
|
2964
4707
|
function dispose() {
|
|
2965
4708
|
stop();
|
|
2966
4709
|
constellationLayer.dispose();
|
|
4710
|
+
if (moonMesh) {
|
|
4711
|
+
scene.remove(moonMesh);
|
|
4712
|
+
moonMesh.geometry.dispose();
|
|
4713
|
+
moonMesh.material.dispose();
|
|
4714
|
+
moonMesh = null;
|
|
4715
|
+
}
|
|
4716
|
+
if (moonGlowMesh) {
|
|
4717
|
+
scene.remove(moonGlowMesh);
|
|
4718
|
+
moonGlowMesh.geometry.dispose();
|
|
4719
|
+
moonGlowMesh.material.dispose();
|
|
4720
|
+
moonGlowMesh = null;
|
|
4721
|
+
}
|
|
4722
|
+
if (sunDiscMesh) {
|
|
4723
|
+
scene.remove(sunDiscMesh);
|
|
4724
|
+
sunDiscMesh.geometry.dispose();
|
|
4725
|
+
sunDiscMesh.material.dispose();
|
|
4726
|
+
sunDiscMesh = null;
|
|
4727
|
+
}
|
|
4728
|
+
if (sunHaloMesh) {
|
|
4729
|
+
scene.remove(sunHaloMesh);
|
|
4730
|
+
sunHaloMesh.geometry.dispose();
|
|
4731
|
+
sunHaloMesh.material.dispose();
|
|
4732
|
+
sunHaloMesh = null;
|
|
4733
|
+
}
|
|
4734
|
+
if (milkyWayMesh) {
|
|
4735
|
+
scene.remove(milkyWayMesh);
|
|
4736
|
+
milkyWayMesh.geometry.dispose();
|
|
4737
|
+
milkyWayMesh.material.dispose();
|
|
4738
|
+
milkyWayMesh = null;
|
|
4739
|
+
}
|
|
4740
|
+
if (skyBackgroundMesh) {
|
|
4741
|
+
scene.remove(skyBackgroundMesh);
|
|
4742
|
+
skyBackgroundMesh.geometry.dispose();
|
|
4743
|
+
skyBackgroundMesh.material.dispose();
|
|
4744
|
+
skyBackgroundMesh = null;
|
|
4745
|
+
}
|
|
2967
4746
|
renderer.dispose();
|
|
2968
4747
|
renderer.domElement.remove();
|
|
2969
4748
|
}
|
|
@@ -2983,6 +4762,7 @@ function createEngine({
|
|
|
2983
4762
|
function flyTo(nodeId, targetFov) {
|
|
2984
4763
|
const node = nodeById.get(nodeId);
|
|
2985
4764
|
if (!node) return;
|
|
4765
|
+
focusedNodeId = nodeId;
|
|
2986
4766
|
const pos = getPosition(node).normalize();
|
|
2987
4767
|
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2988
4768
|
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
@@ -3005,7 +4785,7 @@ function createEngine({
|
|
|
3005
4785
|
}
|
|
3006
4786
|
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled, setHierarchyFilter, flyTo, setProjection };
|
|
3007
4787
|
}
|
|
3008
|
-
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
4788
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG, HORIZON_ZOOM_CONFIG, ZOOM_REVEAL_CONFIG;
|
|
3009
4789
|
var init_createEngine = __esm({
|
|
3010
4790
|
"src/engine/createEngine.ts"() {
|
|
3011
4791
|
init_layout();
|
|
@@ -3013,14 +4793,35 @@ var init_createEngine = __esm({
|
|
|
3013
4793
|
init_ConstellationArtworkLayer();
|
|
3014
4794
|
init_projections();
|
|
3015
4795
|
init_fader();
|
|
4796
|
+
init_LabelManager();
|
|
3016
4797
|
ENGINE_CONFIG = {
|
|
3017
4798
|
minFov: 1,
|
|
3018
4799
|
maxFov: 135,
|
|
3019
|
-
defaultFov:
|
|
4800
|
+
defaultFov: 35,
|
|
3020
4801
|
dragSpeed: 125e-5,
|
|
3021
4802
|
inertiaDamping: 0.92,
|
|
4803
|
+
lowSpeedInertiaDamping: 0.78,
|
|
4804
|
+
lowSpeedVelocityThreshold: 25e-4,
|
|
4805
|
+
velocityStopThreshold: 4e-5,
|
|
4806
|
+
zoomResistanceWideFov: 0.82,
|
|
4807
|
+
movementMassWideFov: 0.74,
|
|
4808
|
+
edgePanMassWideFov: 0.68,
|
|
4809
|
+
inputCompression: 0.018,
|
|
3022
4810
|
blendStart: 35,
|
|
3023
4811
|
blendEnd: 83,
|
|
4812
|
+
freezeBandStartFov: 76,
|
|
4813
|
+
freezeBandEndFov: 84,
|
|
4814
|
+
zenithBiasStartFov: 85,
|
|
4815
|
+
zenithLockBlendEnter: 0.9,
|
|
4816
|
+
zenithLockBlendExit: 0.8,
|
|
4817
|
+
zenithAutoCenterBlendStart: 0.62,
|
|
4818
|
+
zenithAutoCenterBlendEnd: 0.9,
|
|
4819
|
+
zenithAutoCenterMinLerp: 0.012,
|
|
4820
|
+
zenithAutoCenterMaxLerp: 0.16,
|
|
4821
|
+
verticalPanDampStartFov: 72,
|
|
4822
|
+
verticalPanDampEndFov: 96,
|
|
4823
|
+
verticalPanDampLatStartDeg: 45,
|
|
4824
|
+
verticalPanDampLatEndDeg: 82,
|
|
3024
4825
|
zenithStartFov: 75,
|
|
3025
4826
|
zenithStrength: 0.15,
|
|
3026
4827
|
horizonLockStrength: 0.05,
|
|
@@ -3043,10 +4844,34 @@ var init_createEngine = __esm({
|
|
|
3043
4844
|
};
|
|
3044
4845
|
ORDER_REVEAL_CONFIG = {
|
|
3045
4846
|
globalDim: 0.85,
|
|
3046
|
-
pulseAmplitude: 0.
|
|
4847
|
+
pulseAmplitude: 0.12,
|
|
3047
4848
|
pulseDuration: 2,
|
|
3048
4849
|
delayPerChapter: 0.1
|
|
3049
4850
|
};
|
|
4851
|
+
HORIZON_ZOOM_CONFIG = {
|
|
4852
|
+
latStartDeg: 20,
|
|
4853
|
+
// coupling is fully off above this elevation
|
|
4854
|
+
safeFovAtHorizon: 60,
|
|
4855
|
+
// max FOV at the horizon (below freeze-band threshold)
|
|
4856
|
+
lerpRate: 0.03
|
|
4857
|
+
// gentle — should feel like a natural breathing-in
|
|
4858
|
+
};
|
|
4859
|
+
ZOOM_REVEAL_CONFIG = {
|
|
4860
|
+
wideFov: 120,
|
|
4861
|
+
// above this FOV, revealZoom = 0 (nothing new revealed)
|
|
4862
|
+
narrowFov: 8,
|
|
4863
|
+
// below this FOV, revealZoom = 1 (everything visible)
|
|
4864
|
+
zoomCurveExp: 1.8,
|
|
4865
|
+
// non-linear curve exponent (try 1.5 – 2.5)
|
|
4866
|
+
chapterRevealMax: 0.5,
|
|
4867
|
+
// faintest chapter star threshold — visible by ~fov 35
|
|
4868
|
+
chapterFeather: 0.1,
|
|
4869
|
+
// smoothstep width for chapter star fade-in
|
|
4870
|
+
backdropRevealStart: 0.4,
|
|
4871
|
+
// backdrop starts appearing at this mappedZoom
|
|
4872
|
+
backdropRevealEnd: 0.65
|
|
4873
|
+
// backdrop fully visible at this mappedZoom
|
|
4874
|
+
};
|
|
3050
4875
|
}
|
|
3051
4876
|
});
|
|
3052
4877
|
var StarMap = react.forwardRef(
|
|
@@ -32233,7 +34058,7 @@ var RNG = class {
|
|
|
32233
34058
|
const r = Math.sqrt(1 - y * y);
|
|
32234
34059
|
const x = r * Math.cos(theta);
|
|
32235
34060
|
const z = r * Math.sin(theta);
|
|
32236
|
-
return new
|
|
34061
|
+
return new THREE6__namespace.Vector3(x, y, z);
|
|
32237
34062
|
}
|
|
32238
34063
|
};
|
|
32239
34064
|
function simpleNoise3D(v, scale) {
|
|
@@ -32271,11 +34096,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
32271
34096
|
});
|
|
32272
34097
|
});
|
|
32273
34098
|
const bookCount = books.length;
|
|
32274
|
-
const mwRad =
|
|
32275
|
-
const mwNormal = new
|
|
34099
|
+
const mwRad = THREE6__namespace.MathUtils.degToRad(opts.milkyWayAngle);
|
|
34100
|
+
const mwNormal = new THREE6__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
32276
34101
|
const anchors = [];
|
|
32277
34102
|
for (let i = 0; i < bookCount; i++) {
|
|
32278
|
-
let bestP = new
|
|
34103
|
+
let bestP = new THREE6__namespace.Vector3();
|
|
32279
34104
|
let valid = false;
|
|
32280
34105
|
let attempt = 0;
|
|
32281
34106
|
while (!valid && attempt < 100) {
|
|
@@ -32301,7 +34126,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32301
34126
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
32302
34127
|
for (let c = 0; c < book.chapters; c++) {
|
|
32303
34128
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
32304
|
-
const offset = new
|
|
34129
|
+
const offset = new THREE6__namespace.Vector3(
|
|
32305
34130
|
(rng.next() - 0.5) * 2,
|
|
32306
34131
|
(rng.next() - 0.5) * 2,
|
|
32307
34132
|
(rng.next() - 0.5) * 2
|
|
@@ -32322,7 +34147,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32322
34147
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
32323
34148
|
const divId = `D:${book.testament}:${book.division}`;
|
|
32324
34149
|
if (!divisions.has(divId)) {
|
|
32325
|
-
divisions.set(divId, { sum: new
|
|
34150
|
+
divisions.set(divId, { sum: new THREE6__namespace.Vector3(), count: 0 });
|
|
32326
34151
|
}
|
|
32327
34152
|
const entry = divisions.get(divId);
|
|
32328
34153
|
entry.sum.add(anchorPos);
|