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