@project-skymap/library 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2127 -503
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -1
- package/dist/index.d.ts +78 -1
- package/dist/index.js +2126 -502
- 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);
|
|
763
|
+
}
|
|
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;
|
|
665
779
|
}
|
|
666
|
-
|
|
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,12 +1402,15 @@ 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
|
+
}
|
|
955
1414
|
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
956
1415
|
function mix(a, b, t) {
|
|
957
1416
|
return a * (1 - t) + b * t;
|
|
@@ -960,6 +1419,7 @@ function createEngine({
|
|
|
960
1419
|
function syncProjectionState() {
|
|
961
1420
|
if (currentProjection instanceof BlendedProjection) {
|
|
962
1421
|
currentProjection.setFov(state.fov);
|
|
1422
|
+
currentProjection.setBlendOverride(getSceneDebug()?.projectionBlendOverride ?? null);
|
|
963
1423
|
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
964
1424
|
}
|
|
965
1425
|
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
@@ -987,7 +1447,7 @@ function createEngine({
|
|
|
987
1447
|
const uvX = mouseNDC.x * aspectRatio;
|
|
988
1448
|
const uvY = mouseNDC.y;
|
|
989
1449
|
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
990
|
-
return new
|
|
1450
|
+
return new THREE6.Vector3(v.x, v.y, v.z).normalize();
|
|
991
1451
|
}
|
|
992
1452
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
993
1453
|
const aspect = width / height;
|
|
@@ -996,7 +1456,7 @@ function createEngine({
|
|
|
996
1456
|
syncProjectionState();
|
|
997
1457
|
const fovRad = state.fov * Math.PI / 180;
|
|
998
1458
|
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
999
|
-
const vView = new
|
|
1459
|
+
const vView = new THREE6.Vector3(v.x, v.y, v.z).normalize();
|
|
1000
1460
|
return vView.applyQuaternion(camera.quaternion);
|
|
1001
1461
|
}
|
|
1002
1462
|
function smartProjectJS(worldPos) {
|
|
@@ -1006,80 +1466,438 @@ function createEngine({
|
|
|
1006
1466
|
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
1007
1467
|
return result;
|
|
1008
1468
|
}
|
|
1009
|
-
const groundGroup = new
|
|
1469
|
+
const groundGroup = new THREE6.Group();
|
|
1010
1470
|
scene.add(groundGroup);
|
|
1471
|
+
const MAX_HORIZON_POINTS = 64;
|
|
1472
|
+
let groundMaterial = null;
|
|
1473
|
+
let horizonLine = null;
|
|
1474
|
+
let activeHorizonProfile = {
|
|
1475
|
+
mode: 0,
|
|
1476
|
+
pointCount: 0,
|
|
1477
|
+
azDeg: [],
|
|
1478
|
+
altDeg: [],
|
|
1479
|
+
rotateRad: 0,
|
|
1480
|
+
baseAltDeg: 3
|
|
1481
|
+
};
|
|
1482
|
+
let lastHorizonDiagTs = 0;
|
|
1483
|
+
function toColor(input, fallbackHex) {
|
|
1484
|
+
if (!input) return new THREE6.Color(fallbackHex);
|
|
1485
|
+
try {
|
|
1486
|
+
return new THREE6.Color(input);
|
|
1487
|
+
} catch {
|
|
1488
|
+
return new THREE6.Color(fallbackHex);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function applyGroundTheme(cfg) {
|
|
1492
|
+
if (!groundMaterial) return;
|
|
1493
|
+
const theme = getSceneDebug()?.disableHorizonTheme ? void 0 : cfg?.horizonTheme;
|
|
1494
|
+
const uniforms = groundMaterial.uniforms;
|
|
1495
|
+
const atmo = theme?.atmosphere;
|
|
1496
|
+
const mode = theme?.source === "polygonal" && (theme.profile?.points?.length ?? 0) >= 2 ? 1 : 0;
|
|
1497
|
+
const groundColor = toColor(theme?.groundColor, 65794);
|
|
1498
|
+
const fogColor = toColor(theme?.horizonLineColor, 663098);
|
|
1499
|
+
const fogIntensity = THREE6.MathUtils.clamp(atmo?.fogIntensity ?? 0.6, 0, 1.5);
|
|
1500
|
+
const fogVisible = atmo?.fogVisible === false ? 0 : 1;
|
|
1501
|
+
const minBrightness = THREE6.MathUtils.clamp(atmo?.minimalBrightness ?? 0, 0, 1);
|
|
1502
|
+
const rotateRad = (theme?.profile?.angleRotateZDeg ?? 0) * Math.PI / 180;
|
|
1503
|
+
const azSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1504
|
+
const altSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1505
|
+
let pointCount = 0;
|
|
1506
|
+
let sortedPoints = [];
|
|
1507
|
+
if (mode === 1 && theme?.profile?.points) {
|
|
1508
|
+
sortedPoints = [...theme.profile.points].map((p) => ({
|
|
1509
|
+
azDeg: (p.azDeg % 360 + 360) % 360,
|
|
1510
|
+
altDeg: THREE6.MathUtils.clamp(p.altDeg, -30, 35)
|
|
1511
|
+
})).sort((a, b) => a.azDeg - b.azDeg);
|
|
1512
|
+
pointCount = Math.min(sortedPoints.length, MAX_HORIZON_POINTS);
|
|
1513
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1514
|
+
azSamples[i] = sortedPoints[i].azDeg;
|
|
1515
|
+
altSamples[i] = sortedPoints[i].altDeg;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
const baseAltDeg = pointCount > 0 ? altSamples.slice(0, pointCount).reduce((sum, v) => sum + v, 0) / pointCount : 3;
|
|
1519
|
+
activeHorizonProfile = {
|
|
1520
|
+
mode,
|
|
1521
|
+
pointCount,
|
|
1522
|
+
azDeg: azSamples.slice(0, pointCount),
|
|
1523
|
+
altDeg: altSamples.slice(0, pointCount),
|
|
1524
|
+
rotateRad,
|
|
1525
|
+
baseAltDeg
|
|
1526
|
+
};
|
|
1527
|
+
uniforms.color.value = groundColor;
|
|
1528
|
+
uniforms.fogColor.value = fogColor;
|
|
1529
|
+
uniforms.uFogIntensity.value = fogIntensity;
|
|
1530
|
+
uniforms.uFogVisible.value = fogVisible;
|
|
1531
|
+
uniforms.uMinBrightness.value = minBrightness;
|
|
1532
|
+
uniforms.uHorizonMode.value = mode;
|
|
1533
|
+
uniforms.uHorizonPointCount.value = pointCount;
|
|
1534
|
+
uniforms.uHorizonAzDeg.value = azSamples;
|
|
1535
|
+
uniforms.uHorizonAltDeg.value = altSamples;
|
|
1536
|
+
uniforms.uHorizonRotateRad.value = rotateRad;
|
|
1537
|
+
uniforms.uBaseAltDeg.value = baseAltDeg;
|
|
1538
|
+
groundMaterial.uniformsNeedUpdate = true;
|
|
1539
|
+
if (atmosphereMesh && atmosphereMesh.material instanceof THREE6.ShaderMaterial) {
|
|
1540
|
+
const atmUniforms = atmosphereMesh.material.uniforms;
|
|
1541
|
+
const topAltDeg = atmo?.fogBandTopAltDeg ?? 90;
|
|
1542
|
+
const bottomAltDeg = atmo?.fogBandBottomAltDeg ?? -90;
|
|
1543
|
+
atmUniforms.uThemeFogVisible.value = fogVisible;
|
|
1544
|
+
atmUniforms.uThemeFogIntensity.value = fogIntensity;
|
|
1545
|
+
atmUniforms.uThemeFogTopSin.value = Math.sin(THREE6.MathUtils.degToRad(topAltDeg));
|
|
1546
|
+
atmUniforms.uThemeFogBottomSin.value = Math.sin(THREE6.MathUtils.degToRad(bottomAltDeg));
|
|
1547
|
+
atmUniforms.uThemeMinBrightness.value = minBrightness;
|
|
1548
|
+
atmosphereMesh.material.uniformsNeedUpdate = true;
|
|
1549
|
+
}
|
|
1550
|
+
if (horizonLine) {
|
|
1551
|
+
groundGroup.remove(horizonLine);
|
|
1552
|
+
horizonLine.geometry.dispose();
|
|
1553
|
+
horizonLine.material.dispose();
|
|
1554
|
+
horizonLine = null;
|
|
1555
|
+
}
|
|
1556
|
+
const lineThickness = THREE6.MathUtils.clamp(theme?.horizonLineThickness ?? 0, 0, 8);
|
|
1557
|
+
const shouldDrawLine = mode === 1 && pointCount >= 2 && lineThickness > 0;
|
|
1558
|
+
if (!shouldDrawLine) return;
|
|
1559
|
+
const lineColor = toColor(theme?.horizonLineColor, 5601177);
|
|
1560
|
+
const lineRadius = 997;
|
|
1561
|
+
const pts = [];
|
|
1562
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1563
|
+
const sample = sortedPoints[i];
|
|
1564
|
+
const angleDeg = sample.azDeg - (theme?.profile?.angleRotateZDeg ?? 0);
|
|
1565
|
+
const a = THREE6.MathUtils.degToRad(angleDeg);
|
|
1566
|
+
const alt = THREE6.MathUtils.degToRad(sample.altDeg);
|
|
1567
|
+
const rc = Math.cos(alt);
|
|
1568
|
+
pts.push(new THREE6.Vector3(
|
|
1569
|
+
lineRadius * rc * Math.cos(a),
|
|
1570
|
+
lineRadius * Math.sin(alt),
|
|
1571
|
+
lineRadius * rc * Math.sin(a)
|
|
1572
|
+
));
|
|
1573
|
+
}
|
|
1574
|
+
if (pts.length > 0) pts.push(pts[0].clone());
|
|
1575
|
+
const geo = new THREE6.BufferGeometry().setFromPoints(pts);
|
|
1576
|
+
const mat = createSmartMaterial({
|
|
1577
|
+
uniforms: {
|
|
1578
|
+
color: { value: lineColor },
|
|
1579
|
+
alpha: { value: 0.95 }
|
|
1580
|
+
},
|
|
1581
|
+
vertexShaderBody: `
|
|
1582
|
+
uniform vec3 color;
|
|
1583
|
+
varying vec3 vColor;
|
|
1584
|
+
void main() {
|
|
1585
|
+
vColor = color;
|
|
1586
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1587
|
+
gl_Position = smartProject(mvPosition);
|
|
1588
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1589
|
+
}
|
|
1590
|
+
`,
|
|
1591
|
+
fragmentShader: `
|
|
1592
|
+
uniform float alpha;
|
|
1593
|
+
varying vec3 vColor;
|
|
1594
|
+
void main() {
|
|
1595
|
+
float alphaMask = getMaskAlpha();
|
|
1596
|
+
if (alphaMask < 0.01) discard;
|
|
1597
|
+
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
1598
|
+
}
|
|
1599
|
+
`,
|
|
1600
|
+
transparent: true,
|
|
1601
|
+
depthWrite: false,
|
|
1602
|
+
depthTest: true
|
|
1603
|
+
});
|
|
1604
|
+
const line = new THREE6.Line(geo, mat);
|
|
1605
|
+
line.material.linewidth = lineThickness;
|
|
1606
|
+
line.frustumCulled = false;
|
|
1607
|
+
line.renderOrder = 3;
|
|
1608
|
+
horizonLine = line;
|
|
1609
|
+
groundGroup.add(line);
|
|
1610
|
+
}
|
|
1611
|
+
function sampleActiveHorizonAltDeg(azDeg) {
|
|
1612
|
+
const profile = activeHorizonProfile;
|
|
1613
|
+
if (profile.mode !== 1 || profile.pointCount < 2) return profile.baseAltDeg;
|
|
1614
|
+
const query = ((azDeg + THREE6.MathUtils.radToDeg(profile.rotateRad)) % 360 + 360) % 360;
|
|
1615
|
+
const n = profile.pointCount;
|
|
1616
|
+
const firstAz = profile.azDeg[0];
|
|
1617
|
+
const firstAlt = profile.altDeg[0];
|
|
1618
|
+
for (let i = 1; i < n; i++) {
|
|
1619
|
+
const prevAz2 = profile.azDeg[i - 1];
|
|
1620
|
+
const prevAlt2 = profile.altDeg[i - 1];
|
|
1621
|
+
const curAz = profile.azDeg[i];
|
|
1622
|
+
const curAlt = profile.altDeg[i];
|
|
1623
|
+
if (query >= prevAz2 && query <= curAz) {
|
|
1624
|
+
const t2 = (query - prevAz2) / Math.max(1e-4, curAz - prevAz2);
|
|
1625
|
+
return mix(prevAlt2, curAlt, t2);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const prevAz = profile.azDeg[n - 1];
|
|
1629
|
+
const prevAlt = profile.altDeg[n - 1];
|
|
1630
|
+
const wrappedQuery = query < firstAz ? query + 360 : query;
|
|
1631
|
+
const t = (wrappedQuery - prevAz) / Math.max(1e-4, firstAz + 360 - prevAz);
|
|
1632
|
+
return mix(prevAlt, firstAlt, t);
|
|
1633
|
+
}
|
|
1634
|
+
function runHorizonDiagnostics(nowMs) {
|
|
1635
|
+
if (nowMs - lastHorizonDiagTs < 1200) return;
|
|
1636
|
+
lastHorizonDiagTs = nowMs;
|
|
1637
|
+
const points = [];
|
|
1638
|
+
const r = 997;
|
|
1639
|
+
const scale = globalUniforms.uScale.value;
|
|
1640
|
+
const aspect = Math.max(1e-4, globalUniforms.uAspect.value);
|
|
1641
|
+
for (let az = 0; az < 360; az += 2) {
|
|
1642
|
+
const altDeg = sampleActiveHorizonAltDeg(az);
|
|
1643
|
+
const azRad = THREE6.MathUtils.degToRad(az);
|
|
1644
|
+
const altRad = THREE6.MathUtils.degToRad(altDeg);
|
|
1645
|
+
const rc = Math.cos(altRad);
|
|
1646
|
+
const worldPos = new THREE6.Vector3(
|
|
1647
|
+
r * rc * Math.cos(azRad),
|
|
1648
|
+
r * Math.sin(altRad),
|
|
1649
|
+
r * rc * Math.sin(azRad)
|
|
1650
|
+
);
|
|
1651
|
+
const p = smartProjectJS(worldPos);
|
|
1652
|
+
if (currentProjection.isClipped(p.z)) continue;
|
|
1653
|
+
const x = p.x * scale / aspect;
|
|
1654
|
+
const y = p.y * scale;
|
|
1655
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
1656
|
+
if (Math.abs(x) > 1) continue;
|
|
1657
|
+
points.push({ x, y });
|
|
1658
|
+
}
|
|
1659
|
+
if (points.length < 16) {
|
|
1660
|
+
console.debug(`[HorizonDiag] insufficient visible horizon samples at fov=${state.fov.toFixed(1)}`);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const binCount = 12;
|
|
1664
|
+
const maxY = new Array(binCount).fill(-Infinity);
|
|
1665
|
+
for (const p of points) {
|
|
1666
|
+
const ax = Math.min(0.999, Math.abs(p.x));
|
|
1667
|
+
const idx = Math.floor(ax * binCount);
|
|
1668
|
+
maxY[idx] = Math.max(maxY[idx], p.y);
|
|
1669
|
+
}
|
|
1670
|
+
const compact = maxY.map((v) => Number.isFinite(v) ? Number(v.toFixed(3)) : null);
|
|
1671
|
+
let dropCount = 0;
|
|
1672
|
+
for (let i = 1; i < binCount; i++) {
|
|
1673
|
+
const prev = maxY[i - 1];
|
|
1674
|
+
const cur = maxY[i];
|
|
1675
|
+
if (!Number.isFinite(prev) || !Number.isFinite(cur)) continue;
|
|
1676
|
+
if (cur < prev - 0.02) dropCount++;
|
|
1677
|
+
}
|
|
1678
|
+
const flatten = groundMaterial?.uniforms?.uZenithFlatten?.value;
|
|
1679
|
+
const blend = currentProjection instanceof BlendedProjection ? currentProjection.getBlend() : -1;
|
|
1680
|
+
console.debug(
|
|
1681
|
+
`[HorizonDiag] fov=${state.fov.toFixed(1)} latDeg=${THREE6.MathUtils.radToDeg(state.lat).toFixed(1)} mode=${activeHorizonProfile.mode} blend=${blend.toFixed(3)} flatten=${Number(flatten ?? 0).toFixed(3)} drops=${dropCount} bins=${JSON.stringify(compact)}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1011
1684
|
function createGround() {
|
|
1012
1685
|
groundGroup.clear();
|
|
1013
1686
|
const radius = 995;
|
|
1014
|
-
const geometry = new
|
|
1687
|
+
const geometry = new THREE6.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
1015
1688
|
const material = createSmartMaterial({
|
|
1016
1689
|
uniforms: {
|
|
1017
|
-
color: { value: new
|
|
1018
|
-
fogColor: { value: new
|
|
1690
|
+
color: { value: new THREE6.Color(65794) },
|
|
1691
|
+
fogColor: { value: new THREE6.Color(663098) },
|
|
1692
|
+
uFogIntensity: { value: 0.6 },
|
|
1693
|
+
uFogVisible: { value: 1 },
|
|
1694
|
+
uMinBrightness: { value: 0 },
|
|
1695
|
+
uHorizonMode: { value: 0 },
|
|
1696
|
+
uHorizonPointCount: { value: 0 },
|
|
1697
|
+
uHorizonAzDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1698
|
+
uHorizonAltDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1699
|
+
uHorizonRotateRad: { value: 0 },
|
|
1700
|
+
uHorizonRadius: { value: radius },
|
|
1701
|
+
uBaseAltDeg: { value: 3 },
|
|
1702
|
+
uZenithFlatten: { value: 0 }
|
|
1019
1703
|
},
|
|
1020
1704
|
vertexShaderBody: `
|
|
1021
1705
|
varying vec3 vPos;
|
|
1022
1706
|
varying vec3 vWorldPos;
|
|
1707
|
+
varying float vViewDirZ;
|
|
1023
1708
|
void main() {
|
|
1024
1709
|
vPos = position;
|
|
1025
1710
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1026
1711
|
gl_Position = smartProject(mvPosition);
|
|
1027
1712
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1028
1713
|
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
1714
|
+
vViewDirZ = normalize(mvPosition.xyz).z;
|
|
1029
1715
|
}
|
|
1030
1716
|
`,
|
|
1031
1717
|
fragmentShader: `
|
|
1032
1718
|
uniform vec3 color;
|
|
1033
1719
|
uniform vec3 fogColor;
|
|
1720
|
+
uniform float uFogIntensity;
|
|
1721
|
+
uniform float uFogVisible;
|
|
1722
|
+
uniform float uMinBrightness;
|
|
1723
|
+
uniform int uHorizonMode;
|
|
1724
|
+
uniform int uHorizonPointCount;
|
|
1725
|
+
uniform float uHorizonAzDeg[64];
|
|
1726
|
+
uniform float uHorizonAltDeg[64];
|
|
1727
|
+
uniform float uHorizonRotateRad;
|
|
1728
|
+
uniform float uHorizonRadius;
|
|
1729
|
+
uniform float uBaseAltDeg;
|
|
1730
|
+
uniform float uZenithFlatten;
|
|
1034
1731
|
varying vec3 vPos;
|
|
1035
1732
|
varying vec3 vWorldPos;
|
|
1733
|
+
varying float vViewDirZ;
|
|
1734
|
+
|
|
1735
|
+
float samplePolygonalAltDeg(float azDeg) {
|
|
1736
|
+
if (uHorizonPointCount < 2) return 0.0;
|
|
1737
|
+
float z = mod(azDeg, 360.0);
|
|
1738
|
+
if (z < 0.0) z += 360.0;
|
|
1739
|
+
|
|
1740
|
+
float prevAz = uHorizonAzDeg[0];
|
|
1741
|
+
float prevAlt = uHorizonAltDeg[0];
|
|
1742
|
+
for (int i = 1; i < 64; i++) {
|
|
1743
|
+
if (i >= uHorizonPointCount) break;
|
|
1744
|
+
float curAz = uHorizonAzDeg[i];
|
|
1745
|
+
float curAlt = uHorizonAltDeg[i];
|
|
1746
|
+
if (z >= prevAz && z <= curAz) {
|
|
1747
|
+
float t = (z - prevAz) / max(0.0001, curAz - prevAz);
|
|
1748
|
+
return mix(prevAlt, curAlt, t);
|
|
1749
|
+
}
|
|
1750
|
+
prevAz = curAz;
|
|
1751
|
+
prevAlt = curAlt;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
float firstAz = uHorizonAzDeg[0] + 360.0;
|
|
1755
|
+
float firstAlt = uHorizonAltDeg[0];
|
|
1756
|
+
float zw = z;
|
|
1757
|
+
if (zw < uHorizonAzDeg[0]) zw += 360.0;
|
|
1758
|
+
float t = (zw - prevAz) / max(0.0001, firstAz - prevAz);
|
|
1759
|
+
return mix(prevAlt, firstAlt, t);
|
|
1760
|
+
}
|
|
1036
1761
|
|
|
1037
1762
|
void main() {
|
|
1038
1763
|
float alphaMask = getMaskAlpha();
|
|
1039
1764
|
if (alphaMask < 0.01) discard;
|
|
1765
|
+
|
|
1766
|
+
// Keep ground visibility aligned with the active projection clip.
|
|
1767
|
+
float clipZ = -0.1;
|
|
1768
|
+
if (uProjectionType == 1) {
|
|
1769
|
+
clipZ = 0.1;
|
|
1770
|
+
} else if (uProjectionType == 2) {
|
|
1771
|
+
clipZ = mix(-0.1, 0.1, clamp(uBlend, 0.0, 1.0));
|
|
1772
|
+
}
|
|
1773
|
+
if (vViewDirZ > clipZ) discard;
|
|
1040
1774
|
|
|
1041
|
-
// Procedural Horizon (Mountains)
|
|
1042
1775
|
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;
|
|
1776
|
+
float terrainHeight;
|
|
1051
1777
|
|
|
1052
|
-
|
|
1778
|
+
if (uHorizonMode == 1 && uHorizonPointCount >= 2) {
|
|
1779
|
+
float azDeg = mod(degrees(angle) + 360.0 + degrees(uHorizonRotateRad), 360.0);
|
|
1780
|
+
float altDeg = samplePolygonalAltDeg(azDeg);
|
|
1781
|
+
terrainHeight = uHorizonRadius * sin(radians(altDeg));
|
|
1782
|
+
} else {
|
|
1783
|
+
// Procedural Horizon (Mountains)
|
|
1784
|
+
float h = 0.0;
|
|
1785
|
+
h += sin(angle * 6.0) * 35.0;
|
|
1786
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1787
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1788
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1789
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1790
|
+
terrainHeight = h + 12.0;
|
|
1791
|
+
}
|
|
1792
|
+
float circularHeight = uHorizonRadius * sin(radians(uBaseAltDeg));
|
|
1793
|
+
terrainHeight = mix(terrainHeight, circularHeight, clamp(uZenithFlatten, 0.0, 1.0));
|
|
1053
1794
|
|
|
1054
1795
|
if (vPos.y > terrainHeight) discard;
|
|
1055
1796
|
|
|
1056
1797
|
// Atmospheric rim glow just below terrain peaks
|
|
1057
1798
|
float rimDist = terrainHeight - vPos.y;
|
|
1058
|
-
float rim = exp(-rimDist * 0.15) * 0.4;
|
|
1799
|
+
float rim = exp(-rimDist * 0.15) * 0.4 * uFogVisible;
|
|
1059
1800
|
vec3 rimColor = fogColor * 1.5;
|
|
1060
1801
|
|
|
1061
1802
|
// Atmospheric haze \u2014 stronger near horizon
|
|
1062
1803
|
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1063
|
-
vec3 finalCol = mix(color, fogColor, fogFactor *
|
|
1804
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * uFogIntensity * uFogVisible);
|
|
1064
1805
|
|
|
1065
1806
|
// Add rim glow near terrain peaks
|
|
1066
1807
|
finalCol += rimColor * rim;
|
|
1808
|
+
finalCol = max(finalCol, color * uMinBrightness);
|
|
1067
1809
|
|
|
1068
1810
|
gl_FragColor = vec4(finalCol, 1.0);
|
|
1069
1811
|
}
|
|
1070
1812
|
`,
|
|
1071
|
-
side:
|
|
1813
|
+
side: THREE6.BackSide,
|
|
1072
1814
|
transparent: false,
|
|
1073
1815
|
depthWrite: true,
|
|
1074
1816
|
depthTest: true
|
|
1075
1817
|
});
|
|
1076
|
-
|
|
1818
|
+
groundMaterial = material;
|
|
1819
|
+
const ground = new THREE6.Mesh(geometry, material);
|
|
1077
1820
|
groundGroup.add(ground);
|
|
1821
|
+
applyGroundTheme(currentConfig);
|
|
1078
1822
|
}
|
|
1823
|
+
let skyBackgroundMesh = null;
|
|
1079
1824
|
let atmosphereMesh = null;
|
|
1825
|
+
let moonMesh = null;
|
|
1826
|
+
let moonGlowMesh = null;
|
|
1827
|
+
let sunDiscMesh = null;
|
|
1828
|
+
let sunHaloMesh = null;
|
|
1829
|
+
let milkyWayMesh = null;
|
|
1830
|
+
let editHoverMesh = null;
|
|
1831
|
+
let editHoverTargetPos = null;
|
|
1832
|
+
let editDropFlash = 0;
|
|
1833
|
+
function createSkyBackground() {
|
|
1834
|
+
const geo = new THREE6.SphereGeometry(2400, 32, 32);
|
|
1835
|
+
const mat = createSmartMaterial({
|
|
1836
|
+
uniforms: {},
|
|
1837
|
+
vertexShaderBody: `
|
|
1838
|
+
varying vec3 vWorldNormal;
|
|
1839
|
+
void main() {
|
|
1840
|
+
vWorldNormal = normalize(position);
|
|
1841
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
1842
|
+
gl_Position = smartProject(mv);
|
|
1843
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1844
|
+
}
|
|
1845
|
+
`,
|
|
1846
|
+
fragmentShader: `
|
|
1847
|
+
varying vec3 vWorldNormal;
|
|
1848
|
+
void main() {
|
|
1849
|
+
float h = clamp(normalize(vWorldNormal).y, -1.0, 1.0);
|
|
1850
|
+
|
|
1851
|
+
// Scotopic-inspired 5-stop gradient.
|
|
1852
|
+
// Night sky: blue channel ~2.6x red, derived from CIE (x=0.25, y=0.25).
|
|
1853
|
+
vec3 cZenith = vec3(0.010, 0.022, 0.055);
|
|
1854
|
+
vec3 cUpper = vec3(0.015, 0.033, 0.080);
|
|
1855
|
+
vec3 cMid = vec3(0.022, 0.048, 0.108);
|
|
1856
|
+
vec3 cLower = vec3(0.035, 0.072, 0.148);
|
|
1857
|
+
vec3 cHorizon = vec3(0.052, 0.100, 0.190);
|
|
1858
|
+
|
|
1859
|
+
float t1 = smoothstep(0.0, 0.30, h);
|
|
1860
|
+
float t2 = smoothstep(0.3, 0.60, h);
|
|
1861
|
+
float t3 = smoothstep(0.6, 0.85, h);
|
|
1862
|
+
float t4 = smoothstep(0.85, 1.00, h);
|
|
1863
|
+
|
|
1864
|
+
vec3 col = cHorizon;
|
|
1865
|
+
col = mix(col, cLower, t1);
|
|
1866
|
+
col = mix(col, cMid, t2);
|
|
1867
|
+
col = mix(col, cUpper, t3);
|
|
1868
|
+
col = mix(col, cZenith, t4);
|
|
1869
|
+
|
|
1870
|
+
// Rayleigh limb brightening at horizon
|
|
1871
|
+
float limb = exp(-18.0 * abs(h)) * smoothstep(-0.05, 0.06, h);
|
|
1872
|
+
col += vec3(0.012, 0.024, 0.050) * limb;
|
|
1873
|
+
|
|
1874
|
+
// Below ground: fade to near-black
|
|
1875
|
+
float below = smoothstep(-0.04, -0.18, h);
|
|
1876
|
+
col = mix(col, vec3(0.002, 0.003, 0.006), below);
|
|
1877
|
+
|
|
1878
|
+
gl_FragColor = vec4(col, 1.0);
|
|
1879
|
+
}
|
|
1880
|
+
`,
|
|
1881
|
+
transparent: false,
|
|
1882
|
+
depthWrite: false,
|
|
1883
|
+
depthTest: false,
|
|
1884
|
+
side: THREE6.BackSide
|
|
1885
|
+
});
|
|
1886
|
+
skyBackgroundMesh = new THREE6.Mesh(geo, mat);
|
|
1887
|
+
skyBackgroundMesh.renderOrder = -2;
|
|
1888
|
+
skyBackgroundMesh.frustumCulled = false;
|
|
1889
|
+
scene.add(skyBackgroundMesh);
|
|
1890
|
+
}
|
|
1080
1891
|
function createAtmosphere() {
|
|
1081
|
-
const geometry = new
|
|
1892
|
+
const geometry = new THREE6.SphereGeometry(990, 64, 64);
|
|
1082
1893
|
const material = createSmartMaterial({
|
|
1894
|
+
uniforms: {
|
|
1895
|
+
uThemeFogVisible: { value: 1 },
|
|
1896
|
+
uThemeFogTopSin: { value: 0.95 },
|
|
1897
|
+
uThemeFogBottomSin: { value: -1 },
|
|
1898
|
+
uThemeFogIntensity: { value: 1 },
|
|
1899
|
+
uThemeMinBrightness: { value: 0 }
|
|
1900
|
+
},
|
|
1083
1901
|
vertexShaderBody: `
|
|
1084
1902
|
varying vec3 vWorldNormal;
|
|
1085
1903
|
void main() {
|
|
@@ -1095,6 +1913,11 @@ function createEngine({
|
|
|
1095
1913
|
uniform float uAtmDark;
|
|
1096
1914
|
uniform vec3 uColorHorizon;
|
|
1097
1915
|
uniform vec3 uColorZenith;
|
|
1916
|
+
uniform float uThemeFogVisible;
|
|
1917
|
+
uniform float uThemeFogTopSin;
|
|
1918
|
+
uniform float uThemeFogBottomSin;
|
|
1919
|
+
uniform float uThemeFogIntensity;
|
|
1920
|
+
uniform float uThemeMinBrightness;
|
|
1098
1921
|
|
|
1099
1922
|
void main() {
|
|
1100
1923
|
float alphaMask = getMaskAlpha();
|
|
@@ -1108,6 +1931,10 @@ function createEngine({
|
|
|
1108
1931
|
|
|
1109
1932
|
// Non-linear mix for realistic sky falloff
|
|
1110
1933
|
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
1934
|
+
float hazeBand = smoothstep(uThemeFogBottomSin, uThemeFogTopSin, h);
|
|
1935
|
+
float hazeFadeEnd = max(uThemeFogTopSin + 0.001, min(1.0, uThemeFogTopSin + 0.25));
|
|
1936
|
+
hazeBand *= (1.0 - smoothstep(uThemeFogTopSin, hazeFadeEnd, h));
|
|
1937
|
+
float fogTheme = uThemeFogVisible * uThemeFogIntensity;
|
|
1111
1938
|
|
|
1112
1939
|
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
1113
1940
|
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
@@ -1115,26 +1942,393 @@ function createEngine({
|
|
|
1115
1942
|
|
|
1116
1943
|
// 3. Primary horizon glow band (wider than before)
|
|
1117
1944
|
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
1118
|
-
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
1945
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
1119
1946
|
|
|
1120
1947
|
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
1121
1948
|
float warmGlow = exp(-8.0 * abs(h));
|
|
1122
|
-
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow;
|
|
1949
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
1950
|
+
skyColor = max(skyColor, uColorZenith * (0.2 * uThemeMinBrightness));
|
|
1123
1951
|
|
|
1124
1952
|
gl_FragColor = vec4(skyColor, 1.0);
|
|
1125
1953
|
}
|
|
1126
1954
|
`,
|
|
1127
|
-
side:
|
|
1955
|
+
side: THREE6.BackSide,
|
|
1128
1956
|
depthWrite: false,
|
|
1129
1957
|
depthTest: true
|
|
1130
1958
|
});
|
|
1131
|
-
const atm = new
|
|
1959
|
+
const atm = new THREE6.Mesh(geometry, material);
|
|
1132
1960
|
atmosphereMesh = atm;
|
|
1133
1961
|
groundGroup.add(atm);
|
|
1134
1962
|
}
|
|
1135
|
-
|
|
1963
|
+
function createMoon() {
|
|
1964
|
+
const moonDir = new THREE6.Vector3(-0.38, 0.62, -0.68).normalize();
|
|
1965
|
+
const moonWorldPos = moonDir.clone().multiplyScalar(2e3);
|
|
1966
|
+
const glowGeo = new THREE6.PlaneGeometry(1, 1);
|
|
1967
|
+
const glowMat = createSmartMaterial({
|
|
1968
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
1969
|
+
vertexShaderBody: `
|
|
1970
|
+
uniform float uMoonSize;
|
|
1971
|
+
varying vec2 vUv;
|
|
1972
|
+
void main() {
|
|
1973
|
+
vUv = uv;
|
|
1974
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1975
|
+
vec4 projected = smartProject(mvPos);
|
|
1976
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
1977
|
+
vec2 offset = position.xy * uMoonSize * uScale * 2.4;
|
|
1978
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1979
|
+
vScreenPos = projected.xy / projected.w;
|
|
1980
|
+
gl_Position = projected;
|
|
1981
|
+
}
|
|
1982
|
+
`,
|
|
1983
|
+
fragmentShader: `
|
|
1984
|
+
varying vec2 vUv;
|
|
1985
|
+
void main() {
|
|
1986
|
+
float alphaMask = getMaskAlpha();
|
|
1987
|
+
if (alphaMask < 0.01) discard;
|
|
1988
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
1989
|
+
float d = length(p);
|
|
1990
|
+
if (d > 1.0) discard;
|
|
1991
|
+
float halo = exp(-5.0 * d * d) * 0.07;
|
|
1992
|
+
halo += exp(-2.5 * max(0.0, d - 0.42)) * 0.045;
|
|
1993
|
+
if (halo < 0.003) discard;
|
|
1994
|
+
gl_FragColor = vec4(vec3(0.78, 0.88, 1.0) * halo, halo * alphaMask);
|
|
1995
|
+
}
|
|
1996
|
+
`,
|
|
1997
|
+
transparent: true,
|
|
1998
|
+
depthWrite: false,
|
|
1999
|
+
depthTest: true,
|
|
2000
|
+
blending: THREE6.AdditiveBlending
|
|
2001
|
+
});
|
|
2002
|
+
moonGlowMesh = new THREE6.Mesh(glowGeo, glowMat);
|
|
2003
|
+
moonGlowMesh.position.copy(moonWorldPos);
|
|
2004
|
+
moonGlowMesh.frustumCulled = false;
|
|
2005
|
+
moonGlowMesh.renderOrder = 2;
|
|
2006
|
+
scene.add(moonGlowMesh);
|
|
2007
|
+
const discGeo = new THREE6.PlaneGeometry(1, 1);
|
|
2008
|
+
const discMat = createSmartMaterial({
|
|
2009
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
2010
|
+
vertexShaderBody: `
|
|
2011
|
+
uniform float uMoonSize;
|
|
2012
|
+
varying vec2 vUv;
|
|
2013
|
+
void main() {
|
|
2014
|
+
vUv = uv;
|
|
2015
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2016
|
+
vec4 projected = smartProject(mvPos);
|
|
2017
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2018
|
+
vec2 offset = position.xy * uMoonSize * uScale;
|
|
2019
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2020
|
+
vScreenPos = projected.xy / projected.w;
|
|
2021
|
+
gl_Position = projected;
|
|
2022
|
+
}
|
|
2023
|
+
`,
|
|
2024
|
+
fragmentShader: `
|
|
2025
|
+
varying vec2 vUv;
|
|
2026
|
+
void main() {
|
|
2027
|
+
float alphaMask = getMaskAlpha();
|
|
2028
|
+
if (alphaMask < 0.01) discard;
|
|
2029
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2030
|
+
float d = length(p);
|
|
2031
|
+
if (d > 1.0) discard;
|
|
2032
|
+
|
|
2033
|
+
float edge = smoothstep(1.0, 0.90, d);
|
|
2034
|
+
|
|
2035
|
+
// Phase: sunlight from upper-right (gibbous moon)
|
|
2036
|
+
vec2 sunDir2D = normalize(vec2(0.55, 0.45));
|
|
2037
|
+
float phaseRaw = dot(normalize(p + vec2(0.0001)), sunDir2D);
|
|
2038
|
+
float lit = smoothstep(-0.18, 0.32, phaseRaw);
|
|
2039
|
+
|
|
2040
|
+
// Limb darkening (classical sqrt law)
|
|
2041
|
+
float cosTheta = sqrt(max(0.001, 1.0 - d * d));
|
|
2042
|
+
float limb = cosTheta * 0.42 + 0.58;
|
|
2043
|
+
|
|
2044
|
+
// Procedural surface texture
|
|
2045
|
+
float angle = atan(p.y, p.x);
|
|
2046
|
+
float r = d;
|
|
2047
|
+
float detail = sin(angle * 5.0 + 2.1) * sin(r * 8.3) * 0.038
|
|
2048
|
+
+ sin(angle * 11.0 - 1.3) * sin(r * 13.0) * 0.022
|
|
2049
|
+
+ sin(angle * 2.0 + 0.8) * (1.0 - r) * 0.055
|
|
2050
|
+
+ sin(angle * 17.0 + r * 6.5) * 0.014
|
|
2051
|
+
+ sin(angle * 23.0 - r * 11.0) * 0.009;
|
|
2052
|
+
|
|
2053
|
+
// Mare (dark maria) patches
|
|
2054
|
+
float mare1 = 1.0 - smoothstep(0.0, 0.30, length(p - vec2(-0.20, 0.22)));
|
|
2055
|
+
float mare2 = 1.0 - smoothstep(0.0, 0.20, length(p - vec2( 0.10, 0.30)));
|
|
2056
|
+
float mare3 = 1.0 - smoothstep(0.0, 0.24, length(p - vec2( 0.17,-0.06)));
|
|
2057
|
+
float mare4 = 1.0 - smoothstep(0.0, 0.14, length(p - vec2(-0.30,-0.20)));
|
|
2058
|
+
float totalMare = clamp(mare1*0.50 + mare2*0.38 + mare3*0.32 + mare4*0.28, 0.0, 0.58);
|
|
2059
|
+
|
|
2060
|
+
vec3 highland = vec3(0.88, 0.85, 0.80);
|
|
2061
|
+
vec3 mareColor = vec3(0.40, 0.39, 0.37);
|
|
2062
|
+
vec3 moonBase = clamp(mix(highland, mareColor, totalMare) + detail, 0.0, 1.0);
|
|
2063
|
+
|
|
2064
|
+
vec3 litSurface = moonBase * limb;
|
|
2065
|
+
vec3 earthshine = vec3(0.038, 0.052, 0.078);
|
|
2066
|
+
vec3 finalColor = mix(earthshine, litSurface, lit);
|
|
2067
|
+
|
|
2068
|
+
gl_FragColor = vec4(finalColor * edge, edge * alphaMask);
|
|
2069
|
+
}
|
|
2070
|
+
`,
|
|
2071
|
+
transparent: true,
|
|
2072
|
+
depthWrite: true,
|
|
2073
|
+
depthTest: true,
|
|
2074
|
+
blending: THREE6.NormalBlending
|
|
2075
|
+
});
|
|
2076
|
+
moonMesh = new THREE6.Mesh(discGeo, discMat);
|
|
2077
|
+
moonMesh.position.copy(moonWorldPos);
|
|
2078
|
+
moonMesh.frustumCulled = false;
|
|
2079
|
+
moonMesh.renderOrder = 3;
|
|
2080
|
+
scene.add(moonMesh);
|
|
2081
|
+
}
|
|
2082
|
+
function createSun() {
|
|
2083
|
+
const sunDir = new THREE6.Vector3(-1, -0.08, 0).normalize();
|
|
2084
|
+
const sunWorldPos = sunDir.clone().multiplyScalar(2e3);
|
|
2085
|
+
const haloGeo = new THREE6.PlaneGeometry(1, 1);
|
|
2086
|
+
const haloMat = createSmartMaterial({
|
|
2087
|
+
uniforms: { uSunHaloSize: { value: 0.46 } },
|
|
2088
|
+
vertexShaderBody: `
|
|
2089
|
+
uniform float uSunHaloSize;
|
|
2090
|
+
varying vec2 vUv;
|
|
2091
|
+
void main() {
|
|
2092
|
+
vUv = uv;
|
|
2093
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2094
|
+
vec4 projected = smartProject(mvPos);
|
|
2095
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2096
|
+
vec2 offset = position.xy * uSunHaloSize * uScale;
|
|
2097
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2098
|
+
vScreenPos = projected.xy / projected.w;
|
|
2099
|
+
gl_Position = projected;
|
|
2100
|
+
}
|
|
2101
|
+
`,
|
|
2102
|
+
fragmentShader: `
|
|
2103
|
+
varying vec2 vUv;
|
|
2104
|
+
void main() {
|
|
2105
|
+
float alphaMask = getMaskAlpha();
|
|
2106
|
+
if (alphaMask < 0.01) discard;
|
|
2107
|
+
|
|
2108
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2109
|
+
float d = length(p);
|
|
2110
|
+
if (d > 1.0) discard;
|
|
2111
|
+
|
|
2112
|
+
// Asymmetric falloff: spread wider horizontally than vertically
|
|
2113
|
+
float asymDist = length(vec2(p.x * 0.55, p.y));
|
|
2114
|
+
|
|
2115
|
+
// Radial glow: warm near centre, fading outward
|
|
2116
|
+
float glow = exp(-2.8 * asymDist * asymDist) * 1.0;
|
|
2117
|
+
glow += exp(-1.0 * asymDist) * 0.35;
|
|
2118
|
+
|
|
2119
|
+
// Crepuscular rays: fan out from bottom, visible above sun centre
|
|
2120
|
+
float rayMask = smoothstep(-0.05, 0.35, p.y);
|
|
2121
|
+
float rayFade = max(0.0, 1.0 - d) * (1.0 - d);
|
|
2122
|
+
float rayAngle = atan(p.x, max(0.0001, p.y)); // angle from vertical
|
|
2123
|
+
float rays = pow(abs(sin(rayAngle * 7.0 + 0.30)), 9.0) * 0.10
|
|
2124
|
+
+ pow(abs(sin(rayAngle * 13.0 - 1.10)), 14.0) * 0.07
|
|
2125
|
+
+ pow(abs(sin(rayAngle * 19.0 + 2.30)), 11.0) * 0.05;
|
|
2126
|
+
rays *= rayMask * rayFade;
|
|
2127
|
+
|
|
2128
|
+
// Colour: white-yellow \u2192 orange \u2192 hot-pink \u2192 purple
|
|
2129
|
+
vec3 cYellow = vec3(1.00, 0.88, 0.52);
|
|
2130
|
+
vec3 cOrange = vec3(1.00, 0.42, 0.10);
|
|
2131
|
+
vec3 cPink = vec3(0.90, 0.22, 0.52);
|
|
2132
|
+
vec3 cPurple = vec3(0.38, 0.12, 0.48);
|
|
2133
|
+
vec3 col = mix(cYellow, cOrange, smoothstep(0.00, 0.40, asymDist));
|
|
2134
|
+
col = mix(col, cPink, smoothstep(0.35, 0.72, asymDist));
|
|
2135
|
+
col = mix(col, cPurple, smoothstep(0.65, 1.00, asymDist));
|
|
2136
|
+
|
|
2137
|
+
float total = (glow + rays) * alphaMask;
|
|
2138
|
+
if (total < 0.005) discard;
|
|
2139
|
+
gl_FragColor = vec4(col * total, total);
|
|
2140
|
+
}
|
|
2141
|
+
`,
|
|
2142
|
+
transparent: true,
|
|
2143
|
+
depthWrite: false,
|
|
2144
|
+
depthTest: true,
|
|
2145
|
+
blending: THREE6.AdditiveBlending
|
|
2146
|
+
});
|
|
2147
|
+
sunHaloMesh = new THREE6.Mesh(haloGeo, haloMat);
|
|
2148
|
+
sunHaloMesh.position.copy(sunWorldPos);
|
|
2149
|
+
sunHaloMesh.frustumCulled = false;
|
|
2150
|
+
sunHaloMesh.renderOrder = 1;
|
|
2151
|
+
scene.add(sunHaloMesh);
|
|
2152
|
+
const discGeo = new THREE6.PlaneGeometry(1, 1);
|
|
2153
|
+
const discMat = createSmartMaterial({
|
|
2154
|
+
uniforms: { uSunSize: { value: 0.09 } },
|
|
2155
|
+
vertexShaderBody: `
|
|
2156
|
+
uniform float uSunSize;
|
|
2157
|
+
varying vec2 vUv;
|
|
2158
|
+
void main() {
|
|
2159
|
+
vUv = uv;
|
|
2160
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2161
|
+
vec4 projected = smartProject(mvPos);
|
|
2162
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2163
|
+
vec2 offset = position.xy * uSunSize * uScale;
|
|
2164
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2165
|
+
vScreenPos = projected.xy / projected.w;
|
|
2166
|
+
gl_Position = projected;
|
|
2167
|
+
}
|
|
2168
|
+
`,
|
|
2169
|
+
fragmentShader: `
|
|
2170
|
+
varying vec2 vUv;
|
|
2171
|
+
void main() {
|
|
2172
|
+
float alphaMask = getMaskAlpha();
|
|
2173
|
+
if (alphaMask < 0.01) discard;
|
|
2174
|
+
|
|
2175
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2176
|
+
float d = length(p);
|
|
2177
|
+
if (d > 1.0) discard;
|
|
2178
|
+
|
|
2179
|
+
float edge = smoothstep(1.0, 0.86, d);
|
|
2180
|
+
|
|
2181
|
+
// Photosphere limb darkening: bright white core \u2192 orange limb
|
|
2182
|
+
float core = smoothstep(0.28, 0.00, d);
|
|
2183
|
+
float mid = smoothstep(0.68, 0.22, d) * (1.0 - core);
|
|
2184
|
+
float limb = (1.0 - smoothstep(0.70, 1.00, d)) * (1.0 - core - mid);
|
|
2185
|
+
|
|
2186
|
+
vec3 cCore = vec3(1.00, 0.97, 0.88); // hot white
|
|
2187
|
+
vec3 cMid = vec3(1.00, 0.80, 0.38); // yellow
|
|
2188
|
+
vec3 cLimb = vec3(1.00, 0.52, 0.08); // deep orange
|
|
2189
|
+
|
|
2190
|
+
vec3 col = cCore * (core + 0.12) + cMid * mid + cLimb * limb;
|
|
2191
|
+
col = clamp(col, 0.0, 1.5); // allow slight overbright
|
|
2192
|
+
|
|
2193
|
+
gl_FragColor = vec4(col * edge, edge * alphaMask);
|
|
2194
|
+
}
|
|
2195
|
+
`,
|
|
2196
|
+
transparent: true,
|
|
2197
|
+
depthWrite: true,
|
|
2198
|
+
depthTest: true,
|
|
2199
|
+
blending: THREE6.NormalBlending
|
|
2200
|
+
});
|
|
2201
|
+
sunDiscMesh = new THREE6.Mesh(discGeo, discMat);
|
|
2202
|
+
sunDiscMesh.position.copy(sunWorldPos);
|
|
2203
|
+
sunDiscMesh.frustumCulled = false;
|
|
2204
|
+
sunDiscMesh.renderOrder = 3;
|
|
2205
|
+
scene.add(sunDiscMesh);
|
|
2206
|
+
}
|
|
2207
|
+
function createMilkyWay() {
|
|
2208
|
+
if (milkyWayMesh) {
|
|
2209
|
+
scene.remove(milkyWayMesh);
|
|
2210
|
+
milkyWayMesh.geometry.dispose();
|
|
2211
|
+
milkyWayMesh.material.dispose();
|
|
2212
|
+
milkyWayMesh = null;
|
|
2213
|
+
}
|
|
2214
|
+
const geo = new THREE6.PlaneGeometry(1100, 380, 4, 4);
|
|
2215
|
+
const mat = createSmartMaterial({
|
|
2216
|
+
uniforms: {},
|
|
2217
|
+
vertexShaderBody: `
|
|
2218
|
+
varying vec2 vUv;
|
|
2219
|
+
void main() {
|
|
2220
|
+
vUv = uv;
|
|
2221
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
2222
|
+
gl_Position = smartProject(mv);
|
|
2223
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
2224
|
+
}
|
|
2225
|
+
`,
|
|
2226
|
+
fragmentShader: `
|
|
2227
|
+
varying vec2 vUv;
|
|
2228
|
+
|
|
2229
|
+
// --- Noise helpers ---
|
|
2230
|
+
float hash(vec2 p) {
|
|
2231
|
+
p = fract(p * vec2(127.1, 311.7));
|
|
2232
|
+
p += dot(p, p + 19.19);
|
|
2233
|
+
return fract(p.x * p.y);
|
|
2234
|
+
}
|
|
2235
|
+
float vnoise(vec2 p) {
|
|
2236
|
+
vec2 i = floor(p); vec2 f = fract(p);
|
|
2237
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
2238
|
+
return mix(
|
|
2239
|
+
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
|
|
2240
|
+
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), f.x), f.y
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
float fbm(vec2 p) {
|
|
2244
|
+
float v = 0.0; float a = 0.5;
|
|
2245
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
2246
|
+
for (int i = 0; i < 7; i++) { v += a * vnoise(p); p = m * p; a *= 0.5; }
|
|
2247
|
+
return v;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
void main() {
|
|
2251
|
+
float alphaMask = getMaskAlpha();
|
|
2252
|
+
if (alphaMask < 0.01) discard;
|
|
2253
|
+
|
|
2254
|
+
vec2 uv = vUv * 2.0 - 1.0; // -1..1 centred
|
|
2255
|
+
|
|
2256
|
+
// Galactic band: tight Gaussian falloff vertically
|
|
2257
|
+
float bandMask = exp(-uv.y * uv.y * 10.0);
|
|
2258
|
+
|
|
2259
|
+
// Warp UV for organic turbulence (two layers of distortion)
|
|
2260
|
+
vec2 q = vec2(fbm(uv * 1.5),
|
|
2261
|
+
fbm(uv * 1.5 + vec2(5.2, 1.3)));
|
|
2262
|
+
vec2 r = vec2(fbm(uv * 1.0 + 4.0 * q + vec2(1.7, 9.2)),
|
|
2263
|
+
fbm(uv * 1.0 + 4.0 * q + vec2(8.3, 2.8)));
|
|
2264
|
+
|
|
2265
|
+
float nebula = fbm(uv * 2.0 + 2.0 * r);
|
|
2266
|
+
float detail = fbm(uv * 5.0 + r * 3.0 + vec2(3.1, 2.7));
|
|
2267
|
+
float fine = fbm(uv * 10.0 + vec2(1.0, 5.0));
|
|
2268
|
+
|
|
2269
|
+
// Base density
|
|
2270
|
+
float density = smoothstep(0.30, 0.80, nebula) * bandMask;
|
|
2271
|
+
density += smoothstep(0.45, 0.85, detail) * bandMask * 0.35;
|
|
2272
|
+
|
|
2273
|
+
// Dust lanes \u2014 dark patches carved into the band
|
|
2274
|
+
float dust = fbm(uv * 3.5 + vec2(11.0, 7.0));
|
|
2275
|
+
density *= (1.0 - smoothstep(0.52, 0.62, dust) * 0.7 * bandMask);
|
|
2276
|
+
|
|
2277
|
+
// Galactic core boost toward horizontal centre
|
|
2278
|
+
float galCore = exp(-uv.x * uv.x * 1.2) * bandMask;
|
|
2279
|
+
|
|
2280
|
+
// --- Color palette ---
|
|
2281
|
+
vec3 deepBlue = vec3(0.10, 0.15, 0.45);
|
|
2282
|
+
vec3 midBlue = vec3(0.25, 0.30, 0.65);
|
|
2283
|
+
vec3 purple = vec3(0.40, 0.20, 0.60);
|
|
2284
|
+
vec3 coreWarm = vec3(0.85, 0.80, 0.65); // warm star-cluster glow
|
|
2285
|
+
vec3 pinkNeb = vec3(0.65, 0.28, 0.50); // emission nebula pink
|
|
2286
|
+
|
|
2287
|
+
float t1 = smoothstep(0.3, 0.7, nebula);
|
|
2288
|
+
float t2 = smoothstep(0.5, 0.8, detail);
|
|
2289
|
+
float t3 = smoothstep(0.55, 0.75, fine);
|
|
2290
|
+
|
|
2291
|
+
vec3 color = mix(deepBlue, midBlue, t1);
|
|
2292
|
+
color = mix(color, purple, t2 * 0.5);
|
|
2293
|
+
color = mix(color, pinkNeb, t3 * 0.25 * bandMask);
|
|
2294
|
+
color += coreWarm * galCore * 0.45 * density;
|
|
2295
|
+
|
|
2296
|
+
// Micro-star field \u2014 denser in the band
|
|
2297
|
+
float starThresh = mix(0.975, 0.940, bandMask);
|
|
2298
|
+
float starSeed = hash(floor(vUv * 500.0));
|
|
2299
|
+
float star = step(starThresh, starSeed);
|
|
2300
|
+
float starBright = hash(floor(vUv * 500.0) + 37.0);
|
|
2301
|
+
color += vec3(0.90, 0.95, 1.0) * star * (0.4 + 0.6 * starBright);
|
|
2302
|
+
density = max(density, star * bandMask * 0.5);
|
|
2303
|
+
|
|
2304
|
+
// Soft edge vignette
|
|
2305
|
+
float ex = smoothstep(0.0, 0.12, vUv.x) * smoothstep(1.0, 0.88, vUv.x);
|
|
2306
|
+
float ey = smoothstep(0.0, 0.18, vUv.y) * smoothstep(1.0, 0.82, vUv.y);
|
|
2307
|
+
|
|
2308
|
+
float alpha = density * ex * ey * alphaMask * 0.80;
|
|
2309
|
+
if (alpha < 0.004) discard;
|
|
2310
|
+
gl_FragColor = vec4(color, alpha);
|
|
2311
|
+
}
|
|
2312
|
+
`,
|
|
2313
|
+
transparent: true,
|
|
2314
|
+
depthWrite: false,
|
|
2315
|
+
depthTest: true,
|
|
2316
|
+
side: THREE6.DoubleSide,
|
|
2317
|
+
blending: THREE6.AdditiveBlending
|
|
2318
|
+
});
|
|
2319
|
+
milkyWayMesh = new THREE6.Mesh(geo, mat);
|
|
2320
|
+
const mwDir = new THREE6.Vector3(-0.62, 0.6, -0.5).normalize();
|
|
2321
|
+
milkyWayMesh.position.copy(mwDir.clone().multiplyScalar(920));
|
|
2322
|
+
milkyWayMesh.lookAt(0, 0, 0);
|
|
2323
|
+
milkyWayMesh.rotateY(Math.PI);
|
|
2324
|
+
milkyWayMesh.frustumCulled = false;
|
|
2325
|
+
milkyWayMesh.renderOrder = 1;
|
|
2326
|
+
scene.add(milkyWayMesh);
|
|
2327
|
+
}
|
|
2328
|
+
const backdropGroup = new THREE6.Group();
|
|
1136
2329
|
scene.add(backdropGroup);
|
|
1137
|
-
|
|
2330
|
+
let backdropStarsMaterial = null;
|
|
2331
|
+
function createBackdropStars(count = 5e3) {
|
|
1138
2332
|
backdropGroup.clear();
|
|
1139
2333
|
while (backdropGroup.children.length > 0) {
|
|
1140
2334
|
const c = backdropGroup.children[0];
|
|
@@ -1142,7 +2336,7 @@ function createEngine({
|
|
|
1142
2336
|
if (c.geometry) c.geometry.dispose();
|
|
1143
2337
|
if (c.material) c.material.dispose();
|
|
1144
2338
|
}
|
|
1145
|
-
const geometry = new
|
|
2339
|
+
const geometry = new THREE6.BufferGeometry();
|
|
1146
2340
|
const positions = [];
|
|
1147
2341
|
const sizes = [];
|
|
1148
2342
|
const colors = [];
|
|
@@ -1177,14 +2371,17 @@ function createEngine({
|
|
|
1177
2371
|
}
|
|
1178
2372
|
colors.push(cr, cg, cb);
|
|
1179
2373
|
}
|
|
1180
|
-
geometry.setAttribute("position", new
|
|
1181
|
-
geometry.setAttribute("size", new
|
|
1182
|
-
geometry.setAttribute("color", new
|
|
2374
|
+
geometry.setAttribute("position", new THREE6.Float32BufferAttribute(positions, 3));
|
|
2375
|
+
geometry.setAttribute("size", new THREE6.Float32BufferAttribute(sizes, 1));
|
|
2376
|
+
geometry.setAttribute("color", new THREE6.Float32BufferAttribute(colors, 3));
|
|
1183
2377
|
const material = createSmartMaterial({
|
|
1184
2378
|
uniforms: {
|
|
1185
2379
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1186
2380
|
uScale: globalUniforms.uScale,
|
|
1187
|
-
uTime: globalUniforms.uTime
|
|
2381
|
+
uTime: globalUniforms.uTime,
|
|
2382
|
+
uBackdropGain: { value: 1 },
|
|
2383
|
+
uBackdropEnergy: { value: 2.2 },
|
|
2384
|
+
uBackdropSizeExp: { value: 0.9 }
|
|
1188
2385
|
},
|
|
1189
2386
|
vertexShaderBody: `
|
|
1190
2387
|
attribute float size;
|
|
@@ -1195,6 +2392,9 @@ function createEngine({
|
|
|
1195
2392
|
uniform float uAtmExtinction;
|
|
1196
2393
|
uniform float uAtmTwinkle;
|
|
1197
2394
|
uniform float uTime;
|
|
2395
|
+
uniform float uBackdropGain;
|
|
2396
|
+
uniform float uBackdropEnergy;
|
|
2397
|
+
uniform float uBackdropSizeExp;
|
|
1198
2398
|
|
|
1199
2399
|
void main() {
|
|
1200
2400
|
vec3 nPos = normalize(position);
|
|
@@ -1210,15 +2410,16 @@ function createEngine({
|
|
|
1210
2410
|
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1211
2411
|
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
1212
2412
|
|
|
1213
|
-
vColor = color *
|
|
2413
|
+
vColor = color * uBackdropEnergy * extinction * horizonFade * scintillation * uBackdropGain;
|
|
1214
2414
|
|
|
1215
2415
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1216
2416
|
gl_Position = smartProject(mvPosition);
|
|
1217
2417
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1218
2418
|
|
|
1219
|
-
float zoomScale = pow(uScale, 0.
|
|
2419
|
+
float zoomScale = pow(max(uScale, 0.0001), uBackdropSizeExp);
|
|
1220
2420
|
float perceptualSize = pow(size, 0.55);
|
|
1221
|
-
|
|
2421
|
+
float sizeGain = mix(0.78, 1.0, uBackdropGain);
|
|
2422
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * sizeGain * 0.5 * pixelRatio * (800.0 / length(mvPosition.xyz)) * horizonFade, 0.5, 20.0);
|
|
1222
2423
|
}
|
|
1223
2424
|
`,
|
|
1224
2425
|
fragmentShader: `
|
|
@@ -1242,27 +2443,84 @@ function createEngine({
|
|
|
1242
2443
|
transparent: true,
|
|
1243
2444
|
depthWrite: false,
|
|
1244
2445
|
depthTest: true,
|
|
1245
|
-
blending:
|
|
2446
|
+
blending: THREE6.AdditiveBlending
|
|
1246
2447
|
});
|
|
1247
|
-
|
|
2448
|
+
backdropStarsMaterial = material;
|
|
2449
|
+
const points = new THREE6.Points(geometry, material);
|
|
1248
2450
|
points.frustumCulled = false;
|
|
1249
2451
|
backdropGroup.add(points);
|
|
1250
2452
|
}
|
|
2453
|
+
function createEditHoverRing() {
|
|
2454
|
+
const geo = new THREE6.PlaneGeometry(1, 1);
|
|
2455
|
+
const mat = createSmartMaterial({
|
|
2456
|
+
uniforms: {
|
|
2457
|
+
uRingSize: { value: 0.06 },
|
|
2458
|
+
uRingAlpha: { value: 0 },
|
|
2459
|
+
uRingColor: { value: new THREE6.Color(0.55, 0.88, 1) }
|
|
2460
|
+
},
|
|
2461
|
+
vertexShaderBody: `
|
|
2462
|
+
uniform float uRingSize;
|
|
2463
|
+
varying vec2 vUv;
|
|
2464
|
+
void main() {
|
|
2465
|
+
vUv = uv;
|
|
2466
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2467
|
+
vec4 proj = smartProject(mvPos);
|
|
2468
|
+
if (proj.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2469
|
+
vec2 offset = position.xy * uRingSize * uScale;
|
|
2470
|
+
proj.xy += offset / vec2(uAspect, 1.0);
|
|
2471
|
+
vScreenPos = proj.xy / proj.w;
|
|
2472
|
+
gl_Position = proj;
|
|
2473
|
+
}
|
|
2474
|
+
`,
|
|
2475
|
+
fragmentShader: `
|
|
2476
|
+
varying vec2 vUv;
|
|
2477
|
+
uniform float uRingAlpha;
|
|
2478
|
+
uniform vec3 uRingColor;
|
|
2479
|
+
void main() {
|
|
2480
|
+
float alphaMask = getMaskAlpha();
|
|
2481
|
+
if (alphaMask < 0.01) discard;
|
|
2482
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2483
|
+
float d = length(p);
|
|
2484
|
+
float ring = smoothstep(0.52, 0.62, d) * (1.0 - smoothstep(0.80, 0.92, d));
|
|
2485
|
+
float glow = (1.0 - smoothstep(0.55, 0.98, d)) * 0.18;
|
|
2486
|
+
float a = (ring + glow) * uRingAlpha * alphaMask;
|
|
2487
|
+
if (a < 0.005) discard;
|
|
2488
|
+
gl_FragColor = vec4(uRingColor * (ring * 1.2 + glow), a);
|
|
2489
|
+
}
|
|
2490
|
+
`,
|
|
2491
|
+
transparent: true,
|
|
2492
|
+
depthWrite: false,
|
|
2493
|
+
depthTest: false,
|
|
2494
|
+
side: THREE6.DoubleSide,
|
|
2495
|
+
blending: THREE6.AdditiveBlending
|
|
2496
|
+
});
|
|
2497
|
+
editHoverMesh = new THREE6.Mesh(geo, mat);
|
|
2498
|
+
editHoverMesh.renderOrder = 500;
|
|
2499
|
+
editHoverMesh.frustumCulled = false;
|
|
2500
|
+
scene.add(editHoverMesh);
|
|
2501
|
+
}
|
|
2502
|
+
createSkyBackground();
|
|
1251
2503
|
createGround();
|
|
1252
2504
|
createAtmosphere();
|
|
2505
|
+
createMoon();
|
|
2506
|
+
createSun();
|
|
2507
|
+
createMilkyWay();
|
|
1253
2508
|
createBackdropStars();
|
|
1254
|
-
|
|
2509
|
+
createEditHoverRing();
|
|
2510
|
+
const raycaster = new THREE6.Raycaster();
|
|
1255
2511
|
raycaster.params.Points.threshold = 5;
|
|
1256
|
-
new
|
|
1257
|
-
const root = new
|
|
2512
|
+
new THREE6.Vector2();
|
|
2513
|
+
const root = new THREE6.Group();
|
|
1258
2514
|
scene.add(root);
|
|
1259
2515
|
const nodeById = /* @__PURE__ */ new Map();
|
|
1260
2516
|
const starIndexToId = [];
|
|
2517
|
+
const starIdToIndex = /* @__PURE__ */ new Map();
|
|
1261
2518
|
const dynamicLabels = [];
|
|
2519
|
+
const labelManager = new LabelManager();
|
|
1262
2520
|
const hoverLabelMat = createSmartMaterial({
|
|
1263
2521
|
uniforms: {
|
|
1264
2522
|
uMap: { value: null },
|
|
1265
|
-
uSize: { value: new
|
|
2523
|
+
uSize: { value: new THREE6.Vector2(1, 1) },
|
|
1266
2524
|
uAlpha: { value: 0 },
|
|
1267
2525
|
uAngle: { value: 0 }
|
|
1268
2526
|
},
|
|
@@ -1300,7 +2558,7 @@ function createEngine({
|
|
|
1300
2558
|
depthTest: false
|
|
1301
2559
|
// Always on top of stars
|
|
1302
2560
|
});
|
|
1303
|
-
const hoverLabelMesh = new
|
|
2561
|
+
const hoverLabelMesh = new THREE6.Mesh(new THREE6.PlaneGeometry(1, 1), hoverLabelMat);
|
|
1304
2562
|
hoverLabelMesh.visible = false;
|
|
1305
2563
|
hoverLabelMesh.renderOrder = 999;
|
|
1306
2564
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -1324,7 +2582,9 @@ function createEngine({
|
|
|
1324
2582
|
}
|
|
1325
2583
|
nodeById.clear();
|
|
1326
2584
|
starIndexToId.length = 0;
|
|
2585
|
+
starIdToIndex.clear();
|
|
1327
2586
|
dynamicLabels.length = 0;
|
|
2587
|
+
labelManager.clear();
|
|
1328
2588
|
constellationLines = null;
|
|
1329
2589
|
boundaryLines = null;
|
|
1330
2590
|
starPoints = null;
|
|
@@ -1346,49 +2606,132 @@ function createEngine({
|
|
|
1346
2606
|
ctx.textAlign = "center";
|
|
1347
2607
|
ctx.textBaseline = "middle";
|
|
1348
2608
|
ctx.fillText(text, w / 2, h / 2);
|
|
1349
|
-
const tex = new
|
|
1350
|
-
tex.minFilter =
|
|
2609
|
+
const tex = new THREE6.CanvasTexture(canvas);
|
|
2610
|
+
tex.minFilter = THREE6.LinearFilter;
|
|
1351
2611
|
return { tex, aspect: w / h };
|
|
1352
2612
|
}
|
|
1353
2613
|
function getPosition(n) {
|
|
1354
2614
|
if (currentConfig?.arrangement) {
|
|
1355
2615
|
const arr = currentConfig.arrangement[n.id];
|
|
1356
2616
|
if (arr) {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
const y = arr.position[1];
|
|
2617
|
+
const [px, py, pz] = arr.position;
|
|
2618
|
+
if (pz === 0) {
|
|
1360
2619
|
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
2620
|
+
const len3d = Math.sqrt(px * px + py * py);
|
|
2621
|
+
if (len3d < radius * 0.99) {
|
|
2622
|
+
const r_norm = Math.min(1, len3d / radius);
|
|
2623
|
+
const phi = Math.atan2(py, px);
|
|
2624
|
+
const theta = r_norm * (Math.PI / 2);
|
|
2625
|
+
return new THREE6.Vector3(
|
|
2626
|
+
Math.sin(theta) * Math.cos(phi),
|
|
2627
|
+
Math.cos(theta),
|
|
2628
|
+
Math.sin(theta) * Math.sin(phi)
|
|
2629
|
+
).multiplyScalar(radius);
|
|
2630
|
+
}
|
|
1369
2631
|
}
|
|
1370
|
-
return new
|
|
2632
|
+
return new THREE6.Vector3(px, py, pz);
|
|
1371
2633
|
}
|
|
1372
2634
|
}
|
|
1373
|
-
return new
|
|
2635
|
+
return new THREE6.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
1374
2636
|
}
|
|
1375
2637
|
function getBoundaryPoint(angle, t, radius) {
|
|
1376
2638
|
const y = 0.05 + t * (1 - 0.05);
|
|
1377
2639
|
const rY = Math.sqrt(1 - y * y);
|
|
1378
2640
|
const x = Math.cos(angle) * rY;
|
|
1379
2641
|
const z = Math.sin(angle) * rY;
|
|
1380
|
-
return new
|
|
2642
|
+
return new THREE6.Vector3(x, y, z).multiplyScalar(radius);
|
|
2643
|
+
}
|
|
2644
|
+
function updateChapterLabelAnchors() {
|
|
2645
|
+
if (!starPoints) return;
|
|
2646
|
+
const attr = starPoints.geometry.attributes.position;
|
|
2647
|
+
if (!attr) return;
|
|
2648
|
+
const cameraUpWorld = new THREE6.Vector3(0, 1, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2649
|
+
const cameraRightWorld = new THREE6.Vector3(1, 0, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2650
|
+
for (const item of dynamicLabels) {
|
|
2651
|
+
if (item.node.level !== 3) continue;
|
|
2652
|
+
const idx = starIdToIndex.get(item.node.id);
|
|
2653
|
+
if (idx === void 0) continue;
|
|
2654
|
+
const starPos = new THREE6.Vector3(attr.getX(idx), attr.getY(idx), attr.getZ(idx));
|
|
2655
|
+
const normal = starPos.clone().normalize();
|
|
2656
|
+
const tangent = cameraUpWorld.clone().sub(normal.clone().multiplyScalar(cameraUpWorld.dot(normal)));
|
|
2657
|
+
if (tangent.lengthSq() < 1e-6) {
|
|
2658
|
+
tangent.copy(cameraRightWorld).sub(normal.clone().multiplyScalar(cameraRightWorld.dot(normal)));
|
|
2659
|
+
}
|
|
2660
|
+
if (tangent.lengthSq() < 1e-6) continue;
|
|
2661
|
+
tangent.normalize();
|
|
2662
|
+
const starNorm = item.chapterStarSizeNorm ?? 0.5;
|
|
2663
|
+
const baseSize = item.chapterStarBaseSize ?? 3.5;
|
|
2664
|
+
const altitude = normal.y;
|
|
2665
|
+
const horizonFade = THREE6.MathUtils.smoothstep(altitude, -0.1, 0.05);
|
|
2666
|
+
const mvPos = starPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
2667
|
+
const dist = Math.max(1, mvPos.length());
|
|
2668
|
+
const perceptualSize = Math.pow(baseSize, 0.7);
|
|
2669
|
+
const sizeBoost = 1 + Math.pow(baseSize, 0.5) * 0.08;
|
|
2670
|
+
const pointSize = THREE6.MathUtils.clamp(
|
|
2671
|
+
perceptualSize * sizeBoost * 20 * globalUniforms.uScale.value * renderer.getPixelRatio() * (2e3 / dist) * horizonFade,
|
|
2672
|
+
1,
|
|
2673
|
+
600
|
|
2674
|
+
);
|
|
2675
|
+
item.chapterGlowRadiusPx = pointSize * 0.6;
|
|
2676
|
+
const viewportH = Math.max(1, renderer.domElement.clientHeight);
|
|
2677
|
+
const fovRad = state.fov * Math.PI / 180;
|
|
2678
|
+
const worldPerPixel = 2 * dist * Math.tan(fovRad * 0.5) / viewportH;
|
|
2679
|
+
let labelHalfDiagPx = 18;
|
|
2680
|
+
const mat = item.obj.material;
|
|
2681
|
+
if (mat instanceof THREE6.ShaderMaterial && mat.uniforms?.uSize?.value instanceof THREE6.Vector2) {
|
|
2682
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2683
|
+
const revealT = THREE6.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2684
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2685
|
+
const fadeOutScale = 1 + (1 - revealT) * 0.06;
|
|
2686
|
+
const zoomTextBoost = THREE6.MathUtils.lerp(1.4, 0.55, THREE6.MathUtils.smoothstep(state.fov, 8, 46));
|
|
2687
|
+
const starTextBoost = THREE6.MathUtils.lerp(0.9, 1.35, starNorm);
|
|
2688
|
+
const scaleMul = zoomTextBoost * starTextBoost * revealScale * fadeOutScale;
|
|
2689
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2690
|
+
const targetX = item.initialScale.x * scaleMul;
|
|
2691
|
+
const targetY = item.initialScale.y * scaleMul;
|
|
2692
|
+
uSize.x = THREE6.MathUtils.lerp(uSize.x, targetX, 0.2);
|
|
2693
|
+
uSize.y = THREE6.MathUtils.lerp(uSize.y, targetY, 0.2);
|
|
2694
|
+
const size = mat.uniforms.uSize.value;
|
|
2695
|
+
const pixelH = size.y * viewportH * 0.8;
|
|
2696
|
+
const pixelW = size.x * viewportH * 0.8;
|
|
2697
|
+
labelHalfDiagPx = Math.max(6, Math.max(pixelH, pixelW * 0.45) * 0.5);
|
|
2698
|
+
}
|
|
2699
|
+
const edgeMarginPx = THREE6.MathUtils.lerp(1, 3, starNorm);
|
|
2700
|
+
const requiredPx = item.chapterGlowRadiusPx + edgeMarginPx + labelHalfDiagPx;
|
|
2701
|
+
const zoomPush = 1 + (1 - THREE6.MathUtils.smoothstep(state.fov, 8, 30)) * 0.8;
|
|
2702
|
+
const starPush = THREE6.MathUtils.lerp(0.95, 1.2, starNorm);
|
|
2703
|
+
const offset = THREE6.MathUtils.clamp(requiredPx * worldPerPixel * zoomPush * starPush, 3, 76);
|
|
2704
|
+
item.obj.position.copy(starPos);
|
|
2705
|
+
item.obj.position.addScaledVector(tangent, offset);
|
|
2706
|
+
item.obj.position.addScaledVector(normal, 2.5);
|
|
2707
|
+
item.chapterStarWorldPos = starPos.clone();
|
|
2708
|
+
}
|
|
2709
|
+
for (const item of dynamicLabels) {
|
|
2710
|
+
const level = item.node.level;
|
|
2711
|
+
if (level !== 2 && level !== 2.5) continue;
|
|
2712
|
+
const mat = item.obj.material;
|
|
2713
|
+
if (!(mat instanceof THREE6.ShaderMaterial) || !(mat.uniforms?.uSize?.value instanceof THREE6.Vector2)) continue;
|
|
2714
|
+
const entryFov = 22;
|
|
2715
|
+
const zoomBoost = THREE6.MathUtils.lerp(1.3, 0.5, THREE6.MathUtils.smoothstep(state.fov, 8, entryFov));
|
|
2716
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2717
|
+
const revealT = THREE6.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2718
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2719
|
+
const scaleMul = zoomBoost * revealScale;
|
|
2720
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2721
|
+
uSize.x = THREE6.MathUtils.lerp(uSize.x, item.initialScale.x * scaleMul, 0.2);
|
|
2722
|
+
uSize.y = THREE6.MathUtils.lerp(uSize.y, item.initialScale.y * scaleMul, 0.2);
|
|
2723
|
+
}
|
|
1381
2724
|
}
|
|
1382
2725
|
function buildFromModel(model, cfg) {
|
|
1383
2726
|
clearRoot();
|
|
1384
2727
|
bookIdToIndex.clear();
|
|
1385
2728
|
testamentToIndex.clear();
|
|
1386
2729
|
divisionToIndex.clear();
|
|
1387
|
-
scene.background = cfg.background && cfg.background !== "transparent" ? new
|
|
2730
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE6.Color(cfg.background) : null;
|
|
1388
2731
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
1389
2732
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
1390
2733
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
1391
|
-
|
|
2734
|
+
{
|
|
1392
2735
|
const divMap = /* @__PURE__ */ new Map();
|
|
1393
2736
|
for (const n of laidOut.nodes) {
|
|
1394
2737
|
if (n.level === 2 && n.parent) {
|
|
@@ -1398,7 +2741,7 @@ function createEngine({
|
|
|
1398
2741
|
}
|
|
1399
2742
|
}
|
|
1400
2743
|
for (const [divId, books] of divMap.entries()) {
|
|
1401
|
-
const centroid = new
|
|
2744
|
+
const centroid = new THREE6.Vector3();
|
|
1402
2745
|
let count = 0;
|
|
1403
2746
|
for (const b of books) {
|
|
1404
2747
|
const p = getPosition(b);
|
|
@@ -1419,20 +2762,25 @@ function createEngine({
|
|
|
1419
2762
|
const starChapterIndices = [];
|
|
1420
2763
|
const starTestamentIndices = [];
|
|
1421
2764
|
const starDivisionIndices = [];
|
|
2765
|
+
const chapterLineCutById = /* @__PURE__ */ new Map();
|
|
2766
|
+
const chapterStarSizeById = /* @__PURE__ */ new Map();
|
|
2767
|
+
const chapterWeightNormById = /* @__PURE__ */ new Map();
|
|
2768
|
+
let minChapterStarSize = Infinity;
|
|
2769
|
+
let maxChapterStarSize = -Infinity;
|
|
1422
2770
|
const SPECTRAL_COLORS = [
|
|
1423
|
-
new
|
|
2771
|
+
new THREE6.Color(14544639),
|
|
1424
2772
|
// O - Blueish White
|
|
1425
|
-
new
|
|
2773
|
+
new THREE6.Color(15660287),
|
|
1426
2774
|
// B - White
|
|
1427
|
-
new
|
|
2775
|
+
new THREE6.Color(16317695),
|
|
1428
2776
|
// A - White
|
|
1429
|
-
new
|
|
2777
|
+
new THREE6.Color(16777208),
|
|
1430
2778
|
// F - White
|
|
1431
|
-
new
|
|
2779
|
+
new THREE6.Color(16775406),
|
|
1432
2780
|
// G - Yellowish White
|
|
1433
|
-
new
|
|
2781
|
+
new THREE6.Color(16773085),
|
|
1434
2782
|
// K - Pale Orange
|
|
1435
|
-
new
|
|
2783
|
+
new THREE6.Color(16771788)
|
|
1436
2784
|
// M - Light Orange
|
|
1437
2785
|
];
|
|
1438
2786
|
let minWeight = Infinity;
|
|
@@ -1452,15 +2800,38 @@ function createEngine({
|
|
|
1452
2800
|
}
|
|
1453
2801
|
for (const n of laidOut.nodes) {
|
|
1454
2802
|
if (n.level === 3) {
|
|
1455
|
-
const p = getPosition(n);
|
|
1456
|
-
starPositions.push(p.x, p.y, p.z);
|
|
1457
|
-
starIndexToId.push(n.id);
|
|
1458
2803
|
let baseSize = 3.5;
|
|
2804
|
+
let weightNorm = 0;
|
|
1459
2805
|
if (typeof n.weight === "number") {
|
|
1460
|
-
|
|
1461
|
-
|
|
2806
|
+
weightNorm = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
2807
|
+
const sizeExp = cfg.starSizeExponent ?? 4;
|
|
2808
|
+
const sizeScale = cfg.starSizeScale ?? 6;
|
|
2809
|
+
baseSize = Math.pow(weightNorm, sizeExp) * 22 * sizeScale;
|
|
1462
2810
|
}
|
|
2811
|
+
chapterStarSizeById.set(n.id, baseSize);
|
|
2812
|
+
chapterWeightNormById.set(n.id, weightNorm);
|
|
2813
|
+
minChapterStarSize = Math.min(minChapterStarSize, baseSize);
|
|
2814
|
+
maxChapterStarSize = Math.max(maxChapterStarSize, baseSize);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
if (!Number.isFinite(minChapterStarSize)) {
|
|
2818
|
+
minChapterStarSize = 1;
|
|
2819
|
+
maxChapterStarSize = 2;
|
|
2820
|
+
} else if (minChapterStarSize === maxChapterStarSize) {
|
|
2821
|
+
maxChapterStarSize = minChapterStarSize + 1;
|
|
2822
|
+
}
|
|
2823
|
+
for (const n of laidOut.nodes) {
|
|
2824
|
+
if (n.level === 3) {
|
|
2825
|
+
const p = getPosition(n);
|
|
2826
|
+
starPositions.push(p.x, p.y, p.z);
|
|
2827
|
+
starIdToIndex.set(n.id, starIndexToId.length);
|
|
2828
|
+
starIndexToId.push(n.id);
|
|
2829
|
+
const baseSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
1463
2830
|
starSizes.push(baseSize);
|
|
2831
|
+
chapterLineCutById.set(
|
|
2832
|
+
n.id,
|
|
2833
|
+
THREE6.MathUtils.clamp(2.5 + baseSize * 0.45, 3, 40)
|
|
2834
|
+
);
|
|
1464
2835
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
1465
2836
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
1466
2837
|
starColors.push(c.r, c.g, c.b);
|
|
@@ -1511,8 +2882,11 @@ function createEngine({
|
|
|
1511
2882
|
let baseScale = 0.05;
|
|
1512
2883
|
if (n.level === 1) baseScale = 0.08;
|
|
1513
2884
|
else if (n.level === 2) baseScale = 0.04;
|
|
1514
|
-
else if (n.level === 3)
|
|
1515
|
-
|
|
2885
|
+
else if (n.level === 3) {
|
|
2886
|
+
const wn2 = chapterWeightNormById.get(n.id) ?? 0;
|
|
2887
|
+
baseScale = THREE6.MathUtils.lerp(0.019, 0.039, wn2);
|
|
2888
|
+
}
|
|
2889
|
+
const size = new THREE6.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1516
2890
|
const mat = createSmartMaterial({
|
|
1517
2891
|
uniforms: {
|
|
1518
2892
|
uMap: { value: texRes.tex },
|
|
@@ -1551,39 +2925,59 @@ function createEngine({
|
|
|
1551
2925
|
`,
|
|
1552
2926
|
transparent: true,
|
|
1553
2927
|
depthWrite: false,
|
|
1554
|
-
depthTest: true
|
|
2928
|
+
depthTest: n.level === 3 ? false : true
|
|
1555
2929
|
});
|
|
1556
|
-
const mesh = new
|
|
2930
|
+
const mesh = new THREE6.Mesh(new THREE6.PlaneGeometry(1, 1), mat);
|
|
1557
2931
|
let p = getPosition(n);
|
|
1558
2932
|
if (n.level === 1) {
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
2933
|
+
if (cfg.arrangement?.[n.id]) {
|
|
2934
|
+
const arr = cfg.arrangement[n.id];
|
|
2935
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
2936
|
+
} else {
|
|
2937
|
+
if (divisionPositions.has(n.id)) {
|
|
2938
|
+
p.copy(divisionPositions.get(n.id));
|
|
2939
|
+
}
|
|
2940
|
+
const r = layoutCfg.radius * 0.95;
|
|
2941
|
+
const angle = Math.atan2(p.z, p.x);
|
|
2942
|
+
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1561
2943
|
}
|
|
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
2944
|
} else if (n.level === 3) {
|
|
1566
|
-
|
|
1567
|
-
|
|
2945
|
+
const starSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
2946
|
+
const starNorm = THREE6.MathUtils.clamp(
|
|
2947
|
+
(starSize - minChapterStarSize) / (maxChapterStarSize - minChapterStarSize),
|
|
2948
|
+
0,
|
|
2949
|
+
1
|
|
2950
|
+
);
|
|
2951
|
+
const radialOffset = THREE6.MathUtils.lerp(16, 46, starNorm);
|
|
2952
|
+
p.addScaledVector(p.clone().normalize(), radialOffset);
|
|
1568
2953
|
}
|
|
1569
2954
|
mesh.position.set(p.x, p.y, p.z);
|
|
1570
2955
|
mesh.scale.set(size.x, size.y, 1);
|
|
1571
2956
|
mesh.frustumCulled = false;
|
|
1572
2957
|
mesh.userData = { id: n.id };
|
|
1573
2958
|
root.add(mesh);
|
|
1574
|
-
|
|
2959
|
+
const wn = n.level === 3 ? chapterWeightNormById.get(n.id) ?? 0 : 0;
|
|
2960
|
+
const chapterMaxFovBias = n.level === 3 ? THREE6.MathUtils.lerp(-4, 8, wn) : 0;
|
|
2961
|
+
dynamicLabels.push({
|
|
2962
|
+
obj: mesh,
|
|
2963
|
+
node: n,
|
|
2964
|
+
initialScale: size.clone(),
|
|
2965
|
+
maxFovBias: chapterMaxFovBias,
|
|
2966
|
+
chapterStarSizeNorm: n.level === 3 ? wn : void 0,
|
|
2967
|
+
chapterStarBaseSize: n.level === 3 ? chapterStarSizeById.get(n.id) ?? 3.5 : void 0
|
|
2968
|
+
});
|
|
1575
2969
|
}
|
|
1576
2970
|
}
|
|
1577
2971
|
}
|
|
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
|
|
2972
|
+
const starGeo = new THREE6.BufferGeometry();
|
|
2973
|
+
starGeo.setAttribute("position", new THREE6.Float32BufferAttribute(starPositions, 3));
|
|
2974
|
+
starGeo.setAttribute("size", new THREE6.Float32BufferAttribute(starSizes, 1));
|
|
2975
|
+
starGeo.setAttribute("color", new THREE6.Float32BufferAttribute(starColors, 3));
|
|
2976
|
+
starGeo.setAttribute("phase", new THREE6.Float32BufferAttribute(starPhases, 1));
|
|
2977
|
+
starGeo.setAttribute("bookIndex", new THREE6.Float32BufferAttribute(starBookIndices, 1));
|
|
2978
|
+
starGeo.setAttribute("chapterIndex", new THREE6.Float32BufferAttribute(starChapterIndices, 1));
|
|
2979
|
+
starGeo.setAttribute("testamentIndex", new THREE6.Float32BufferAttribute(starTestamentIndices, 1));
|
|
2980
|
+
starGeo.setAttribute("divisionIndex", new THREE6.Float32BufferAttribute(starDivisionIndices, 1));
|
|
1587
2981
|
const starMat = createSmartMaterial({
|
|
1588
2982
|
uniforms: {
|
|
1589
2983
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -1592,7 +2986,7 @@ function createEngine({
|
|
|
1592
2986
|
uActiveBookIndex: { value: -1 },
|
|
1593
2987
|
uOrderRevealStrength: { value: 0 },
|
|
1594
2988
|
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1595
|
-
uPulseParams: { value: new
|
|
2989
|
+
uPulseParams: { value: new THREE6.Vector3(
|
|
1596
2990
|
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1597
2991
|
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1598
2992
|
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
@@ -1613,6 +3007,7 @@ function createEngine({
|
|
|
1613
3007
|
attribute float divisionIndex;
|
|
1614
3008
|
|
|
1615
3009
|
varying vec3 vColor;
|
|
3010
|
+
varying float vSize;
|
|
1616
3011
|
uniform float pixelRatio;
|
|
1617
3012
|
|
|
1618
3013
|
uniform float uTime;
|
|
@@ -1685,41 +3080,159 @@ function createEngine({
|
|
|
1685
3080
|
gl_Position = smartProject(mvPosition);
|
|
1686
3081
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1687
3082
|
|
|
1688
|
-
float sizeBoost = 1.0 + activePulse * 0.
|
|
1689
|
-
|
|
1690
|
-
|
|
3083
|
+
float sizeBoost = 1.0 + activePulse * 0.15;
|
|
3084
|
+
// pow(size, 0.7) is gentler compression than 0.55 \u2014 preserves more of
|
|
3085
|
+
// the aggressive JS curve so large stars stay visually dominant.
|
|
3086
|
+
float perceptualSize = pow(size, 0.7);
|
|
3087
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 20.0) * uScale * pixelRatio * (2000.0 / length(mvPosition.xyz)) * horizonFade, 1.0, 600.0);
|
|
3088
|
+
vSize = gl_PointSize;
|
|
1691
3089
|
}
|
|
1692
3090
|
`,
|
|
1693
3091
|
fragmentShader: `
|
|
1694
|
-
varying vec3 vColor;
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
float
|
|
3092
|
+
varying vec3 vColor;
|
|
3093
|
+
varying float vSize;
|
|
3094
|
+
void main() {
|
|
3095
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
3096
|
+
float d = length(coord) * 2.0;
|
|
3097
|
+
if (d > 1.0) discard;
|
|
3098
|
+
|
|
3099
|
+
float alphaMask = getMaskAlpha();
|
|
3100
|
+
if (alphaMask < 0.01) discard;
|
|
3101
|
+
|
|
3102
|
+
// --- Multi-layer Gaussian star model ---
|
|
3103
|
+
// Tight white-hot core
|
|
3104
|
+
float core = exp(-d * d * 9.0);
|
|
3105
|
+
// Broader coloured inner halo
|
|
3106
|
+
float innerGlow = exp(-d * d * 3.0) * 0.45;
|
|
3107
|
+
// Wide faint bloom that fades smoothly to the disc edge
|
|
3108
|
+
float outerBloom = max(0.0, 1.0 - d * d) * 0.10;
|
|
3109
|
+
|
|
3110
|
+
float k = core + innerGlow + outerBloom;
|
|
3111
|
+
|
|
3112
|
+
// White-hot centre \u2192 spectral colour at the halo
|
|
3113
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.88);
|
|
3114
|
+
|
|
3115
|
+
// --- Size-dependent diffraction spikes ---
|
|
3116
|
+
// Only appear on larger (brighter) stars, matching real optics.
|
|
3117
|
+
float spikeFactor = smoothstep(10.0, 24.0, vSize);
|
|
3118
|
+
float spikeH = exp(-coord.y * coord.y * 180.0) * exp(-abs(coord.x) * 6.0);
|
|
3119
|
+
float spikeV = exp(-coord.x * coord.x * 180.0) * exp(-abs(coord.y) * 6.0);
|
|
3120
|
+
float spikes = (spikeH + spikeV) * 0.18 * spikeFactor;
|
|
1707
3121
|
|
|
1708
|
-
|
|
1709
|
-
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.7);
|
|
1710
|
-
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
3122
|
+
gl_FragColor = vec4(finalColor * (k + spikes) * alphaMask, 1.0);
|
|
1711
3123
|
}
|
|
1712
3124
|
`,
|
|
1713
3125
|
transparent: true,
|
|
1714
3126
|
depthWrite: false,
|
|
1715
3127
|
depthTest: true,
|
|
1716
|
-
blending:
|
|
3128
|
+
blending: THREE6.AdditiveBlending
|
|
1717
3129
|
});
|
|
1718
|
-
starPoints = new
|
|
3130
|
+
starPoints = new THREE6.Points(starGeo, starMat);
|
|
1719
3131
|
starPoints.frustumCulled = false;
|
|
1720
3132
|
root.add(starPoints);
|
|
1721
3133
|
const linePoints = [];
|
|
3134
|
+
const lineWeights = [];
|
|
3135
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1722
3136
|
const bookMap = /* @__PURE__ */ new Map();
|
|
3137
|
+
const parseBookKeyFromChapterId = (id) => {
|
|
3138
|
+
if (!id) return null;
|
|
3139
|
+
const parts = id.split(":");
|
|
3140
|
+
if (parts.length < 3 || parts[0] !== "C") return null;
|
|
3141
|
+
return parts[1] || null;
|
|
3142
|
+
};
|
|
3143
|
+
const weightScaleFromLabel = (weight) => {
|
|
3144
|
+
if (weight === "thin") return 0.65;
|
|
3145
|
+
if (weight === "bold") return 1.6;
|
|
3146
|
+
return 1;
|
|
3147
|
+
};
|
|
3148
|
+
const edgeKey = (aNodeId, bNodeId) => aNodeId < bNodeId ? `${aNodeId}|${bNodeId}` : `${bNodeId}|${aNodeId}`;
|
|
3149
|
+
const addTruncatedSegment = (aNodeId, bNodeId, weightScale) => {
|
|
3150
|
+
if (aNodeId === bNodeId) return;
|
|
3151
|
+
const k = edgeKey(aNodeId, bNodeId);
|
|
3152
|
+
if (seenEdges.has(k)) return;
|
|
3153
|
+
seenEdges.add(k);
|
|
3154
|
+
const aNode = nodeById.get(aNodeId);
|
|
3155
|
+
const bNode = nodeById.get(bNodeId);
|
|
3156
|
+
if (!aNode || !bNode) return;
|
|
3157
|
+
const p1 = getPosition(aNode);
|
|
3158
|
+
const p2 = getPosition(bNode);
|
|
3159
|
+
const dir = new THREE6.Vector3().subVectors(p2, p1);
|
|
3160
|
+
const len = dir.length();
|
|
3161
|
+
if (len < 1e-3) return;
|
|
3162
|
+
dir.divideScalar(len);
|
|
3163
|
+
let cutA = chapterLineCutById.get(aNodeId) ?? 4;
|
|
3164
|
+
let cutB = chapterLineCutById.get(bNodeId) ?? 4;
|
|
3165
|
+
const maxTotalCut = len * 0.8;
|
|
3166
|
+
const totalCut = cutA + cutB;
|
|
3167
|
+
if (totalCut > maxTotalCut && totalCut > 0) {
|
|
3168
|
+
const scale = maxTotalCut / totalCut;
|
|
3169
|
+
cutA *= scale;
|
|
3170
|
+
cutB *= scale;
|
|
3171
|
+
}
|
|
3172
|
+
const a = p1.clone().addScaledVector(dir, cutA);
|
|
3173
|
+
const b = p2.clone().addScaledVector(dir, -cutB);
|
|
3174
|
+
linePoints.push(a.x, a.y, a.z);
|
|
3175
|
+
linePoints.push(b.x, b.y, b.z);
|
|
3176
|
+
lineWeights.push(weightScale);
|
|
3177
|
+
};
|
|
3178
|
+
const customBooks = /* @__PURE__ */ new Set();
|
|
3179
|
+
const rawConstellations = cfg.constellations && Array.isArray(cfg.constellations.constellations) ? cfg.constellations.constellations : [];
|
|
3180
|
+
for (const c of rawConstellations) {
|
|
3181
|
+
const linePaths = Array.isArray(c?.linePaths) ? c.linePaths : [];
|
|
3182
|
+
const lineSegments = Array.isArray(c?.lineSegments) ? c.lineSegments : [];
|
|
3183
|
+
if (linePaths.length === 0 && lineSegments.length === 0) continue;
|
|
3184
|
+
const anchorBookKey = parseBookKeyFromChapterId(c?.anchors?.[0]);
|
|
3185
|
+
if (anchorBookKey) customBooks.add(anchorBookKey);
|
|
3186
|
+
for (const segDef of lineSegments) {
|
|
3187
|
+
let from;
|
|
3188
|
+
let to;
|
|
3189
|
+
let weightLabel;
|
|
3190
|
+
if (Array.isArray(segDef)) {
|
|
3191
|
+
const raw = segDef;
|
|
3192
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3193
|
+
weightLabel = raw[0];
|
|
3194
|
+
from = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3195
|
+
to = typeof raw[2] === "string" ? raw[2] : void 0;
|
|
3196
|
+
} else {
|
|
3197
|
+
from = typeof raw[0] === "string" ? raw[0] : void 0;
|
|
3198
|
+
to = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3199
|
+
}
|
|
3200
|
+
} else if (segDef) {
|
|
3201
|
+
from = typeof segDef.from === "string" ? segDef.from : void 0;
|
|
3202
|
+
to = typeof segDef.to === "string" ? segDef.to : void 0;
|
|
3203
|
+
weightLabel = typeof segDef.weight === "string" ? segDef.weight : void 0;
|
|
3204
|
+
}
|
|
3205
|
+
if (!from || !to) continue;
|
|
3206
|
+
const k1 = parseBookKeyFromChapterId(from);
|
|
3207
|
+
const k2 = parseBookKeyFromChapterId(to);
|
|
3208
|
+
if (k1) customBooks.add(k1);
|
|
3209
|
+
if (k2) customBooks.add(k2);
|
|
3210
|
+
addTruncatedSegment(from, to, weightScaleFromLabel(weightLabel));
|
|
3211
|
+
}
|
|
3212
|
+
for (const pathDef of linePaths) {
|
|
3213
|
+
let nodes = [];
|
|
3214
|
+
let weightLabel = void 0;
|
|
3215
|
+
if (Array.isArray(pathDef)) {
|
|
3216
|
+
const raw = pathDef;
|
|
3217
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3218
|
+
weightLabel = raw[0];
|
|
3219
|
+
nodes = raw.slice(1).filter((v) => typeof v === "string");
|
|
3220
|
+
} else {
|
|
3221
|
+
nodes = raw.filter((v) => typeof v === "string");
|
|
3222
|
+
}
|
|
3223
|
+
} else if (pathDef && Array.isArray(pathDef.nodes)) {
|
|
3224
|
+
nodes = pathDef.nodes.filter((v) => typeof v === "string");
|
|
3225
|
+
weightLabel = typeof pathDef.weight === "string" ? pathDef.weight : void 0;
|
|
3226
|
+
}
|
|
3227
|
+
if (nodes.length < 2) continue;
|
|
3228
|
+
const inferredBookKey = parseBookKeyFromChapterId(nodes[0]);
|
|
3229
|
+
if (inferredBookKey) customBooks.add(inferredBookKey);
|
|
3230
|
+
const w = weightScaleFromLabel(weightLabel);
|
|
3231
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
3232
|
+
addTruncatedSegment(nodes[i], nodes[i + 1], w);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
1723
3236
|
for (const n of laidOut.nodes) {
|
|
1724
3237
|
if (n.level === 3 && n.parent) {
|
|
1725
3238
|
const list = bookMap.get(n.parent) ?? [];
|
|
@@ -1730,24 +3243,27 @@ function createEngine({
|
|
|
1730
3243
|
for (const chapters of bookMap.values()) {
|
|
1731
3244
|
chapters.sort((a, b) => (a.meta?.chapter || 0) - (b.meta?.chapter || 0));
|
|
1732
3245
|
if (chapters.length < 2) continue;
|
|
3246
|
+
const bookKey = chapters[0]?.meta?.bookKey ?? null;
|
|
3247
|
+
if (bookKey && customBooks.has(bookKey)) continue;
|
|
1733
3248
|
for (let i = 0; i < chapters.length - 1; i++) {
|
|
1734
3249
|
const c1 = chapters[i];
|
|
1735
3250
|
const c2 = chapters[i + 1];
|
|
1736
3251
|
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);
|
|
3252
|
+
addTruncatedSegment(c1.id, c2.id, 1);
|
|
1741
3253
|
}
|
|
1742
3254
|
}
|
|
1743
3255
|
if (linePoints.length > 0) {
|
|
1744
3256
|
const quadPositions = [];
|
|
1745
3257
|
const quadUvs = [];
|
|
3258
|
+
const quadLineWeight = [];
|
|
3259
|
+
const quadSegmentIndex = [];
|
|
1746
3260
|
const quadIndices = [];
|
|
1747
3261
|
const lineWidth = 8;
|
|
3262
|
+
const segmentCount = linePoints.length / 6;
|
|
1748
3263
|
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1749
3264
|
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1750
3265
|
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
3266
|
+
const segIndex = i / 6;
|
|
1751
3267
|
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1752
3268
|
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1753
3269
|
if (len < 1e-3) continue;
|
|
@@ -1772,23 +3288,36 @@ function createEngine({
|
|
|
1772
3288
|
quadUvs.push(1, -1);
|
|
1773
3289
|
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1774
3290
|
quadUvs.push(1, 1);
|
|
3291
|
+
const w = lineWeights[segIndex] ?? 1;
|
|
3292
|
+
quadLineWeight.push(w, w, w, w);
|
|
3293
|
+
quadSegmentIndex.push(segIndex, segIndex, segIndex, segIndex);
|
|
1775
3294
|
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1776
3295
|
}
|
|
1777
|
-
const lineGeo = new
|
|
1778
|
-
lineGeo.setAttribute("position", new
|
|
1779
|
-
lineGeo.setAttribute("lineUv", new
|
|
3296
|
+
const lineGeo = new THREE6.BufferGeometry();
|
|
3297
|
+
lineGeo.setAttribute("position", new THREE6.Float32BufferAttribute(quadPositions, 3));
|
|
3298
|
+
lineGeo.setAttribute("lineUv", new THREE6.Float32BufferAttribute(quadUvs, 2));
|
|
3299
|
+
lineGeo.setAttribute("lineWeight", new THREE6.Float32BufferAttribute(quadLineWeight, 1));
|
|
3300
|
+
lineGeo.setAttribute("segmentIndex", new THREE6.Float32BufferAttribute(quadSegmentIndex, 1));
|
|
1780
3301
|
lineGeo.setIndex(quadIndices);
|
|
1781
3302
|
const lineMat = createSmartMaterial({
|
|
1782
3303
|
uniforms: {
|
|
1783
|
-
color: { value: new
|
|
3304
|
+
color: { value: new THREE6.Color(11193599) },
|
|
1784
3305
|
uLineWidth: { value: 1.5 },
|
|
1785
|
-
uGlowIntensity: { value: 0.3 }
|
|
3306
|
+
uGlowIntensity: { value: 0.3 },
|
|
3307
|
+
uReveal: { value: 0 },
|
|
3308
|
+
uSegmentCount: { value: Math.max(1, segmentCount) }
|
|
1786
3309
|
},
|
|
1787
3310
|
vertexShaderBody: `
|
|
1788
3311
|
attribute vec2 lineUv;
|
|
3312
|
+
attribute float lineWeight;
|
|
3313
|
+
attribute float segmentIndex;
|
|
1789
3314
|
varying vec2 vLineUv;
|
|
3315
|
+
varying float vLineWeight;
|
|
3316
|
+
varying float vSegmentIndex;
|
|
1790
3317
|
void main() {
|
|
1791
3318
|
vLineUv = lineUv;
|
|
3319
|
+
vLineWeight = lineWeight;
|
|
3320
|
+
vSegmentIndex = segmentIndex;
|
|
1792
3321
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1793
3322
|
gl_Position = smartProject(mvPosition);
|
|
1794
3323
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
@@ -1798,32 +3327,53 @@ function createEngine({
|
|
|
1798
3327
|
uniform vec3 color;
|
|
1799
3328
|
uniform float uLineWidth;
|
|
1800
3329
|
uniform float uGlowIntensity;
|
|
3330
|
+
uniform float uReveal;
|
|
3331
|
+
uniform float uSegmentCount;
|
|
1801
3332
|
varying vec2 vLineUv;
|
|
3333
|
+
varying float vLineWeight;
|
|
3334
|
+
varying float vSegmentIndex;
|
|
1802
3335
|
void main() {
|
|
1803
3336
|
float alphaMask = getMaskAlpha();
|
|
1804
3337
|
if (alphaMask < 0.01) discard;
|
|
1805
3338
|
|
|
3339
|
+
// Progressive line draw tuned closer to Stellarium feel:
|
|
3340
|
+
// - eased global reveal
|
|
3341
|
+
// - sequential segment staggering with slight overlap
|
|
3342
|
+
// - smooth growth of each segment endpoint
|
|
3343
|
+
float reveal = smoothstep(0.0, 1.0, uReveal);
|
|
3344
|
+
float segCount = max(uSegmentCount, 1.0);
|
|
3345
|
+
float segStart = vSegmentIndex / segCount;
|
|
3346
|
+
float segSpan = (1.25 / segCount) + 0.04;
|
|
3347
|
+
float localReveal = clamp((reveal - segStart) / segSpan, 0.0, 1.0);
|
|
3348
|
+
localReveal = smoothstep(0.0, 1.0, localReveal);
|
|
3349
|
+
|
|
3350
|
+
// Keep fragment only when x is before the animated endpoint.
|
|
3351
|
+
float endpointMask = 1.0 - smoothstep(localReveal - 0.03, localReveal + 0.02, vLineUv.x);
|
|
3352
|
+
// Fade in segment brightness as it begins drawing.
|
|
3353
|
+
float drawMask = endpointMask * smoothstep(0.0, 0.08, localReveal);
|
|
3354
|
+
if (drawMask < 0.001) discard;
|
|
3355
|
+
|
|
1806
3356
|
float dist = abs(vLineUv.y);
|
|
1807
3357
|
|
|
1808
3358
|
// Anti-aliased core line
|
|
1809
|
-
float hw = uLineWidth * 0.05;
|
|
3359
|
+
float hw = (uLineWidth * vLineWeight) * 0.05;
|
|
1810
3360
|
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1811
3361
|
|
|
1812
3362
|
// Soft glow extending outward
|
|
1813
|
-
float glow = (1.0 - dist) * uGlowIntensity;
|
|
3363
|
+
float glow = (1.0 - dist) * uGlowIntensity * vLineWeight;
|
|
1814
3364
|
|
|
1815
3365
|
float alpha = max(glow, base);
|
|
1816
3366
|
if (alpha < 0.005) discard;
|
|
1817
3367
|
|
|
1818
|
-
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
3368
|
+
gl_FragColor = vec4(color, alpha * alphaMask * drawMask);
|
|
1819
3369
|
}
|
|
1820
3370
|
`,
|
|
1821
3371
|
transparent: true,
|
|
1822
3372
|
depthWrite: false,
|
|
1823
|
-
blending:
|
|
1824
|
-
side:
|
|
3373
|
+
blending: THREE6.AdditiveBlending,
|
|
3374
|
+
side: THREE6.DoubleSide
|
|
1825
3375
|
});
|
|
1826
|
-
constellationLines = new
|
|
3376
|
+
constellationLines = new THREE6.Mesh(lineGeo, lineMat);
|
|
1827
3377
|
constellationLines.frustumCulled = false;
|
|
1828
3378
|
root.add(constellationLines);
|
|
1829
3379
|
}
|
|
@@ -1836,7 +3386,7 @@ function createEngine({
|
|
|
1836
3386
|
if (groupList) {
|
|
1837
3387
|
groupList.forEach((g, idx) => {
|
|
1838
3388
|
const groupId = `G:${bookId}:${idx}`;
|
|
1839
|
-
let p = new
|
|
3389
|
+
let p = new THREE6.Vector3();
|
|
1840
3390
|
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1841
3391
|
const arr = cfg.arrangement[groupId];
|
|
1842
3392
|
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
@@ -1855,7 +3405,7 @@ function createEngine({
|
|
|
1855
3405
|
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1856
3406
|
if (texRes) {
|
|
1857
3407
|
const baseScale = 0.036;
|
|
1858
|
-
const size = new
|
|
3408
|
+
const size = new THREE6.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1859
3409
|
const mat = createSmartMaterial({
|
|
1860
3410
|
uniforms: {
|
|
1861
3411
|
uMap: { value: texRes.tex },
|
|
@@ -1896,7 +3446,7 @@ function createEngine({
|
|
|
1896
3446
|
depthWrite: false,
|
|
1897
3447
|
depthTest: true
|
|
1898
3448
|
});
|
|
1899
|
-
const mesh = new
|
|
3449
|
+
const mesh = new THREE6.Mesh(new THREE6.PlaneGeometry(1, 1), mat);
|
|
1900
3450
|
mesh.position.copy(p);
|
|
1901
3451
|
mesh.scale.set(size.x, size.y, 1);
|
|
1902
3452
|
mesh.frustumCulled = false;
|
|
@@ -1918,14 +3468,14 @@ function createEngine({
|
|
|
1918
3468
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1919
3469
|
if (boundaries.length > 0) {
|
|
1920
3470
|
const boundaryMat = createSmartMaterial({
|
|
1921
|
-
uniforms: { color: { value: new
|
|
3471
|
+
uniforms: { color: { value: new THREE6.Color(5601177) } },
|
|
1922
3472
|
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
3473
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1924
3474
|
transparent: true,
|
|
1925
3475
|
depthWrite: false,
|
|
1926
|
-
blending:
|
|
3476
|
+
blending: THREE6.AdditiveBlending
|
|
1927
3477
|
});
|
|
1928
|
-
const boundaryGeo = new
|
|
3478
|
+
const boundaryGeo = new THREE6.BufferGeometry();
|
|
1929
3479
|
const bPoints = [];
|
|
1930
3480
|
boundaries.forEach((angle) => {
|
|
1931
3481
|
const steps = 32;
|
|
@@ -1938,8 +3488,8 @@ function createEngine({
|
|
|
1938
3488
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1939
3489
|
}
|
|
1940
3490
|
});
|
|
1941
|
-
boundaryGeo.setAttribute("position", new
|
|
1942
|
-
boundaryLines = new
|
|
3491
|
+
boundaryGeo.setAttribute("position", new THREE6.Float32BufferAttribute(bPoints, 3));
|
|
3492
|
+
boundaryLines = new THREE6.LineSegments(boundaryGeo, boundaryMat);
|
|
1943
3493
|
boundaryLines.frustumCulled = false;
|
|
1944
3494
|
root.add(boundaryLines);
|
|
1945
3495
|
}
|
|
@@ -1958,7 +3508,7 @@ function createEngine({
|
|
|
1958
3508
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1959
3509
|
const phi = Math.atan2(y, x);
|
|
1960
3510
|
const theta = r_norm * (Math.PI / 2);
|
|
1961
|
-
return new
|
|
3511
|
+
return new THREE6.Vector3(
|
|
1962
3512
|
Math.sin(theta) * Math.cos(phi),
|
|
1963
3513
|
Math.cos(theta),
|
|
1964
3514
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1971,22 +3521,23 @@ function createEngine({
|
|
|
1971
3521
|
}
|
|
1972
3522
|
}
|
|
1973
3523
|
if (polyPoints.length > 0) {
|
|
1974
|
-
const polyGeo = new
|
|
1975
|
-
polyGeo.setAttribute("position", new
|
|
3524
|
+
const polyGeo = new THREE6.BufferGeometry();
|
|
3525
|
+
polyGeo.setAttribute("position", new THREE6.Float32BufferAttribute(polyPoints, 3));
|
|
1976
3526
|
const polyMat = createSmartMaterial({
|
|
1977
|
-
uniforms: { color: { value: new
|
|
3527
|
+
uniforms: { color: { value: new THREE6.Color(3718648) } },
|
|
1978
3528
|
// Cyan-ish
|
|
1979
3529
|
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
3530
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1981
3531
|
transparent: true,
|
|
1982
3532
|
depthWrite: false,
|
|
1983
|
-
blending:
|
|
3533
|
+
blending: THREE6.AdditiveBlending
|
|
1984
3534
|
});
|
|
1985
|
-
const polyLines = new
|
|
3535
|
+
const polyLines = new THREE6.LineSegments(polyGeo, polyMat);
|
|
1986
3536
|
polyLines.frustumCulled = false;
|
|
1987
3537
|
root.add(polyLines);
|
|
1988
3538
|
}
|
|
1989
3539
|
}
|
|
3540
|
+
labelManager.setLabels(dynamicLabels);
|
|
1990
3541
|
resize();
|
|
1991
3542
|
}
|
|
1992
3543
|
let lastData = void 0;
|
|
@@ -2007,6 +3558,10 @@ function createEngine({
|
|
|
2007
3558
|
}
|
|
2008
3559
|
function setConfig(cfg) {
|
|
2009
3560
|
currentConfig = cfg;
|
|
3561
|
+
applyGroundTheme(cfg);
|
|
3562
|
+
const externalFocusId = cfg.focus?.nodeId;
|
|
3563
|
+
if (typeof externalFocusId === "string") focusedNodeId = externalFocusId;
|
|
3564
|
+
if (externalFocusId === null) focusedNodeId = null;
|
|
2010
3565
|
if (cfg.projection) setProjection(cfg.projection);
|
|
2011
3566
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
2012
3567
|
state.lon = cfg.camera.lon;
|
|
@@ -2047,6 +3602,25 @@ function createEngine({
|
|
|
2047
3602
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
2048
3603
|
}
|
|
2049
3604
|
if (cfg.constellations) {
|
|
3605
|
+
const getLayoutPosition = (id) => {
|
|
3606
|
+
const n = nodeById.get(id);
|
|
3607
|
+
if (!n) return null;
|
|
3608
|
+
const x = n.meta?.x ?? 0;
|
|
3609
|
+
const y = n.meta?.y ?? 0;
|
|
3610
|
+
const z = n.meta?.z ?? 0;
|
|
3611
|
+
if (z === 0) {
|
|
3612
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
3613
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
3614
|
+
const phi = Math.atan2(y, x);
|
|
3615
|
+
const theta = r_norm * (Math.PI / 2);
|
|
3616
|
+
return new THREE6.Vector3(
|
|
3617
|
+
Math.sin(theta) * Math.cos(phi),
|
|
3618
|
+
Math.cos(theta),
|
|
3619
|
+
Math.sin(theta) * Math.sin(phi)
|
|
3620
|
+
).multiplyScalar(radius);
|
|
3621
|
+
}
|
|
3622
|
+
return new THREE6.Vector3(x, y, z);
|
|
3623
|
+
};
|
|
2050
3624
|
constellationLayer.load(cfg.constellations, (id) => {
|
|
2051
3625
|
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
2052
3626
|
const arr = cfg.arrangement[id];
|
|
@@ -2057,17 +3631,16 @@ function createEngine({
|
|
|
2057
3631
|
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
2058
3632
|
const phi = Math.atan2(y, x);
|
|
2059
3633
|
const theta = r_norm * (Math.PI / 2);
|
|
2060
|
-
return new
|
|
3634
|
+
return new THREE6.Vector3(
|
|
2061
3635
|
Math.sin(theta) * Math.cos(phi),
|
|
2062
3636
|
Math.cos(theta),
|
|
2063
3637
|
Math.sin(theta) * Math.sin(phi)
|
|
2064
3638
|
).multiplyScalar(radius);
|
|
2065
3639
|
}
|
|
2066
|
-
return new
|
|
3640
|
+
return new THREE6.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
2067
3641
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
});
|
|
3642
|
+
return getLayoutPosition(id);
|
|
3643
|
+
}, getLayoutPosition);
|
|
2071
3644
|
}
|
|
2072
3645
|
}
|
|
2073
3646
|
function setHandlers(next) {
|
|
@@ -2092,7 +3665,7 @@ function createEngine({
|
|
|
2092
3665
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
2093
3666
|
}
|
|
2094
3667
|
for (const item of constellationLayer.getItems()) {
|
|
2095
|
-
arr[item.config.id] = { position: [item.
|
|
3668
|
+
arr[item.config.id] = { position: [item.center.x, item.center.y, item.center.z] };
|
|
2096
3669
|
}
|
|
2097
3670
|
Object.assign(arr, state.tempArrangement);
|
|
2098
3671
|
return arr;
|
|
@@ -2116,60 +3689,70 @@ function createEngine({
|
|
|
2116
3689
|
const uAspect = camera.aspect;
|
|
2117
3690
|
const w = rect.width;
|
|
2118
3691
|
const h = rect.height;
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
3692
|
+
const isEditMode = currentConfig?.editable ?? false;
|
|
3693
|
+
function pickLabel(threshold) {
|
|
3694
|
+
let closest = null;
|
|
3695
|
+
let minDist = threshold;
|
|
3696
|
+
for (const item of dynamicLabels) {
|
|
3697
|
+
if (!item.obj.visible) continue;
|
|
3698
|
+
if (isNodeFiltered(item.node)) continue;
|
|
3699
|
+
const labelMat = item.obj.material;
|
|
3700
|
+
if ((labelMat?.uniforms?.uAlpha?.value ?? 0) < 0.1) continue;
|
|
3701
|
+
const pWorld = item.obj.position;
|
|
3702
|
+
const pProj = smartProjectJS(pWorld);
|
|
3703
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
3704
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
3705
|
+
const yNDC = pProj.y * uScale;
|
|
3706
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
3707
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
3708
|
+
const d = Math.sqrt((mX - sX) ** 2 + (mY - sY) ** 2);
|
|
3709
|
+
if (d < minDist) {
|
|
3710
|
+
minDist = d;
|
|
3711
|
+
closest = item;
|
|
3712
|
+
}
|
|
2138
3713
|
}
|
|
3714
|
+
return closest;
|
|
2139
3715
|
}
|
|
3716
|
+
if (isEditMode) {
|
|
3717
|
+
if (starPoints) {
|
|
3718
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3719
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3720
|
+
raycaster.ray.direction.copy(worldDir);
|
|
3721
|
+
raycaster.params.Points.threshold = 65 * (state.fov / 60);
|
|
3722
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
3723
|
+
const pointHit = hits[0];
|
|
3724
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
3725
|
+
const id = starIndexToId[pointHit.index];
|
|
3726
|
+
if (id) {
|
|
3727
|
+
const node = nodeById.get(id);
|
|
3728
|
+
if (node && !isNodeFiltered(node)) {
|
|
3729
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3730
|
+
const starPos = new THREE6.Vector3(attr.getX(pointHit.index), attr.getY(pointHit.index), attr.getZ(pointHit.index));
|
|
3731
|
+
return { type: "star", node, index: pointHit.index, point: starPos, object: void 0 };
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
const editLabel = pickLabel(isTouchDevice ? 48 : 32);
|
|
3737
|
+
if (editLabel) {
|
|
3738
|
+
return { type: "label", node: editLabel.node, object: editLabel.obj, point: editLabel.obj.position.clone(), index: void 0 };
|
|
3739
|
+
}
|
|
3740
|
+
return void 0;
|
|
3741
|
+
}
|
|
3742
|
+
const closestLabel = pickLabel(isTouchDevice ? 48 : 40);
|
|
2140
3743
|
if (closestLabel) {
|
|
2141
3744
|
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
2142
3745
|
}
|
|
2143
3746
|
let closestConst = null;
|
|
2144
3747
|
let minConstDist = Infinity;
|
|
3748
|
+
const artWorldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3749
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3750
|
+
raycaster.ray.direction.copy(artWorldDir);
|
|
2145
3751
|
for (const item of constellationLayer.getItems()) {
|
|
2146
3752
|
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);
|
|
3753
|
+
const hits = raycaster.intersectObject(item.mesh, false);
|
|
3754
|
+
if (hits.length > 0) {
|
|
3755
|
+
const d = hits[0].distance;
|
|
2173
3756
|
if (!closestConst || d < minConstDist) {
|
|
2174
3757
|
minConstDist = d;
|
|
2175
3758
|
closestConst = item;
|
|
@@ -2182,7 +3765,7 @@ function createEngine({
|
|
|
2182
3765
|
label: closestConst.config.title,
|
|
2183
3766
|
level: -1
|
|
2184
3767
|
};
|
|
2185
|
-
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.
|
|
3768
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.center.clone(), index: void 0 };
|
|
2186
3769
|
}
|
|
2187
3770
|
if (starPoints) {
|
|
2188
3771
|
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
@@ -2213,11 +3796,20 @@ function createEngine({
|
|
|
2213
3796
|
if (hit) {
|
|
2214
3797
|
state.dragMode = "node";
|
|
2215
3798
|
state.draggedNodeId = hit.node.id;
|
|
2216
|
-
|
|
3799
|
+
if (hit.type === "star" && hit.index !== void 0 && starPoints) {
|
|
3800
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3801
|
+
const starWorldPos = new THREE6.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3802
|
+
state.draggedDist = starWorldPos.length();
|
|
3803
|
+
} else {
|
|
3804
|
+
state.draggedDist = hit.point.length();
|
|
3805
|
+
}
|
|
2217
3806
|
document.body.style.cursor = "crosshair";
|
|
3807
|
+
state.velocityX = 0;
|
|
3808
|
+
state.velocityY = 0;
|
|
2218
3809
|
if (hit.type === "star") {
|
|
2219
3810
|
state.draggedStarIndex = hit.index ?? -1;
|
|
2220
3811
|
state.draggedGroup = null;
|
|
3812
|
+
state.tempArrangement = {};
|
|
2221
3813
|
} else if (hit.type === "label") {
|
|
2222
3814
|
const bookId = hit.node.id;
|
|
2223
3815
|
const children = [];
|
|
@@ -2228,7 +3820,7 @@ function createEngine({
|
|
|
2228
3820
|
if (starId) {
|
|
2229
3821
|
const starNode = nodeById.get(starId);
|
|
2230
3822
|
if (starNode && starNode.parent === bookId) {
|
|
2231
|
-
children.push({ index: i, initialPos: new
|
|
3823
|
+
children.push({ index: i, initialPos: new THREE6.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
2232
3824
|
}
|
|
2233
3825
|
}
|
|
2234
3826
|
}
|
|
@@ -2236,7 +3828,7 @@ function createEngine({
|
|
|
2236
3828
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
2237
3829
|
state.draggedStarIndex = -1;
|
|
2238
3830
|
} else if (hit.type === "constellation") {
|
|
2239
|
-
state.draggedGroup =
|
|
3831
|
+
state.draggedGroup = { labelInitialPos: hit.point.clone(), children: [] };
|
|
2240
3832
|
state.draggedStarIndex = -1;
|
|
2241
3833
|
}
|
|
2242
3834
|
}
|
|
@@ -2265,6 +3857,9 @@ function createEngine({
|
|
|
2265
3857
|
const attr = starPoints.geometry.attributes.position;
|
|
2266
3858
|
attr.setXYZ(idx, newPos.x, newPos.y, newPos.z);
|
|
2267
3859
|
attr.needsUpdate = true;
|
|
3860
|
+
editHoverTargetPos = newPos.clone();
|
|
3861
|
+
const starId = starIndexToId[idx];
|
|
3862
|
+
if (starId) state.tempArrangement[starId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2268
3863
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
2269
3864
|
const group = state.draggedGroup;
|
|
2270
3865
|
const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
|
|
@@ -2274,16 +3869,19 @@ function createEngine({
|
|
|
2274
3869
|
} else if (state.draggedNodeId) {
|
|
2275
3870
|
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
2276
3871
|
if (cItem) {
|
|
2277
|
-
|
|
3872
|
+
const vS = group.labelInitialPos.clone().normalize();
|
|
3873
|
+
const vE = newPos.clone().normalize();
|
|
3874
|
+
cItem.mesh.quaternion.setFromUnitVectors(vS, vE);
|
|
3875
|
+
cItem.center.copy(newPos);
|
|
2278
3876
|
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2279
3877
|
}
|
|
2280
3878
|
}
|
|
2281
3879
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
2282
3880
|
const vEnd = newPos.clone().normalize();
|
|
2283
|
-
const q = new
|
|
3881
|
+
const q = new THREE6.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
2284
3882
|
if (starPoints && group.children.length > 0) {
|
|
2285
3883
|
const attr = starPoints.geometry.attributes.position;
|
|
2286
|
-
const tempVec = new
|
|
3884
|
+
const tempVec = new THREE6.Vector3();
|
|
2287
3885
|
for (const child of group.children) {
|
|
2288
3886
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
2289
3887
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -2319,7 +3917,7 @@ function createEngine({
|
|
|
2319
3917
|
if (res) {
|
|
2320
3918
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
2321
3919
|
const baseScale = 0.03;
|
|
2322
|
-
const size = new
|
|
3920
|
+
const size = new THREE6.Vector2(baseScale * res.aspect, baseScale);
|
|
2323
3921
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
2324
3922
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
2325
3923
|
}
|
|
@@ -2327,10 +3925,19 @@ function createEngine({
|
|
|
2327
3925
|
hoverLabelMesh.position.copy(hit.point);
|
|
2328
3926
|
hoverLabelMat.uniforms.uAlpha.value = 1;
|
|
2329
3927
|
hoverLabelMesh.visible = true;
|
|
3928
|
+
if (currentConfig?.editable && hit.type === "star" && hit.index !== void 0 && starPoints) {
|
|
3929
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3930
|
+
editHoverTargetPos = new THREE6.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3931
|
+
} else if (currentConfig?.editable && hit.type === "star") {
|
|
3932
|
+
editHoverTargetPos = hit.point.clone();
|
|
3933
|
+
}
|
|
2330
3934
|
} else {
|
|
2331
3935
|
currentHoverNodeId = null;
|
|
2332
3936
|
hoverLabelMat.uniforms.uAlpha.value = 0;
|
|
2333
3937
|
hoverLabelMesh.visible = false;
|
|
3938
|
+
if (currentConfig?.editable && state.dragMode !== "node") {
|
|
3939
|
+
editHoverTargetPos = null;
|
|
3940
|
+
}
|
|
2334
3941
|
}
|
|
2335
3942
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
2336
3943
|
handlers._lastHoverId = hit?.node.id;
|
|
@@ -2347,6 +3954,7 @@ function createEngine({
|
|
|
2347
3954
|
if (state.dragMode === "node") {
|
|
2348
3955
|
const fullArr = getFullArrangement();
|
|
2349
3956
|
handlers.onArrangementChange?.(fullArr);
|
|
3957
|
+
editDropFlash = 1;
|
|
2350
3958
|
state.dragMode = "none";
|
|
2351
3959
|
state.draggedNodeId = null;
|
|
2352
3960
|
state.draggedStarIndex = -1;
|
|
@@ -2361,10 +3969,12 @@ function createEngine({
|
|
|
2361
3969
|
if (hit) {
|
|
2362
3970
|
handlers.onSelect?.(hit.node);
|
|
2363
3971
|
constellationLayer.setFocused(hit.node.id);
|
|
3972
|
+
focusedNodeId = hit.node.id;
|
|
2364
3973
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2365
3974
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2366
3975
|
} else {
|
|
2367
3976
|
setFocusedBook(null);
|
|
3977
|
+
focusedNodeId = null;
|
|
2368
3978
|
}
|
|
2369
3979
|
}
|
|
2370
3980
|
} else {
|
|
@@ -2372,10 +3982,12 @@ function createEngine({
|
|
|
2372
3982
|
if (hit) {
|
|
2373
3983
|
handlers.onSelect?.(hit.node);
|
|
2374
3984
|
constellationLayer.setFocused(hit.node.id);
|
|
3985
|
+
focusedNodeId = hit.node.id;
|
|
2375
3986
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2376
3987
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2377
3988
|
} else {
|
|
2378
3989
|
setFocusedBook(null);
|
|
3990
|
+
focusedNodeId = null;
|
|
2379
3991
|
}
|
|
2380
3992
|
}
|
|
2381
3993
|
}
|
|
@@ -2391,7 +4003,7 @@ function createEngine({
|
|
|
2391
4003
|
handlers.onFovChange?.(state.fov);
|
|
2392
4004
|
updateUniforms();
|
|
2393
4005
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
2394
|
-
const quaternion = new
|
|
4006
|
+
const quaternion = new THREE6.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
2395
4007
|
const dampStartFov = 40;
|
|
2396
4008
|
const dampEndFov = 120;
|
|
2397
4009
|
let spinAmount = 1;
|
|
@@ -2400,27 +4012,27 @@ function createEngine({
|
|
|
2400
4012
|
spinAmount = 1 - Math.pow(t, 1.5) * 0.8;
|
|
2401
4013
|
}
|
|
2402
4014
|
if (spinAmount < 0.999) {
|
|
2403
|
-
const identityQuat = new
|
|
4015
|
+
const identityQuat = new THREE6.Quaternion();
|
|
2404
4016
|
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2405
4017
|
}
|
|
2406
4018
|
const y = Math.sin(state.lat);
|
|
2407
4019
|
const r = Math.cos(state.lat);
|
|
2408
4020
|
const x = r * Math.sin(state.lon);
|
|
2409
4021
|
const z = -r * Math.cos(state.lon);
|
|
2410
|
-
const currentLook = new
|
|
4022
|
+
const currentLook = new THREE6.Vector3(x, y, z);
|
|
2411
4023
|
const camForward = currentLook.clone().normalize();
|
|
2412
4024
|
const camUp = camera.up.clone();
|
|
2413
|
-
const camRight = new
|
|
2414
|
-
const camUpOrtho = new
|
|
2415
|
-
const mat = new
|
|
2416
|
-
const qOld = new
|
|
4025
|
+
const camRight = new THREE6.Vector3().crossVectors(camForward, camUp).normalize();
|
|
4026
|
+
const camUpOrtho = new THREE6.Vector3().crossVectors(camRight, camForward).normalize();
|
|
4027
|
+
const mat = new THREE6.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
4028
|
+
const qOld = new THREE6.Quaternion().setFromRotationMatrix(mat);
|
|
2417
4029
|
const qNew = qOld.clone().multiply(quaternion);
|
|
2418
|
-
const newForward = new
|
|
4030
|
+
const newForward = new THREE6.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
2419
4031
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
2420
4032
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
2421
|
-
const newUp = new
|
|
4033
|
+
const newUp = new THREE6.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
2422
4034
|
camera.up.copy(newUp);
|
|
2423
|
-
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
4035
|
+
if (!getSceneDebug()?.disableZenithBias && e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
2424
4036
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
2425
4037
|
let t = (state.fov - ENGINE_CONFIG.zenithStartFov) / range;
|
|
2426
4038
|
t = Math.max(0, Math.min(1, t));
|
|
@@ -2532,7 +4144,7 @@ function createEngine({
|
|
|
2532
4144
|
state.fov = state.pinchStartFov / scale;
|
|
2533
4145
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2534
4146
|
handlers.onFovChange?.(state.fov);
|
|
2535
|
-
if (state.fov > prevFov && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
4147
|
+
if (!getSceneDebug()?.disableZenithBias && state.fov > prevFov && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
2536
4148
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
2537
4149
|
let t = (state.fov - ENGINE_CONFIG.zenithStartFov) / range;
|
|
2538
4150
|
t = Math.max(0, Math.min(1, t));
|
|
@@ -2768,14 +4380,24 @@ function createEngine({
|
|
|
2768
4380
|
const r = Math.cos(state.lat);
|
|
2769
4381
|
const x = r * Math.sin(state.lon);
|
|
2770
4382
|
const z = -r * Math.cos(state.lon);
|
|
2771
|
-
const target = new
|
|
2772
|
-
const idealUp = new
|
|
4383
|
+
const target = new THREE6.Vector3(x, y, z);
|
|
4384
|
+
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
4385
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
2774
4386
|
camera.up.normalize();
|
|
2775
4387
|
camera.lookAt(target);
|
|
2776
4388
|
camera.updateMatrixWorld();
|
|
2777
4389
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
4390
|
+
if (groundMaterial?.uniforms?.uZenithFlatten) {
|
|
4391
|
+
const flatten = getSceneDebug()?.disableZenithFlatten ? 0 : THREE6.MathUtils.smoothstep(
|
|
4392
|
+
state.lat,
|
|
4393
|
+
THREE6.MathUtils.degToRad(68),
|
|
4394
|
+
THREE6.MathUtils.degToRad(88)
|
|
4395
|
+
);
|
|
4396
|
+
groundMaterial.uniforms.uZenithFlatten.value = flatten;
|
|
4397
|
+
}
|
|
2778
4398
|
updateUniforms();
|
|
4399
|
+
if (getSceneDebug()?.horizonDiagnostics) runHorizonDiagnostics(now);
|
|
4400
|
+
updateChapterLabelAnchors();
|
|
2779
4401
|
const nowSec = now / 1e3;
|
|
2780
4402
|
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2781
4403
|
lastTickTime = nowSec;
|
|
@@ -2783,21 +4405,60 @@ function createEngine({
|
|
|
2783
4405
|
linesFader.update(dt);
|
|
2784
4406
|
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2785
4407
|
artFader.update(dt);
|
|
2786
|
-
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
}
|
|
4408
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01, camera, dt);
|
|
4409
|
+
const baseArtOpacity = THREE6.MathUtils.clamp(currentConfig?.constellationBaseOpacity ?? 1, 0, 300);
|
|
4410
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased * baseArtOpacity);
|
|
2790
4411
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
4412
|
+
if (backdropStarsMaterial?.uniforms) {
|
|
4413
|
+
const minGain = THREE6.MathUtils.clamp(currentConfig?.backdropWideFovGain ?? 0.42, 0, 1);
|
|
4414
|
+
const fovT = THREE6.MathUtils.smoothstep(state.fov, 24, 100);
|
|
4415
|
+
const gain = THREE6.MathUtils.lerp(1, minGain, fovT);
|
|
4416
|
+
backdropStarsMaterial.uniforms.uBackdropGain.value = gain;
|
|
4417
|
+
backdropStarsMaterial.uniforms.uBackdropEnergy.value = THREE6.MathUtils.clamp(currentConfig?.backdropEnergy ?? 2.2, 0.2, 5);
|
|
4418
|
+
backdropStarsMaterial.uniforms.uBackdropSizeExp.value = THREE6.MathUtils.clamp(currentConfig?.backdropSizeExponent ?? 0.9, 0.4, 1.4);
|
|
4419
|
+
}
|
|
4420
|
+
if (skyBackgroundMesh) skyBackgroundMesh.visible = currentConfig?.background !== "transparent";
|
|
2791
4421
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2792
|
-
|
|
2793
|
-
|
|
4422
|
+
if (moonMesh) moonMesh.visible = currentConfig?.showMoon ?? true;
|
|
4423
|
+
if (moonGlowMesh) moonGlowMesh.visible = currentConfig?.showMoon ?? true;
|
|
4424
|
+
const showSun = currentConfig?.showSunrise ?? true;
|
|
4425
|
+
if (sunDiscMesh) sunDiscMesh.visible = showSun;
|
|
4426
|
+
if (sunHaloMesh) sunHaloMesh.visible = showSun;
|
|
4427
|
+
if (milkyWayMesh) milkyWayMesh.visible = currentConfig?.showMilkyWay ?? true;
|
|
4428
|
+
if (editHoverMesh) {
|
|
4429
|
+
const ringMat = editHoverMesh.material;
|
|
4430
|
+
const isEditing = currentConfig?.editable ?? false;
|
|
4431
|
+
const isDraggingStar = state.dragMode === "node" && state.draggedStarIndex !== -1;
|
|
4432
|
+
const hasTarget = isEditing && editHoverTargetPos !== null;
|
|
4433
|
+
if (hasTarget) {
|
|
4434
|
+
editHoverMesh.position.copy(editHoverTargetPos);
|
|
4435
|
+
const pulseBoost = editDropFlash * 1.8;
|
|
4436
|
+
const targetAlpha = 0.8 + pulseBoost;
|
|
4437
|
+
ringMat.uniforms.uRingAlpha.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, targetAlpha, 0.15);
|
|
4438
|
+
const tGold = isDraggingStar ? 1 : editDropFlash;
|
|
4439
|
+
const targetColor = new THREE6.Color(
|
|
4440
|
+
THREE6.MathUtils.lerp(0.55, 1, tGold),
|
|
4441
|
+
THREE6.MathUtils.lerp(0.88, 0.82, tGold),
|
|
4442
|
+
THREE6.MathUtils.lerp(1, 0.18, tGold)
|
|
4443
|
+
);
|
|
4444
|
+
ringMat.uniforms.uRingColor.value.lerp(targetColor, 0.18);
|
|
4445
|
+
const baseSize = isDraggingStar ? 0.075 : 0.06;
|
|
4446
|
+
const targetSize = baseSize * (1 + editDropFlash * 0.7);
|
|
4447
|
+
ringMat.uniforms.uRingSize.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingSize.value, targetSize, 0.18);
|
|
4448
|
+
editDropFlash = Math.max(0, editDropFlash - dt * 3);
|
|
4449
|
+
} else {
|
|
4450
|
+
ringMat.uniforms.uRingAlpha.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, 0, 0.15);
|
|
4451
|
+
ringMat.uniforms.uRingSize.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingSize.value, 0.06, 0.2);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
2794
4454
|
if (constellationLines) {
|
|
2795
4455
|
constellationLines.visible = linesFader.eased > 0.01;
|
|
2796
4456
|
if (constellationLines.visible && constellationLines.material) {
|
|
2797
4457
|
const mat = constellationLines.material;
|
|
2798
4458
|
if (mat.uniforms?.color) {
|
|
2799
4459
|
mat.uniforms.color.value.setHex(11193599);
|
|
2800
|
-
mat.
|
|
4460
|
+
if (mat.uniforms.uReveal) mat.uniforms.uReveal.value = linesFader.eased;
|
|
4461
|
+
mat.opacity = 1;
|
|
2801
4462
|
}
|
|
2802
4463
|
}
|
|
2803
4464
|
}
|
|
@@ -2808,116 +4469,35 @@ function createEngine({
|
|
|
2808
4469
|
const screenW = rect.width;
|
|
2809
4470
|
const screenH = rect.height;
|
|
2810
4471
|
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;
|
|
4472
|
+
const hoverId = handlers._lastHoverId ?? null;
|
|
2859
4473
|
const selectedId = state.draggedNodeId;
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
4474
|
+
labelManager.update({
|
|
4475
|
+
nowMs: now,
|
|
4476
|
+
dt,
|
|
4477
|
+
fov: state.fov,
|
|
4478
|
+
camera,
|
|
4479
|
+
projectionId: currentProjection.id,
|
|
4480
|
+
screenW,
|
|
4481
|
+
screenH,
|
|
4482
|
+
globalScale: globalUniforms.uScale.value,
|
|
4483
|
+
aspect,
|
|
4484
|
+
hoverId,
|
|
4485
|
+
selectedId,
|
|
4486
|
+
focusedId: focusedNodeId,
|
|
4487
|
+
shouldFilter: !!currentFilter && filterStrength > 0.01,
|
|
4488
|
+
isNodeFiltered: (node) => {
|
|
4489
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
4490
|
+
return !!nodeToCheck && isNodeFiltered(nodeToCheck);
|
|
4491
|
+
},
|
|
4492
|
+
toggles: {
|
|
4493
|
+
showBookLabels: currentConfig?.showBookLabels === true,
|
|
4494
|
+
showDivisionLabels: currentConfig?.showDivisionLabels === true,
|
|
4495
|
+
showChapterLabels: currentConfig?.showChapterLabels === true,
|
|
4496
|
+
showGroupLabels: currentConfig?.showGroupLabels === true
|
|
4497
|
+
},
|
|
4498
|
+
config: currentConfig?.labelBehavior,
|
|
4499
|
+
project: smartProjectJS
|
|
2870
4500
|
});
|
|
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
4501
|
renderer.render(scene, camera);
|
|
2922
4502
|
}
|
|
2923
4503
|
function stop() {
|
|
@@ -2942,6 +4522,48 @@ function createEngine({
|
|
|
2942
4522
|
function dispose() {
|
|
2943
4523
|
stop();
|
|
2944
4524
|
constellationLayer.dispose();
|
|
4525
|
+
if (moonMesh) {
|
|
4526
|
+
scene.remove(moonMesh);
|
|
4527
|
+
moonMesh.geometry.dispose();
|
|
4528
|
+
moonMesh.material.dispose();
|
|
4529
|
+
moonMesh = null;
|
|
4530
|
+
}
|
|
4531
|
+
if (moonGlowMesh) {
|
|
4532
|
+
scene.remove(moonGlowMesh);
|
|
4533
|
+
moonGlowMesh.geometry.dispose();
|
|
4534
|
+
moonGlowMesh.material.dispose();
|
|
4535
|
+
moonGlowMesh = null;
|
|
4536
|
+
}
|
|
4537
|
+
if (sunDiscMesh) {
|
|
4538
|
+
scene.remove(sunDiscMesh);
|
|
4539
|
+
sunDiscMesh.geometry.dispose();
|
|
4540
|
+
sunDiscMesh.material.dispose();
|
|
4541
|
+
sunDiscMesh = null;
|
|
4542
|
+
}
|
|
4543
|
+
if (sunHaloMesh) {
|
|
4544
|
+
scene.remove(sunHaloMesh);
|
|
4545
|
+
sunHaloMesh.geometry.dispose();
|
|
4546
|
+
sunHaloMesh.material.dispose();
|
|
4547
|
+
sunHaloMesh = null;
|
|
4548
|
+
}
|
|
4549
|
+
if (milkyWayMesh) {
|
|
4550
|
+
scene.remove(milkyWayMesh);
|
|
4551
|
+
milkyWayMesh.geometry.dispose();
|
|
4552
|
+
milkyWayMesh.material.dispose();
|
|
4553
|
+
milkyWayMesh = null;
|
|
4554
|
+
}
|
|
4555
|
+
if (skyBackgroundMesh) {
|
|
4556
|
+
scene.remove(skyBackgroundMesh);
|
|
4557
|
+
skyBackgroundMesh.geometry.dispose();
|
|
4558
|
+
skyBackgroundMesh.material.dispose();
|
|
4559
|
+
skyBackgroundMesh = null;
|
|
4560
|
+
}
|
|
4561
|
+
if (editHoverMesh) {
|
|
4562
|
+
scene.remove(editHoverMesh);
|
|
4563
|
+
editHoverMesh.geometry.dispose();
|
|
4564
|
+
editHoverMesh.material.dispose();
|
|
4565
|
+
editHoverMesh = null;
|
|
4566
|
+
}
|
|
2945
4567
|
renderer.dispose();
|
|
2946
4568
|
renderer.domElement.remove();
|
|
2947
4569
|
}
|
|
@@ -2961,6 +4583,7 @@ function createEngine({
|
|
|
2961
4583
|
function flyTo(nodeId, targetFov) {
|
|
2962
4584
|
const node = nodeById.get(nodeId);
|
|
2963
4585
|
if (!node) return;
|
|
4586
|
+
focusedNodeId = nodeId;
|
|
2964
4587
|
const pos = getPosition(node).normalize();
|
|
2965
4588
|
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2966
4589
|
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
@@ -2991,10 +4614,11 @@ var init_createEngine = __esm({
|
|
|
2991
4614
|
init_ConstellationArtworkLayer();
|
|
2992
4615
|
init_projections();
|
|
2993
4616
|
init_fader();
|
|
4617
|
+
init_LabelManager();
|
|
2994
4618
|
ENGINE_CONFIG = {
|
|
2995
4619
|
minFov: 1,
|
|
2996
4620
|
maxFov: 135,
|
|
2997
|
-
defaultFov:
|
|
4621
|
+
defaultFov: 35,
|
|
2998
4622
|
dragSpeed: 125e-5,
|
|
2999
4623
|
inertiaDamping: 0.92,
|
|
3000
4624
|
blendStart: 35,
|
|
@@ -3021,7 +4645,7 @@ var init_createEngine = __esm({
|
|
|
3021
4645
|
};
|
|
3022
4646
|
ORDER_REVEAL_CONFIG = {
|
|
3023
4647
|
globalDim: 0.85,
|
|
3024
|
-
pulseAmplitude: 0.
|
|
4648
|
+
pulseAmplitude: 0.12,
|
|
3025
4649
|
pulseDuration: 2,
|
|
3026
4650
|
delayPerChapter: 0.1
|
|
3027
4651
|
};
|
|
@@ -32211,7 +33835,7 @@ var RNG = class {
|
|
|
32211
33835
|
const r = Math.sqrt(1 - y * y);
|
|
32212
33836
|
const x = r * Math.cos(theta);
|
|
32213
33837
|
const z = r * Math.sin(theta);
|
|
32214
|
-
return new
|
|
33838
|
+
return new THREE6.Vector3(x, y, z);
|
|
32215
33839
|
}
|
|
32216
33840
|
};
|
|
32217
33841
|
function simpleNoise3D(v, scale) {
|
|
@@ -32249,11 +33873,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
32249
33873
|
});
|
|
32250
33874
|
});
|
|
32251
33875
|
const bookCount = books.length;
|
|
32252
|
-
const mwRad =
|
|
32253
|
-
const mwNormal = new
|
|
33876
|
+
const mwRad = THREE6.MathUtils.degToRad(opts.milkyWayAngle);
|
|
33877
|
+
const mwNormal = new THREE6.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
32254
33878
|
const anchors = [];
|
|
32255
33879
|
for (let i = 0; i < bookCount; i++) {
|
|
32256
|
-
let bestP = new
|
|
33880
|
+
let bestP = new THREE6.Vector3();
|
|
32257
33881
|
let valid = false;
|
|
32258
33882
|
let attempt = 0;
|
|
32259
33883
|
while (!valid && attempt < 100) {
|
|
@@ -32279,7 +33903,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32279
33903
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
32280
33904
|
for (let c = 0; c < book.chapters; c++) {
|
|
32281
33905
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
32282
|
-
const offset = new
|
|
33906
|
+
const offset = new THREE6.Vector3(
|
|
32283
33907
|
(rng.next() - 0.5) * 2,
|
|
32284
33908
|
(rng.next() - 0.5) * 2,
|
|
32285
33909
|
(rng.next() - 0.5) * 2
|
|
@@ -32300,7 +33924,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32300
33924
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
32301
33925
|
const divId = `D:${book.testament}:${book.division}`;
|
|
32302
33926
|
if (!divisions.has(divId)) {
|
|
32303
|
-
divisions.set(divId, { sum: new
|
|
33927
|
+
divisions.set(divId, { sum: new THREE6.Vector3(), count: 0 });
|
|
32304
33928
|
}
|
|
32305
33929
|
const entry = divisions.get(divId);
|
|
32306
33930
|
entry.sum.add(anchorPos);
|