@project-skymap/library 0.7.4 → 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 +2129 -505
- 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 +2128 -504
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var THREE6 = require('three');
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
|
|
|
22
22
|
return Object.freeze(n);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
var
|
|
25
|
+
var THREE6__namespace = /*#__PURE__*/_interopNamespace(THREE6);
|
|
26
26
|
|
|
27
27
|
var __defProp = Object.defineProperty;
|
|
28
28
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -151,14 +151,14 @@ var init_constellations = __esm({
|
|
|
151
151
|
});
|
|
152
152
|
function lookAt(point, target, up) {
|
|
153
153
|
const zAxis = target.clone().normalize();
|
|
154
|
-
let xAxis = new
|
|
154
|
+
let xAxis = new THREE6__namespace.Vector3().crossVectors(up, zAxis);
|
|
155
155
|
if (xAxis.lengthSq() < 1e-4) {
|
|
156
|
-
xAxis = new
|
|
156
|
+
xAxis = new THREE6__namespace.Vector3().crossVectors(new THREE6__namespace.Vector3(1, 0, 0), zAxis);
|
|
157
157
|
}
|
|
158
158
|
xAxis.normalize();
|
|
159
|
-
const yAxis = new
|
|
160
|
-
const m = new
|
|
161
|
-
const v = new
|
|
159
|
+
const yAxis = new THREE6__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
160
|
+
const m = new THREE6__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
161
|
+
const v = new THREE6__namespace.Vector3(point.x, point.y, point.z);
|
|
162
162
|
v.applyMatrix4(m);
|
|
163
163
|
v.add(target);
|
|
164
164
|
return { x: v.x, y: v.y, z: v.z };
|
|
@@ -239,7 +239,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
239
239
|
const radiusAtY = Math.sqrt(1 - y * y);
|
|
240
240
|
const x = Math.cos(midAngle) * radiusAtY;
|
|
241
241
|
const z = Math.sin(midAngle) * radiusAtY;
|
|
242
|
-
const labelPos = new
|
|
242
|
+
const labelPos = new THREE6__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
243
243
|
uDivision.meta.x = labelPos.x;
|
|
244
244
|
uDivision.meta.y = labelPos.y;
|
|
245
245
|
uDivision.meta.z = labelPos.z;
|
|
@@ -255,7 +255,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
255
255
|
const theta = startAngle + t * angleSpan;
|
|
256
256
|
const x = Math.cos(theta) * radiusAtY;
|
|
257
257
|
const z = Math.sin(theta) * radiusAtY;
|
|
258
|
-
const bookPos = new
|
|
258
|
+
const bookPos = new THREE6__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
259
259
|
const labelPos = bookPos.clone();
|
|
260
260
|
labelPos.y += radius * 0.025;
|
|
261
261
|
labelPos.setLength(radius);
|
|
@@ -266,7 +266,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
266
266
|
if (chapters.length > 0) {
|
|
267
267
|
const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
|
|
268
268
|
const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
|
|
269
|
-
const up = new
|
|
269
|
+
const up = new THREE6__namespace.Vector3(0, 1, 0);
|
|
270
270
|
chapters.forEach((chap, idx) => {
|
|
271
271
|
const uChap = updatedNodeMap.get(chap.id);
|
|
272
272
|
const lp = localPoints[idx];
|
|
@@ -285,10 +285,10 @@ function computeLayoutPositions(model, layout) {
|
|
|
285
285
|
testaments.forEach((t) => {
|
|
286
286
|
const children = childrenMap.get(t.id) ?? [];
|
|
287
287
|
if (children.length === 0) return;
|
|
288
|
-
const centroid = new
|
|
288
|
+
const centroid = new THREE6__namespace.Vector3();
|
|
289
289
|
children.forEach((c) => {
|
|
290
290
|
const u = updatedNodeMap.get(c.id);
|
|
291
|
-
centroid.add(new
|
|
291
|
+
centroid.add(new THREE6__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
|
|
292
292
|
});
|
|
293
293
|
centroid.divideScalar(children.length);
|
|
294
294
|
if (centroid.length() > 0.1) {
|
|
@@ -409,7 +409,7 @@ float getMaskAlpha() {
|
|
|
409
409
|
});
|
|
410
410
|
function createSmartMaterial(params) {
|
|
411
411
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
412
|
-
return new
|
|
412
|
+
return new THREE6__namespace.ShaderMaterial({
|
|
413
413
|
uniforms,
|
|
414
414
|
vertexShader: `
|
|
415
415
|
${BLEND_CHUNK}
|
|
@@ -423,8 +423,8 @@ function createSmartMaterial(params) {
|
|
|
423
423
|
transparent: params.transparent || false,
|
|
424
424
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
425
425
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
426
|
-
side: params.side ||
|
|
427
|
-
blending: params.blending ||
|
|
426
|
+
side: params.side || THREE6__namespace.FrontSide,
|
|
427
|
+
blending: params.blending || THREE6__namespace.NormalBlending
|
|
428
428
|
});
|
|
429
429
|
}
|
|
430
430
|
var globalUniforms;
|
|
@@ -442,16 +442,102 @@ var init_materials = __esm({
|
|
|
442
442
|
uAtmGlow: { value: 1 },
|
|
443
443
|
uAtmDark: { value: 0.6 },
|
|
444
444
|
uAtmExtinction: { value: 4 },
|
|
445
|
-
uAtmTwinkle: { value: 0
|
|
446
|
-
uColorHorizon: { value: new
|
|
447
|
-
uColorZenith: { value: new
|
|
445
|
+
uAtmTwinkle: { value: 0 },
|
|
446
|
+
uColorHorizon: { value: new THREE6__namespace.Color(3825292) },
|
|
447
|
+
uColorZenith: { value: new THREE6__namespace.Color(132104) }
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
450
|
});
|
|
451
|
+
|
|
452
|
+
// src/engine/fader.ts
|
|
453
|
+
var Fader;
|
|
454
|
+
var init_fader = __esm({
|
|
455
|
+
"src/engine/fader.ts"() {
|
|
456
|
+
Fader = class {
|
|
457
|
+
target = false;
|
|
458
|
+
value = 0;
|
|
459
|
+
duration;
|
|
460
|
+
constructor(duration = 0.3) {
|
|
461
|
+
this.duration = duration;
|
|
462
|
+
}
|
|
463
|
+
update(dt) {
|
|
464
|
+
const goal = this.target ? 1 : 0;
|
|
465
|
+
if (this.value === goal) return;
|
|
466
|
+
const speed = 1 / this.duration;
|
|
467
|
+
const step = speed * dt;
|
|
468
|
+
const diff = goal - this.value;
|
|
469
|
+
this.value += Math.sign(diff) * Math.min(step, Math.abs(diff));
|
|
470
|
+
}
|
|
471
|
+
/** Smoothstep-eased value for perceptually smooth transitions */
|
|
472
|
+
get eased() {
|
|
473
|
+
const v = this.value;
|
|
474
|
+
return v * v * (3 - 2 * v);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
function buildSphereQuad(center, rightDir, upDir, halfWidth, halfHeight, domeRadius, subdivisions = 8) {
|
|
480
|
+
const vertsPerSide = subdivisions + 1;
|
|
481
|
+
const vertCount = vertsPerSide * vertsPerSide;
|
|
482
|
+
const positions = new Float32Array(vertCount * 3);
|
|
483
|
+
const uvs = new Float32Array(vertCount * 2);
|
|
484
|
+
const centerNorm = center.clone().normalize();
|
|
485
|
+
const temp = new THREE6__namespace.Vector3();
|
|
486
|
+
const tangent = new THREE6__namespace.Vector3();
|
|
487
|
+
const q = new THREE6__namespace.Quaternion();
|
|
488
|
+
const halfAngleX = Math.atan2(halfWidth, domeRadius);
|
|
489
|
+
const halfAngleY = Math.atan2(halfHeight, domeRadius);
|
|
490
|
+
for (let iy = 0; iy < vertsPerSide; iy++) {
|
|
491
|
+
for (let ix = 0; ix < vertsPerSide; ix++) {
|
|
492
|
+
const idx = iy * vertsPerSide + ix;
|
|
493
|
+
const u = ix / subdivisions;
|
|
494
|
+
const v = iy / subdivisions;
|
|
495
|
+
const angX = (u - 0.5) * 2 * halfAngleX;
|
|
496
|
+
const angY = (v - 0.5) * 2 * halfAngleY;
|
|
497
|
+
tangent.copy(rightDir).multiplyScalar(angX).addScaledVector(upDir, angY);
|
|
498
|
+
const angle = tangent.length();
|
|
499
|
+
if (angle > 1e-5) {
|
|
500
|
+
tangent.normalize();
|
|
501
|
+
q.setFromAxisAngle(tangent, angle);
|
|
502
|
+
temp.copy(centerNorm).applyQuaternion(q).multiplyScalar(domeRadius);
|
|
503
|
+
} else {
|
|
504
|
+
temp.copy(center);
|
|
505
|
+
}
|
|
506
|
+
positions[idx * 3 + 0] = temp.x;
|
|
507
|
+
positions[idx * 3 + 1] = temp.y;
|
|
508
|
+
positions[idx * 3 + 2] = temp.z;
|
|
509
|
+
uvs[idx * 2 + 0] = u;
|
|
510
|
+
uvs[idx * 2 + 1] = 1 - v;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const indexCount = subdivisions * subdivisions * 6;
|
|
514
|
+
const indices = new Uint16Array(indexCount);
|
|
515
|
+
let ii = 0;
|
|
516
|
+
for (let iy = 0; iy < subdivisions; iy++) {
|
|
517
|
+
for (let ix = 0; ix < subdivisions; ix++) {
|
|
518
|
+
const a = iy * vertsPerSide + ix;
|
|
519
|
+
const b = a + 1;
|
|
520
|
+
const c = a + vertsPerSide;
|
|
521
|
+
const d = c + 1;
|
|
522
|
+
indices[ii++] = a;
|
|
523
|
+
indices[ii++] = c;
|
|
524
|
+
indices[ii++] = b;
|
|
525
|
+
indices[ii++] = b;
|
|
526
|
+
indices[ii++] = c;
|
|
527
|
+
indices[ii++] = d;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const geometry = new THREE6__namespace.BufferGeometry();
|
|
531
|
+
geometry.setAttribute("position", new THREE6__namespace.BufferAttribute(positions, 3));
|
|
532
|
+
geometry.setAttribute("uv", new THREE6__namespace.BufferAttribute(uvs, 2));
|
|
533
|
+
geometry.setIndex(new THREE6__namespace.BufferAttribute(indices, 1));
|
|
534
|
+
return geometry;
|
|
535
|
+
}
|
|
451
536
|
var ConstellationArtworkLayer;
|
|
452
537
|
var init_ConstellationArtworkLayer = __esm({
|
|
453
538
|
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
454
539
|
init_materials();
|
|
540
|
+
init_fader();
|
|
455
541
|
ConstellationArtworkLayer = class {
|
|
456
542
|
root;
|
|
457
543
|
items = [];
|
|
@@ -459,27 +545,22 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
459
545
|
hoveredId = null;
|
|
460
546
|
focusedId = null;
|
|
461
547
|
constructor(root) {
|
|
462
|
-
this.textureLoader = new
|
|
548
|
+
this.textureLoader = new THREE6__namespace.TextureLoader();
|
|
463
549
|
this.textureLoader.crossOrigin = "anonymous";
|
|
464
|
-
this.root = new
|
|
550
|
+
this.root = new THREE6__namespace.Group();
|
|
465
551
|
this.root.renderOrder = -1;
|
|
466
552
|
root.add(this.root);
|
|
467
553
|
}
|
|
468
554
|
getItems() {
|
|
469
555
|
return this.items;
|
|
470
556
|
}
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
if (item) {
|
|
474
|
-
item.mesh.position.copy(pos);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
load(config, getPosition) {
|
|
557
|
+
load(config, getPosition, getOrientationPosition) {
|
|
558
|
+
const getAnchorPos = getOrientationPosition ?? getPosition;
|
|
478
559
|
this.clear();
|
|
479
560
|
console.log(`[Constellation] Loading ${config.constellations.length} constellations from ${config.atlasBasePath}`);
|
|
480
561
|
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
481
562
|
config.constellations.forEach((c) => {
|
|
482
|
-
let center = new
|
|
563
|
+
let center = new THREE6__namespace.Vector3();
|
|
483
564
|
let valid = false;
|
|
484
565
|
let radius = 2e3;
|
|
485
566
|
const arrPos = getPosition(c.id);
|
|
@@ -497,7 +578,7 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
497
578
|
}
|
|
498
579
|
}
|
|
499
580
|
} else if (c.center) {
|
|
500
|
-
center.set(c.center[0], c.center[1],
|
|
581
|
+
center.set(c.center[0], c.center[1], 0);
|
|
501
582
|
valid = true;
|
|
502
583
|
} else if (c.anchors.length > 0) {
|
|
503
584
|
const points = [];
|
|
@@ -517,100 +598,65 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
517
598
|
}
|
|
518
599
|
}
|
|
519
600
|
if (!valid) return;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
let
|
|
601
|
+
const centerNorm = center.clone().normalize();
|
|
602
|
+
let rightDir = new THREE6__namespace.Vector3();
|
|
603
|
+
let upDir = new THREE6__namespace.Vector3();
|
|
523
604
|
if (c.anchors.length >= 2) {
|
|
524
|
-
const p0 =
|
|
525
|
-
const p1 =
|
|
526
|
-
if (p0 && p1) {
|
|
527
|
-
const diff = new
|
|
528
|
-
|
|
605
|
+
const p0 = getAnchorPos(c.anchors[0]);
|
|
606
|
+
const p1 = getAnchorPos(c.anchors[1]);
|
|
607
|
+
if (p0 && p1 && p0.distanceTo(p1) > 1e-3) {
|
|
608
|
+
const diff = new THREE6__namespace.Vector3().subVectors(p1, p0);
|
|
609
|
+
rightDir.copy(diff).sub(centerNorm.clone().multiplyScalar(diff.dot(centerNorm))).normalize();
|
|
610
|
+
upDir.crossVectors(centerNorm, rightDir).normalize();
|
|
611
|
+
rightDir.crossVectors(upDir, centerNorm).normalize();
|
|
612
|
+
} else {
|
|
613
|
+
this._defaultTangentFrame(centerNorm, rightDir, upDir);
|
|
529
614
|
}
|
|
530
615
|
} else {
|
|
531
|
-
|
|
532
|
-
|
|
616
|
+
this._defaultTangentFrame(centerNorm, rightDir, upDir);
|
|
617
|
+
}
|
|
618
|
+
if (c.rotationDeg !== 0) {
|
|
619
|
+
const q = new THREE6__namespace.Quaternion().setFromAxisAngle(centerNorm, THREE6__namespace.MathUtils.degToRad(c.rotationDeg));
|
|
620
|
+
rightDir.applyQuaternion(q);
|
|
621
|
+
upDir.applyQuaternion(q);
|
|
533
622
|
}
|
|
534
|
-
const top = new THREE5__namespace.Vector3().crossVectors(upVec, right).normalize();
|
|
535
|
-
right.crossVectors(top, upVec).normalize();
|
|
536
|
-
new THREE5__namespace.Matrix4().makeBasis(right, top, normal);
|
|
537
|
-
const geometry = new THREE5__namespace.PlaneGeometry(1, 1);
|
|
538
623
|
let size = c.radius;
|
|
539
624
|
if (size <= 1) size *= radius;
|
|
540
625
|
size *= 2;
|
|
626
|
+
const sqrtAspect = Math.sqrt(c.aspectRatio ?? 1);
|
|
627
|
+
const halfWidth = size / 2 * sqrtAspect;
|
|
628
|
+
const halfHeight = size / 2 / sqrtAspect;
|
|
629
|
+
const geometry = buildSphereQuad(center, rightDir, upDir, halfWidth, halfHeight, radius, 8);
|
|
541
630
|
const texPath = `${basePath}/${c.image}`;
|
|
542
|
-
|
|
543
|
-
if (c.blend === "additive") blending = THREE5__namespace.AdditiveBlending;
|
|
631
|
+
const blending = c.blend === "additive" ? THREE6__namespace.AdditiveBlending : THREE6__namespace.NormalBlending;
|
|
544
632
|
const material = createSmartMaterial({
|
|
545
633
|
uniforms: {
|
|
546
634
|
uMap: { value: this.textureLoader.load(texPath) },
|
|
547
|
-
|
|
548
|
-
uOpacity: { value: c.opacity },
|
|
549
|
-
uSize: { value: size },
|
|
550
|
-
uImgRotation: { value: THREE5__namespace.MathUtils.degToRad(c.rotationDeg) },
|
|
551
|
-
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
552
|
-
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
635
|
+
uOpacity: { value: c.opacity }
|
|
553
636
|
},
|
|
554
637
|
vertexShaderBody: `
|
|
555
|
-
#ifdef GL_ES
|
|
556
|
-
precision highp float;
|
|
557
|
-
#endif
|
|
558
|
-
|
|
559
|
-
uniform float uSize;
|
|
560
|
-
uniform float uImgRotation;
|
|
561
|
-
uniform float uImgAspect;
|
|
562
|
-
|
|
563
638
|
varying vec2 vUv;
|
|
564
|
-
|
|
639
|
+
varying float vClipFade;
|
|
565
640
|
void main() {
|
|
566
641
|
vUv = uv;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
581
|
-
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
582
|
-
vec2 screenDelta = screenUp - screenCenter;
|
|
583
|
-
|
|
584
|
-
float horizonAngle = 0.0;
|
|
585
|
-
if (length(screenDelta) > 0.001) {
|
|
586
|
-
vec2 screenDir = normalize(screenDelta);
|
|
587
|
-
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
642
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
643
|
+
|
|
644
|
+
// Compute clip-boundary fade BEFORE smartProject
|
|
645
|
+
// (Stellarium culls entire constellations near the boundary;
|
|
646
|
+
// we fade per-vertex for smoother transitions)
|
|
647
|
+
vec3 viewDir = normalize(mvPosition.xyz);
|
|
648
|
+
float clipZ;
|
|
649
|
+
if (uProjectionType == 0) {
|
|
650
|
+
clipZ = -0.1;
|
|
651
|
+
} else if (uProjectionType == 1) {
|
|
652
|
+
clipZ = 0.1;
|
|
653
|
+
} else {
|
|
654
|
+
clipZ = mix(-0.1, 0.1, uBlend);
|
|
588
655
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// 5. Billboard Offset
|
|
594
|
-
vec2 offset = position.xy;
|
|
595
|
-
|
|
596
|
-
float cr = cos(finalAngle);
|
|
597
|
-
float sr = sin(finalAngle);
|
|
598
|
-
vec2 rotated = vec2(
|
|
599
|
-
offset.x * cr - offset.y * sr,
|
|
600
|
-
offset.x * sr + offset.y * cr
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
rotated.x *= uImgAspect;
|
|
604
|
-
|
|
605
|
-
float dist = length(mvCenter.xyz);
|
|
606
|
-
float scale = (uSize / dist) * uScale;
|
|
607
|
-
|
|
608
|
-
rotated *= scale;
|
|
609
|
-
rotated.x /= uAspect;
|
|
610
|
-
|
|
611
|
-
gl_Position = clipCenter;
|
|
612
|
-
gl_Position.xy += rotated * clipCenter.w;
|
|
613
|
-
|
|
656
|
+
// Smooth fade over 0.3 radians before the clip threshold
|
|
657
|
+
vClipFade = smoothstep(clipZ, clipZ - 0.3, viewDir.z);
|
|
658
|
+
|
|
659
|
+
gl_Position = smartProject(mvPosition);
|
|
614
660
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
615
661
|
}
|
|
616
662
|
`,
|
|
@@ -621,30 +667,50 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
621
667
|
uniform sampler2D uMap;
|
|
622
668
|
uniform float uOpacity;
|
|
623
669
|
varying vec2 vUv;
|
|
670
|
+
varying float vClipFade;
|
|
624
671
|
void main() {
|
|
625
672
|
float mask = getMaskAlpha();
|
|
626
673
|
if (mask < 0.01) discard;
|
|
627
674
|
vec4 tex = texture2D(uMap, vUv);
|
|
628
|
-
|
|
675
|
+
|
|
676
|
+
// Apply a slight blue tinge to the artwork
|
|
677
|
+
vec3 color = tex.rgb * vec3(0.8, 0.9, 1.0);
|
|
678
|
+
|
|
679
|
+
// vClipFade smoothly hides vertices near the projection
|
|
680
|
+
// clip boundary, preventing mesh distortion from escape positions
|
|
681
|
+
gl_FragColor = vec4(color, tex.a * uOpacity * mask * vClipFade);
|
|
629
682
|
}
|
|
630
683
|
`,
|
|
631
684
|
transparent: true,
|
|
632
685
|
depthWrite: false,
|
|
633
686
|
depthTest: true,
|
|
634
687
|
blending,
|
|
635
|
-
side:
|
|
688
|
+
side: THREE6__namespace.DoubleSide
|
|
636
689
|
});
|
|
637
690
|
material.uniforms.uMap.value = this.textureLoader.load(
|
|
638
691
|
texPath,
|
|
639
692
|
(tex) => {
|
|
640
|
-
tex.minFilter =
|
|
641
|
-
tex.magFilter =
|
|
693
|
+
tex.minFilter = THREE6__namespace.LinearFilter;
|
|
694
|
+
tex.magFilter = THREE6__namespace.LinearFilter;
|
|
642
695
|
tex.generateMipmaps = false;
|
|
643
696
|
tex.needsUpdate = true;
|
|
644
697
|
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
645
698
|
const natAspect = tex.image.width / tex.image.height;
|
|
646
|
-
|
|
699
|
+
const sqrtNatAspect = Math.sqrt(natAspect);
|
|
700
|
+
const newHalfWidth = size / 2 * sqrtNatAspect;
|
|
701
|
+
const newHalfHeight = size / 2 / sqrtNatAspect;
|
|
702
|
+
const newGeometry = buildSphereQuad(center, rightDir, upDir, newHalfWidth, newHalfHeight, radius, 8);
|
|
703
|
+
const item2 = this.items.find((i) => i.config.id === c.id);
|
|
704
|
+
if (item2) {
|
|
705
|
+
item2.mesh.geometry.dispose();
|
|
706
|
+
item2.mesh.geometry = newGeometry;
|
|
707
|
+
item2.halfWidth = newHalfWidth;
|
|
708
|
+
item2.halfHeight = newHalfHeight;
|
|
709
|
+
item2.imageLoadedFader.target = true;
|
|
710
|
+
}
|
|
647
711
|
}
|
|
712
|
+
const item = this.items.find((i) => i.config.id === c.id);
|
|
713
|
+
if (item) item.imageLoadedFader.target = true;
|
|
648
714
|
console.log(`[Constellation] Loaded: ${c.id} (${tex.image.width}x${tex.image.height})`);
|
|
649
715
|
},
|
|
650
716
|
(progress) => {
|
|
@@ -657,23 +723,55 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
657
723
|
material.polygonOffset = true;
|
|
658
724
|
material.polygonOffsetFactor = -c.zBias;
|
|
659
725
|
}
|
|
660
|
-
const mesh = new
|
|
726
|
+
const mesh = new THREE6__namespace.Mesh(geometry, material);
|
|
661
727
|
mesh.frustumCulled = false;
|
|
662
728
|
mesh.userData = { id: c.id, type: "constellation" };
|
|
663
|
-
mesh.position.copy(center);
|
|
664
729
|
this.root.add(mesh);
|
|
665
|
-
this.items.push({
|
|
730
|
+
this.items.push({
|
|
731
|
+
config: c,
|
|
732
|
+
mesh,
|
|
733
|
+
material,
|
|
734
|
+
baseOpacity: c.opacity,
|
|
735
|
+
center: center.clone(),
|
|
736
|
+
rightDir: rightDir.clone(),
|
|
737
|
+
upDir: upDir.clone(),
|
|
738
|
+
halfWidth,
|
|
739
|
+
halfHeight,
|
|
740
|
+
domeRadius: radius,
|
|
741
|
+
visibleFader: new Fader(0.35),
|
|
742
|
+
imageLoadedFader: new Fader(0.5)
|
|
743
|
+
});
|
|
666
744
|
});
|
|
667
745
|
}
|
|
746
|
+
_defaultTangentFrame(centerNorm, rightDir, upDir) {
|
|
747
|
+
const worldUp = new THREE6__namespace.Vector3(0, 1, 0);
|
|
748
|
+
if (Math.abs(centerNorm.dot(worldUp)) > 0.99) {
|
|
749
|
+
rightDir.crossVectors(new THREE6__namespace.Vector3(1, 0, 0), centerNorm).normalize();
|
|
750
|
+
} else {
|
|
751
|
+
rightDir.crossVectors(worldUp, centerNorm).normalize();
|
|
752
|
+
}
|
|
753
|
+
upDir.crossVectors(centerNorm, rightDir).normalize();
|
|
754
|
+
rightDir.crossVectors(upDir, centerNorm).normalize();
|
|
755
|
+
}
|
|
668
756
|
_globalOpacity = 1;
|
|
669
757
|
setGlobalOpacity(v) {
|
|
670
758
|
this._globalOpacity = v;
|
|
671
759
|
}
|
|
672
|
-
|
|
760
|
+
/**
|
|
761
|
+
* Update visibility and opacity.
|
|
762
|
+
* Accepts an optional camera for Stellarium-style visibility culling:
|
|
763
|
+
* constellations whose center is near or past the projection clip
|
|
764
|
+
* boundary are hidden to prevent mesh distortion from escape positions.
|
|
765
|
+
*/
|
|
766
|
+
update(fov, showArt, camera, dt = 0.016) {
|
|
673
767
|
this.root.visible = showArt;
|
|
674
768
|
if (!showArt) {
|
|
675
769
|
return;
|
|
676
770
|
}
|
|
771
|
+
let cameraForward = null;
|
|
772
|
+
if (camera) {
|
|
773
|
+
cameraForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
774
|
+
}
|
|
677
775
|
for (const item of this.items) {
|
|
678
776
|
const { fade } = item.config;
|
|
679
777
|
let opacity = fade.maxOpacity;
|
|
@@ -683,10 +781,30 @@ var init_ConstellationArtworkLayer = __esm({
|
|
|
683
781
|
opacity = fade.minOpacity;
|
|
684
782
|
} else {
|
|
685
783
|
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
686
|
-
opacity =
|
|
784
|
+
opacity = THREE6__namespace.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
785
|
+
}
|
|
786
|
+
const halfAngleX = Math.atan2(item.halfWidth, item.domeRadius);
|
|
787
|
+
const halfAngleY = Math.atan2(item.halfHeight, item.domeRadius);
|
|
788
|
+
const diameterDeg = Math.max(halfAngleX, halfAngleY) * 2 * THREE6__namespace.MathUtils.RAD2DEG;
|
|
789
|
+
const zoomFade = THREE6__namespace.MathUtils.smoothstep(fov, diameterDeg / 5, diameterDeg / 2);
|
|
790
|
+
opacity *= zoomFade;
|
|
791
|
+
opacity = Math.min(Math.max(opacity, 0), 1) * this._globalOpacity * item.baseOpacity;
|
|
792
|
+
if (cameraForward) {
|
|
793
|
+
const centerDir = item.center.clone().normalize();
|
|
794
|
+
const dot = cameraForward.dot(centerDir);
|
|
795
|
+
const angle = Math.acos(THREE6__namespace.MathUtils.clamp(dot, -1, 1));
|
|
796
|
+
const angularRadius = Math.max(halfAngleX, halfAngleY);
|
|
797
|
+
const margin = THREE6__namespace.MathUtils.degToRad(8);
|
|
798
|
+
item.visibleFader.target = angle <= Math.PI * 0.5 + angularRadius + margin;
|
|
799
|
+
} else {
|
|
800
|
+
item.visibleFader.target = true;
|
|
687
801
|
}
|
|
688
|
-
|
|
802
|
+
item.visibleFader.update(dt);
|
|
803
|
+
opacity *= item.visibleFader.eased;
|
|
804
|
+
item.imageLoadedFader.update(dt);
|
|
805
|
+
opacity *= item.imageLoadedFader.eased;
|
|
689
806
|
item.material.uniforms.uOpacity.value = opacity;
|
|
807
|
+
item.mesh.visible = opacity > 1e-3;
|
|
690
808
|
}
|
|
691
809
|
}
|
|
692
810
|
setHovered(id) {
|
|
@@ -783,6 +901,7 @@ var init_projections = __esm({
|
|
|
783
901
|
blendEnd;
|
|
784
902
|
/** Current blend factor, updated via setFov() */
|
|
785
903
|
blend = 0;
|
|
904
|
+
blendOverride = null;
|
|
786
905
|
constructor(blendStart = 40, blendEnd = 100) {
|
|
787
906
|
this.blendStart = blendStart;
|
|
788
907
|
this.blendEnd = blendEnd;
|
|
@@ -801,14 +920,22 @@ var init_projections = __esm({
|
|
|
801
920
|
this.blend = t * t * (3 - 2 * t);
|
|
802
921
|
}
|
|
803
922
|
getBlend() {
|
|
804
|
-
return this.blend;
|
|
923
|
+
return this.blendOverride ?? this.blend;
|
|
924
|
+
}
|
|
925
|
+
setBlendOverride(value) {
|
|
926
|
+
if (value === null || value === void 0 || Number.isNaN(value)) {
|
|
927
|
+
this.blendOverride = null;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
this.blendOverride = Math.max(0, Math.min(1, value));
|
|
805
931
|
}
|
|
806
932
|
forward(dir) {
|
|
807
|
-
|
|
808
|
-
if (
|
|
933
|
+
const b = this.getBlend();
|
|
934
|
+
if (b > 0.5 && dir.z > 0.4) return null;
|
|
935
|
+
if (b < 0.1 && dir.z > -0.1) return null;
|
|
809
936
|
const kLinear = 1 / Math.max(0.01, -dir.z);
|
|
810
937
|
const kStereo = 2 / (1 - dir.z);
|
|
811
|
-
const k = kLinear * (1 -
|
|
938
|
+
const k = kLinear * (1 - b) + kStereo * b;
|
|
812
939
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
813
940
|
}
|
|
814
941
|
inverse(uvX, uvY, fovRad) {
|
|
@@ -817,7 +944,8 @@ var init_projections = __esm({
|
|
|
817
944
|
const thetaLin = Math.atan(r * halfHeightLin);
|
|
818
945
|
const halfHeightStereo = 2 * Math.tan(fovRad / 4);
|
|
819
946
|
const thetaStereo = 2 * Math.atan(r * halfHeightStereo / 2);
|
|
820
|
-
const
|
|
947
|
+
const b = this.getBlend();
|
|
948
|
+
const theta = thetaLin * (1 - b) + thetaStereo * b;
|
|
821
949
|
const phi = Math.atan2(uvY, uvX);
|
|
822
950
|
const sinT = Math.sin(theta);
|
|
823
951
|
return {
|
|
@@ -829,11 +957,13 @@ var init_projections = __esm({
|
|
|
829
957
|
getScale(fovRad) {
|
|
830
958
|
const scaleLinear = 1 / Math.tan(fovRad / 2);
|
|
831
959
|
const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
|
|
832
|
-
|
|
960
|
+
const b = this.getBlend();
|
|
961
|
+
return scaleLinear * (1 - b) + scaleStereo * b;
|
|
833
962
|
}
|
|
834
963
|
isClipped(dirZ) {
|
|
835
|
-
|
|
836
|
-
if (
|
|
964
|
+
const b = this.getBlend();
|
|
965
|
+
if (b > 0.5) return dirZ > 0.4;
|
|
966
|
+
if (b < 0.1) return dirZ > -0.1;
|
|
837
967
|
return false;
|
|
838
968
|
}
|
|
839
969
|
};
|
|
@@ -843,30 +973,366 @@ var init_projections = __esm({
|
|
|
843
973
|
};
|
|
844
974
|
}
|
|
845
975
|
});
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
"
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
976
|
+
function levelToClass(level) {
|
|
977
|
+
if (level === 1) return "division";
|
|
978
|
+
if (level === 2) return "book";
|
|
979
|
+
if (level === 2.5) return "group";
|
|
980
|
+
return "chapter";
|
|
981
|
+
}
|
|
982
|
+
function isClassEnabled(classKey, toggles) {
|
|
983
|
+
if (classKey === "division") return toggles.showDivisionLabels;
|
|
984
|
+
if (classKey === "book") return toggles.showBookLabels;
|
|
985
|
+
if (classKey === "group") return toggles.showGroupLabels;
|
|
986
|
+
return toggles.showChapterLabels;
|
|
987
|
+
}
|
|
988
|
+
function lerp(a, b, t) {
|
|
989
|
+
return a + (b - a) * t;
|
|
990
|
+
}
|
|
991
|
+
function resolveLabelBehavior(config) {
|
|
992
|
+
const classes = { ...DEFAULT_LABEL_BEHAVIOR.classes };
|
|
993
|
+
const mergeClass = (key, source) => {
|
|
994
|
+
const base = DEFAULT_LABEL_BEHAVIOR.classes[key];
|
|
995
|
+
return {
|
|
996
|
+
minFov: source?.minFov ?? base.minFov,
|
|
997
|
+
maxFov: source?.maxFov ?? base.maxFov,
|
|
998
|
+
priority: source?.priority ?? base.priority,
|
|
999
|
+
mode: source?.mode ?? base.mode,
|
|
1000
|
+
maxOverlapPx: source?.maxOverlapPx ?? base.maxOverlapPx,
|
|
1001
|
+
radialFadeStart: source?.radialFadeStart ?? base.radialFadeStart,
|
|
1002
|
+
radialFadeEnd: source?.radialFadeEnd ?? base.radialFadeEnd,
|
|
1003
|
+
fadeDuration: source?.fadeDuration ?? base.fadeDuration
|
|
1004
|
+
};
|
|
1005
|
+
};
|
|
1006
|
+
classes.division = mergeClass("division", config?.classes?.division);
|
|
1007
|
+
classes.book = mergeClass("book", config?.classes?.book);
|
|
1008
|
+
classes.group = mergeClass("group", config?.classes?.group);
|
|
1009
|
+
classes.chapter = mergeClass("chapter", config?.classes?.chapter);
|
|
1010
|
+
return {
|
|
1011
|
+
hideBackFacing: config?.hideBackFacing ?? DEFAULT_LABEL_BEHAVIOR.hideBackFacing,
|
|
1012
|
+
overlapPaddingPx: config?.overlapPaddingPx ?? DEFAULT_LABEL_BEHAVIOR.overlapPaddingPx,
|
|
1013
|
+
reappearDelayMs: config?.reappearDelayMs ?? DEFAULT_LABEL_BEHAVIOR.reappearDelayMs,
|
|
1014
|
+
classes
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function boundsOverlapDepth(a, b) {
|
|
1018
|
+
const ix0 = Math.max(a.x, b.x);
|
|
1019
|
+
const iy0 = Math.max(a.y, b.y);
|
|
1020
|
+
const ix1 = Math.min(a.x + a.w, b.x + b.w);
|
|
1021
|
+
const iy1 = Math.min(a.y + a.h, b.y + b.h);
|
|
1022
|
+
if (ix1 <= ix0 || iy1 <= iy0) return 0;
|
|
1023
|
+
return Math.min(ix1 - ix0, iy1 - iy0);
|
|
1024
|
+
}
|
|
1025
|
+
function boundsDistPoint(rect, px, py) {
|
|
1026
|
+
const cx = rect.x + rect.w * 0.5;
|
|
1027
|
+
const cy = rect.y + rect.h * 0.5;
|
|
1028
|
+
const dx = Math.max(Math.abs(px - cx) - rect.w * 0.5, 0);
|
|
1029
|
+
const dy = Math.max(Math.abs(py - cy) - rect.h * 0.5, 0);
|
|
1030
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1031
|
+
}
|
|
1032
|
+
function getLabelUniforms(obj) {
|
|
1033
|
+
const material = obj.material;
|
|
1034
|
+
if (!(material instanceof THREE6__namespace.ShaderMaterial) || !material.uniforms) return null;
|
|
1035
|
+
const uniforms = material.uniforms;
|
|
1036
|
+
const uSize = uniforms.uSize;
|
|
1037
|
+
const uAlpha = uniforms.uAlpha;
|
|
1038
|
+
const uAngle = uniforms.uAngle;
|
|
1039
|
+
if (!uSize || !(uSize.value instanceof THREE6__namespace.Vector2) || !uAlpha || typeof uAlpha.value !== "number") {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
uSize: { value: uSize.value },
|
|
1044
|
+
uAlpha: { value: uAlpha.value },
|
|
1045
|
+
uAngle: uAngle && typeof uAngle.value === "number" ? { value: uAngle.value } : void 0
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function applyUniformAlpha(obj, alpha, angle) {
|
|
1049
|
+
const material = obj.material;
|
|
1050
|
+
if (!(material instanceof THREE6__namespace.ShaderMaterial) || !material.uniforms) return;
|
|
1051
|
+
const uniforms = material.uniforms;
|
|
1052
|
+
if (uniforms.uAlpha && typeof uniforms.uAlpha.value === "number") {
|
|
1053
|
+
uniforms.uAlpha.value = alpha;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var DEFAULT_LABEL_BEHAVIOR, LabelManager;
|
|
1057
|
+
var init_LabelManager = __esm({
|
|
1058
|
+
"src/engine/LabelManager.ts"() {
|
|
1059
|
+
init_fader();
|
|
1060
|
+
DEFAULT_LABEL_BEHAVIOR = {
|
|
1061
|
+
hideBackFacing: true,
|
|
1062
|
+
overlapPaddingPx: 2,
|
|
1063
|
+
reappearDelayMs: 60,
|
|
1064
|
+
classes: {
|
|
1065
|
+
division: {
|
|
1066
|
+
minFov: 55,
|
|
1067
|
+
maxFov: 180,
|
|
1068
|
+
priority: 70,
|
|
1069
|
+
mode: "floating",
|
|
1070
|
+
maxOverlapPx: 10,
|
|
1071
|
+
radialFadeStart: 1,
|
|
1072
|
+
radialFadeEnd: 1.2,
|
|
1073
|
+
fadeDuration: 0.28
|
|
1074
|
+
},
|
|
1075
|
+
book: {
|
|
1076
|
+
minFov: 0,
|
|
1077
|
+
maxFov: 22,
|
|
1078
|
+
priority: 60,
|
|
1079
|
+
mode: "pinned",
|
|
1080
|
+
maxOverlapPx: 999,
|
|
1081
|
+
radialFadeStart: 1,
|
|
1082
|
+
radialFadeEnd: 1.2,
|
|
1083
|
+
fadeDuration: 0.22
|
|
1084
|
+
},
|
|
1085
|
+
group: {
|
|
1086
|
+
minFov: 0,
|
|
1087
|
+
maxFov: 22,
|
|
1088
|
+
priority: 42,
|
|
1089
|
+
mode: "pinned",
|
|
1090
|
+
maxOverlapPx: 999,
|
|
1091
|
+
radialFadeStart: 1,
|
|
1092
|
+
radialFadeEnd: 1.2,
|
|
1093
|
+
fadeDuration: 0.22
|
|
1094
|
+
},
|
|
1095
|
+
chapter: {
|
|
1096
|
+
minFov: 0,
|
|
1097
|
+
maxFov: 22,
|
|
1098
|
+
priority: 30,
|
|
1099
|
+
mode: "pinned",
|
|
1100
|
+
maxOverlapPx: 999,
|
|
1101
|
+
radialFadeStart: 0.55,
|
|
1102
|
+
radialFadeEnd: 0.95,
|
|
1103
|
+
fadeDuration: 0.16
|
|
1104
|
+
}
|
|
857
1105
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
|
|
1106
|
+
};
|
|
1107
|
+
LabelManager = class {
|
|
1108
|
+
records = /* @__PURE__ */ new Map();
|
|
1109
|
+
setLabels(labels) {
|
|
1110
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
1111
|
+
for (const label of labels) {
|
|
1112
|
+
activeIds.add(label.node.id);
|
|
1113
|
+
const existing = this.records.get(label.node.id);
|
|
1114
|
+
if (existing) {
|
|
1115
|
+
existing.label = label;
|
|
1116
|
+
existing.classKey = levelToClass(label.node.level);
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
this.records.set(label.node.id, {
|
|
1120
|
+
id: label.node.id,
|
|
1121
|
+
label,
|
|
1122
|
+
fader: new Fader(0.2),
|
|
1123
|
+
classKey: levelToClass(label.node.level),
|
|
1124
|
+
lastRejectedAtMs: 0,
|
|
1125
|
+
lastAccepted: false,
|
|
1126
|
+
targetAlpha: 0
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
for (const [id] of this.records) {
|
|
1130
|
+
if (!activeIds.has(id)) {
|
|
1131
|
+
this.records.delete(id);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
865
1134
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1135
|
+
clear() {
|
|
1136
|
+
this.records.clear();
|
|
1137
|
+
}
|
|
1138
|
+
update(ctx) {
|
|
1139
|
+
const behavior = resolveLabelBehavior(ctx.config);
|
|
1140
|
+
const candidates = [];
|
|
1141
|
+
const cameraForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(ctx.camera.quaternion);
|
|
1142
|
+
for (const record of this.records.values()) {
|
|
1143
|
+
const classBehavior = behavior.classes[record.classKey];
|
|
1144
|
+
record.fader.duration = classBehavior.fadeDuration;
|
|
1145
|
+
const isEnabled = isClassEnabled(record.classKey, ctx.toggles);
|
|
1146
|
+
const isSpecial = record.id === ctx.selectedId || record.id === ctx.hoverId || record.id === ctx.focusedId;
|
|
1147
|
+
let targetAlpha = 0;
|
|
1148
|
+
let angleTarget;
|
|
1149
|
+
if (isEnabled) {
|
|
1150
|
+
const maxFov = classBehavior.maxFov + (record.label.maxFovBias ?? 0);
|
|
1151
|
+
const inFovRange = ctx.fov >= classBehavior.minFov && ctx.fov <= maxFov;
|
|
1152
|
+
if (inFovRange || isSpecial) {
|
|
1153
|
+
const pWorld = record.label.obj.position;
|
|
1154
|
+
const pProj = ctx.project(pWorld);
|
|
1155
|
+
const frontVisible = pProj.z <= 0.2;
|
|
1156
|
+
let backFacing = false;
|
|
1157
|
+
if (behavior.hideBackFacing) {
|
|
1158
|
+
const worldDir = pWorld.clone().normalize();
|
|
1159
|
+
backFacing = worldDir.dot(cameraForward) < -0.2;
|
|
1160
|
+
}
|
|
1161
|
+
if (frontVisible && !backFacing) {
|
|
1162
|
+
const ndcX = pProj.x * ctx.globalScale / ctx.aspect;
|
|
1163
|
+
const ndcY = pProj.y * ctx.globalScale;
|
|
1164
|
+
const sX = (ndcX * 0.5 + 0.5) * ctx.screenW;
|
|
1165
|
+
const sY = (-ndcY * 0.5 + 0.5) * ctx.screenH;
|
|
1166
|
+
const uniforms = getLabelUniforms(record.label.obj);
|
|
1167
|
+
if (uniforms) {
|
|
1168
|
+
const pixelH = uniforms.uSize.value.y * ctx.screenH * 0.8;
|
|
1169
|
+
const pixelW = uniforms.uSize.value.x * ctx.screenH * 0.8;
|
|
1170
|
+
targetAlpha = 1;
|
|
1171
|
+
if (targetAlpha > 0 && record.classKey === "chapter" && !isSpecial) {
|
|
1172
|
+
const dist = Math.sqrt(ndcX * ndcX + ndcY * ndcY);
|
|
1173
|
+
const fovWeight = THREE6__namespace.MathUtils.smoothstep(ctx.fov, 10, 40);
|
|
1174
|
+
const focusOuter = THREE6__namespace.MathUtils.lerp(0.82, 0.62, fovWeight);
|
|
1175
|
+
const focusInner = THREE6__namespace.MathUtils.lerp(0.24, 0.16, fovWeight);
|
|
1176
|
+
const centerFocus = 1 - THREE6__namespace.MathUtils.smoothstep(dist, focusInner, focusOuter);
|
|
1177
|
+
const chapterVisibility = THREE6__namespace.MathUtils.lerp(1, centerFocus, fovWeight);
|
|
1178
|
+
targetAlpha *= chapterVisibility;
|
|
1179
|
+
if (dist > focusOuter && ctx.fov > 12) {
|
|
1180
|
+
targetAlpha = 0;
|
|
1181
|
+
}
|
|
1182
|
+
if (dist < 0.18 && ctx.fov < 58) {
|
|
1183
|
+
targetAlpha = Math.max(targetAlpha, 0.55);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (targetAlpha > 0 && (record.classKey === "book" || record.classKey === "group") && !isSpecial) {
|
|
1187
|
+
const dist = Math.sqrt(ndcX * ndcX + ndcY * ndcY);
|
|
1188
|
+
const fovWeight = THREE6__namespace.MathUtils.smoothstep(ctx.fov, 15, 58);
|
|
1189
|
+
const focusOuter = THREE6__namespace.MathUtils.lerp(0.95, 0.7, fovWeight);
|
|
1190
|
+
const focusInner = THREE6__namespace.MathUtils.lerp(0.35, 0.22, fovWeight);
|
|
1191
|
+
const centerFocus = 1 - THREE6__namespace.MathUtils.smoothstep(dist, focusInner, focusOuter);
|
|
1192
|
+
const bookVisibility = THREE6__namespace.MathUtils.lerp(1, centerFocus, fovWeight);
|
|
1193
|
+
targetAlpha *= bookVisibility;
|
|
1194
|
+
if (dist > focusOuter && ctx.fov > 20) {
|
|
1195
|
+
targetAlpha = 0;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (targetAlpha > 0 && ctx.shouldFilter) {
|
|
1199
|
+
const node = record.label.node;
|
|
1200
|
+
if (node.level === 3) {
|
|
1201
|
+
targetAlpha = 0;
|
|
1202
|
+
} else if (node.level === 2 || node.level === 2.5) {
|
|
1203
|
+
if (ctx.isNodeFiltered(node)) targetAlpha = 0;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (targetAlpha > 0 && record.classKey === "chapter" && record.label.chapterStarWorldPos) {
|
|
1207
|
+
const starProj = ctx.project(record.label.chapterStarWorldPos);
|
|
1208
|
+
if (starProj.z <= 0.2) {
|
|
1209
|
+
const starNdcX = starProj.x * ctx.globalScale / ctx.aspect;
|
|
1210
|
+
const starNdcY = starProj.y * ctx.globalScale;
|
|
1211
|
+
const starSX = (starNdcX * 0.5 + 0.5) * ctx.screenW;
|
|
1212
|
+
const starSY = (-starNdcY * 0.5 + 0.5) * ctx.screenH;
|
|
1213
|
+
const rect = {
|
|
1214
|
+
x: sX - pixelW / 2,
|
|
1215
|
+
y: sY - pixelH / 2,
|
|
1216
|
+
w: pixelW,
|
|
1217
|
+
h: pixelH,
|
|
1218
|
+
priority: classBehavior.priority
|
|
1219
|
+
};
|
|
1220
|
+
const glowRadiusPx = record.label.chapterGlowRadiusPx ?? 18;
|
|
1221
|
+
const clearancePx = Math.max(1, glowRadiusPx * 0.02);
|
|
1222
|
+
const distToLabel = boundsDistPoint(rect, starSX, starSY);
|
|
1223
|
+
if (glowRadiusPx >= 70) {
|
|
1224
|
+
const threshold = glowRadiusPx + clearancePx;
|
|
1225
|
+
const visibility = THREE6__namespace.MathUtils.smoothstep(distToLabel, threshold - 4, threshold + 2);
|
|
1226
|
+
const lowFovRelief = 1 - THREE6__namespace.MathUtils.smoothstep(ctx.fov, 8, 18);
|
|
1227
|
+
const boostedVisibility = isSpecial ? Math.max(visibility, 0.92) : Math.max(visibility, lowFovRelief * 0.85);
|
|
1228
|
+
targetAlpha *= boostedVisibility;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (record.classKey === "division" && uniforms.uAngle) {
|
|
1233
|
+
angleTarget = 0;
|
|
1234
|
+
if (ctx.projectionId !== "perspective") {
|
|
1235
|
+
const dx = sX - ctx.screenW / 2;
|
|
1236
|
+
const dy = sY - ctx.screenH / 2;
|
|
1237
|
+
angleTarget = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (targetAlpha > 0) {
|
|
1241
|
+
const priorityBoost = isSpecial ? record.id === ctx.selectedId ? 400 : 300 : 0;
|
|
1242
|
+
candidates.push({
|
|
1243
|
+
record,
|
|
1244
|
+
behavior: classBehavior,
|
|
1245
|
+
uniforms,
|
|
1246
|
+
sX,
|
|
1247
|
+
sY,
|
|
1248
|
+
w: pixelW,
|
|
1249
|
+
h: pixelH,
|
|
1250
|
+
ndcX,
|
|
1251
|
+
ndcY,
|
|
1252
|
+
priority: classBehavior.priority + priorityBoost,
|
|
1253
|
+
isPinned: isSpecial || classBehavior.mode === "pinned",
|
|
1254
|
+
isSpecial,
|
|
1255
|
+
centerDist: Math.sqrt((sX - ctx.screenW * 0.5) ** 2 + (sY - ctx.screenH * 0.5) ** 2)
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (typeof angleTarget === "number") {
|
|
1263
|
+
const material = record.label.obj.material;
|
|
1264
|
+
if (material instanceof THREE6__namespace.ShaderMaterial && material.uniforms.uAngle && typeof material.uniforms.uAngle.value === "number") {
|
|
1265
|
+
const current = material.uniforms.uAngle.value;
|
|
1266
|
+
material.uniforms.uAngle.value = lerp(current, angleTarget, 0.1);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
record.targetAlpha = targetAlpha;
|
|
1270
|
+
}
|
|
1271
|
+
candidates.sort((a, b) => {
|
|
1272
|
+
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
1273
|
+
if (a.record.lastAccepted !== b.record.lastAccepted) return a.record.lastAccepted ? -1 : 1;
|
|
1274
|
+
return a.centerDist - b.centerDist;
|
|
1275
|
+
});
|
|
1276
|
+
let guaranteedCenterChapterId = null;
|
|
1277
|
+
if (ctx.toggles.showChapterLabels && ctx.fov <= behavior.classes.chapter.maxFov + 15) {
|
|
1278
|
+
const centerChapter = candidates.filter((c) => c.record.classKey === "chapter" && c.record.targetAlpha > 0).sort((a, b) => a.centerDist - b.centerDist)[0];
|
|
1279
|
+
if (centerChapter) {
|
|
1280
|
+
guaranteedCenterChapterId = centerChapter.record.id;
|
|
1281
|
+
centerChapter.record.targetAlpha = Math.max(centerChapter.record.targetAlpha, 0.85);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const occupied = [];
|
|
1285
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const c of candidates) {
|
|
1287
|
+
if (c.record.targetAlpha <= 0) continue;
|
|
1288
|
+
const rect = {
|
|
1289
|
+
x: c.sX - c.w / 2 - behavior.overlapPaddingPx,
|
|
1290
|
+
y: c.sY - c.h / 2 - behavior.overlapPaddingPx,
|
|
1291
|
+
w: c.w + behavior.overlapPaddingPx * 2,
|
|
1292
|
+
h: c.h + behavior.overlapPaddingPx * 2,
|
|
1293
|
+
priority: c.priority
|
|
1294
|
+
};
|
|
1295
|
+
let rejected = false;
|
|
1296
|
+
const isGuaranteedCenterChapter = c.record.id === guaranteedCenterChapterId;
|
|
1297
|
+
if (!c.isPinned && !isGuaranteedCenterChapter) {
|
|
1298
|
+
if (!c.record.lastAccepted && !c.isSpecial && c.record.lastRejectedAtMs > 0) {
|
|
1299
|
+
const sinceReject = ctx.nowMs - c.record.lastRejectedAtMs;
|
|
1300
|
+
if (sinceReject < behavior.reappearDelayMs) {
|
|
1301
|
+
rejected = true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (!rejected) {
|
|
1305
|
+
for (const other of occupied) {
|
|
1306
|
+
if (other.priority < c.priority) continue;
|
|
1307
|
+
const overlapDepth = boundsOverlapDepth(rect, other);
|
|
1308
|
+
if (overlapDepth > c.behavior.maxOverlapPx) {
|
|
1309
|
+
rejected = true;
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (rejected) {
|
|
1316
|
+
c.record.lastAccepted = false;
|
|
1317
|
+
c.record.lastRejectedAtMs = ctx.nowMs;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
occupied.push(rect);
|
|
1321
|
+
accepted.add(c.record.id);
|
|
1322
|
+
c.record.lastAccepted = true;
|
|
1323
|
+
}
|
|
1324
|
+
for (const record of this.records.values()) {
|
|
1325
|
+
const acceptedThisFrame = accepted.has(record.id) && record.targetAlpha > 0;
|
|
1326
|
+
record.fader.target = acceptedThisFrame;
|
|
1327
|
+
record.fader.update(ctx.dt);
|
|
1328
|
+
const baseAlpha = acceptedThisFrame ? record.targetAlpha : record.targetAlpha > 0 ? 1 : 0;
|
|
1329
|
+
const alpha = record.fader.eased * baseAlpha;
|
|
1330
|
+
applyUniformAlpha(record.label.obj, alpha);
|
|
1331
|
+
record.label.obj.visible = alpha > 0.01;
|
|
1332
|
+
if (!acceptedThisFrame) {
|
|
1333
|
+
record.lastAccepted = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
870
1336
|
}
|
|
871
1337
|
};
|
|
872
1338
|
}
|
|
@@ -893,6 +1359,7 @@ function createEngine({
|
|
|
893
1359
|
}) {
|
|
894
1360
|
let hoveredBookId = null;
|
|
895
1361
|
let focusedBookId = null;
|
|
1362
|
+
let focusedNodeId = null;
|
|
896
1363
|
let orderRevealEnabled = true;
|
|
897
1364
|
let activeBookIndex = -1;
|
|
898
1365
|
let orderRevealStrength = 0;
|
|
@@ -911,26 +1378,15 @@ function createEngine({
|
|
|
911
1378
|
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
912
1379
|
const testamentToIndex = /* @__PURE__ */ new Map();
|
|
913
1380
|
const divisionToIndex = /* @__PURE__ */ new Map();
|
|
914
|
-
const renderer = new
|
|
1381
|
+
const renderer = new THREE6__namespace.WebGLRenderer({ antialias: true, alpha: false });
|
|
915
1382
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
916
1383
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
917
1384
|
container.appendChild(renderer.domElement);
|
|
918
|
-
const scene = new
|
|
919
|
-
scene.background =
|
|
920
|
-
const camera = new
|
|
1385
|
+
const scene = new THREE6__namespace.Scene();
|
|
1386
|
+
scene.background = null;
|
|
1387
|
+
const camera = new THREE6__namespace.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
921
1388
|
camera.position.set(0, 0, 0);
|
|
922
1389
|
camera.up.set(0, 1, 0);
|
|
923
|
-
function setHoveredBook(id) {
|
|
924
|
-
if (id === hoveredBookId) return;
|
|
925
|
-
const now = performance.now();
|
|
926
|
-
if (hoveredBookId) {
|
|
927
|
-
hoverCooldowns.set(hoveredBookId, now);
|
|
928
|
-
}
|
|
929
|
-
if (id) {
|
|
930
|
-
hoverCooldowns.get(id) || 0;
|
|
931
|
-
}
|
|
932
|
-
hoveredBookId = id;
|
|
933
|
-
}
|
|
934
1390
|
let running = false;
|
|
935
1391
|
let raf = 0;
|
|
936
1392
|
const state = {
|
|
@@ -968,12 +1424,15 @@ function createEngine({
|
|
|
968
1424
|
longPressTimer: null,
|
|
969
1425
|
longPressTriggered: false
|
|
970
1426
|
};
|
|
971
|
-
const mouseNDC = new
|
|
1427
|
+
const mouseNDC = new THREE6__namespace.Vector2();
|
|
972
1428
|
let isMouseInWindow = false;
|
|
973
1429
|
let isTouchDevice = false;
|
|
974
1430
|
let edgeHoverStart = 0;
|
|
975
1431
|
let handlers = { onSelect, onHover, onArrangementChange, onFovChange, onLongPress };
|
|
976
1432
|
let currentConfig;
|
|
1433
|
+
function getSceneDebug() {
|
|
1434
|
+
return currentConfig?.debug?.sceneMechanics;
|
|
1435
|
+
}
|
|
977
1436
|
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
978
1437
|
function mix(a, b, t) {
|
|
979
1438
|
return a * (1 - t) + b * t;
|
|
@@ -982,6 +1441,7 @@ function createEngine({
|
|
|
982
1441
|
function syncProjectionState() {
|
|
983
1442
|
if (currentProjection instanceof BlendedProjection) {
|
|
984
1443
|
currentProjection.setFov(state.fov);
|
|
1444
|
+
currentProjection.setBlendOverride(getSceneDebug()?.projectionBlendOverride ?? null);
|
|
985
1445
|
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
986
1446
|
}
|
|
987
1447
|
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
@@ -992,10 +1452,10 @@ function createEngine({
|
|
|
992
1452
|
let scale = currentProjection.getScale(fovRad);
|
|
993
1453
|
const aspect = camera.aspect;
|
|
994
1454
|
if (currentConfig?.fitProjection) {
|
|
995
|
-
if (aspect
|
|
1455
|
+
if (aspect >= 1) {
|
|
996
1456
|
scale /= aspect;
|
|
997
1457
|
} else {
|
|
998
|
-
scale *= aspect;
|
|
1458
|
+
scale *= aspect * aspect;
|
|
999
1459
|
}
|
|
1000
1460
|
}
|
|
1001
1461
|
globalUniforms.uScale.value = scale;
|
|
@@ -1009,7 +1469,7 @@ function createEngine({
|
|
|
1009
1469
|
const uvX = mouseNDC.x * aspectRatio;
|
|
1010
1470
|
const uvY = mouseNDC.y;
|
|
1011
1471
|
const v = currentProjection.inverse(uvX, uvY, fovRad);
|
|
1012
|
-
return new
|
|
1472
|
+
return new THREE6__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
1013
1473
|
}
|
|
1014
1474
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
1015
1475
|
const aspect = width / height;
|
|
@@ -1018,7 +1478,7 @@ function createEngine({
|
|
|
1018
1478
|
syncProjectionState();
|
|
1019
1479
|
const fovRad = state.fov * Math.PI / 180;
|
|
1020
1480
|
const v = currentProjection.inverse(ndcX * aspect, ndcY, fovRad);
|
|
1021
|
-
const vView = new
|
|
1481
|
+
const vView = new THREE6__namespace.Vector3(v.x, v.y, v.z).normalize();
|
|
1022
1482
|
return vView.applyQuaternion(camera.quaternion);
|
|
1023
1483
|
}
|
|
1024
1484
|
function smartProjectJS(worldPos) {
|
|
@@ -1028,80 +1488,438 @@ function createEngine({
|
|
|
1028
1488
|
if (!result) return { x: 0, y: 0, z: dir.z };
|
|
1029
1489
|
return result;
|
|
1030
1490
|
}
|
|
1031
|
-
const groundGroup = new
|
|
1491
|
+
const groundGroup = new THREE6__namespace.Group();
|
|
1032
1492
|
scene.add(groundGroup);
|
|
1493
|
+
const MAX_HORIZON_POINTS = 64;
|
|
1494
|
+
let groundMaterial = null;
|
|
1495
|
+
let horizonLine = null;
|
|
1496
|
+
let activeHorizonProfile = {
|
|
1497
|
+
mode: 0,
|
|
1498
|
+
pointCount: 0,
|
|
1499
|
+
azDeg: [],
|
|
1500
|
+
altDeg: [],
|
|
1501
|
+
rotateRad: 0,
|
|
1502
|
+
baseAltDeg: 3
|
|
1503
|
+
};
|
|
1504
|
+
let lastHorizonDiagTs = 0;
|
|
1505
|
+
function toColor(input, fallbackHex) {
|
|
1506
|
+
if (!input) return new THREE6__namespace.Color(fallbackHex);
|
|
1507
|
+
try {
|
|
1508
|
+
return new THREE6__namespace.Color(input);
|
|
1509
|
+
} catch {
|
|
1510
|
+
return new THREE6__namespace.Color(fallbackHex);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function applyGroundTheme(cfg) {
|
|
1514
|
+
if (!groundMaterial) return;
|
|
1515
|
+
const theme = getSceneDebug()?.disableHorizonTheme ? void 0 : cfg?.horizonTheme;
|
|
1516
|
+
const uniforms = groundMaterial.uniforms;
|
|
1517
|
+
const atmo = theme?.atmosphere;
|
|
1518
|
+
const mode = theme?.source === "polygonal" && (theme.profile?.points?.length ?? 0) >= 2 ? 1 : 0;
|
|
1519
|
+
const groundColor = toColor(theme?.groundColor, 65794);
|
|
1520
|
+
const fogColor = toColor(theme?.horizonLineColor, 663098);
|
|
1521
|
+
const fogIntensity = THREE6__namespace.MathUtils.clamp(atmo?.fogIntensity ?? 0.6, 0, 1.5);
|
|
1522
|
+
const fogVisible = atmo?.fogVisible === false ? 0 : 1;
|
|
1523
|
+
const minBrightness = THREE6__namespace.MathUtils.clamp(atmo?.minimalBrightness ?? 0, 0, 1);
|
|
1524
|
+
const rotateRad = (theme?.profile?.angleRotateZDeg ?? 0) * Math.PI / 180;
|
|
1525
|
+
const azSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1526
|
+
const altSamples = new Array(MAX_HORIZON_POINTS).fill(0);
|
|
1527
|
+
let pointCount = 0;
|
|
1528
|
+
let sortedPoints = [];
|
|
1529
|
+
if (mode === 1 && theme?.profile?.points) {
|
|
1530
|
+
sortedPoints = [...theme.profile.points].map((p) => ({
|
|
1531
|
+
azDeg: (p.azDeg % 360 + 360) % 360,
|
|
1532
|
+
altDeg: THREE6__namespace.MathUtils.clamp(p.altDeg, -30, 35)
|
|
1533
|
+
})).sort((a, b) => a.azDeg - b.azDeg);
|
|
1534
|
+
pointCount = Math.min(sortedPoints.length, MAX_HORIZON_POINTS);
|
|
1535
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1536
|
+
azSamples[i] = sortedPoints[i].azDeg;
|
|
1537
|
+
altSamples[i] = sortedPoints[i].altDeg;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const baseAltDeg = pointCount > 0 ? altSamples.slice(0, pointCount).reduce((sum, v) => sum + v, 0) / pointCount : 3;
|
|
1541
|
+
activeHorizonProfile = {
|
|
1542
|
+
mode,
|
|
1543
|
+
pointCount,
|
|
1544
|
+
azDeg: azSamples.slice(0, pointCount),
|
|
1545
|
+
altDeg: altSamples.slice(0, pointCount),
|
|
1546
|
+
rotateRad,
|
|
1547
|
+
baseAltDeg
|
|
1548
|
+
};
|
|
1549
|
+
uniforms.color.value = groundColor;
|
|
1550
|
+
uniforms.fogColor.value = fogColor;
|
|
1551
|
+
uniforms.uFogIntensity.value = fogIntensity;
|
|
1552
|
+
uniforms.uFogVisible.value = fogVisible;
|
|
1553
|
+
uniforms.uMinBrightness.value = minBrightness;
|
|
1554
|
+
uniforms.uHorizonMode.value = mode;
|
|
1555
|
+
uniforms.uHorizonPointCount.value = pointCount;
|
|
1556
|
+
uniforms.uHorizonAzDeg.value = azSamples;
|
|
1557
|
+
uniforms.uHorizonAltDeg.value = altSamples;
|
|
1558
|
+
uniforms.uHorizonRotateRad.value = rotateRad;
|
|
1559
|
+
uniforms.uBaseAltDeg.value = baseAltDeg;
|
|
1560
|
+
groundMaterial.uniformsNeedUpdate = true;
|
|
1561
|
+
if (atmosphereMesh && atmosphereMesh.material instanceof THREE6__namespace.ShaderMaterial) {
|
|
1562
|
+
const atmUniforms = atmosphereMesh.material.uniforms;
|
|
1563
|
+
const topAltDeg = atmo?.fogBandTopAltDeg ?? 90;
|
|
1564
|
+
const bottomAltDeg = atmo?.fogBandBottomAltDeg ?? -90;
|
|
1565
|
+
atmUniforms.uThemeFogVisible.value = fogVisible;
|
|
1566
|
+
atmUniforms.uThemeFogIntensity.value = fogIntensity;
|
|
1567
|
+
atmUniforms.uThemeFogTopSin.value = Math.sin(THREE6__namespace.MathUtils.degToRad(topAltDeg));
|
|
1568
|
+
atmUniforms.uThemeFogBottomSin.value = Math.sin(THREE6__namespace.MathUtils.degToRad(bottomAltDeg));
|
|
1569
|
+
atmUniforms.uThemeMinBrightness.value = minBrightness;
|
|
1570
|
+
atmosphereMesh.material.uniformsNeedUpdate = true;
|
|
1571
|
+
}
|
|
1572
|
+
if (horizonLine) {
|
|
1573
|
+
groundGroup.remove(horizonLine);
|
|
1574
|
+
horizonLine.geometry.dispose();
|
|
1575
|
+
horizonLine.material.dispose();
|
|
1576
|
+
horizonLine = null;
|
|
1577
|
+
}
|
|
1578
|
+
const lineThickness = THREE6__namespace.MathUtils.clamp(theme?.horizonLineThickness ?? 0, 0, 8);
|
|
1579
|
+
const shouldDrawLine = mode === 1 && pointCount >= 2 && lineThickness > 0;
|
|
1580
|
+
if (!shouldDrawLine) return;
|
|
1581
|
+
const lineColor = toColor(theme?.horizonLineColor, 5601177);
|
|
1582
|
+
const lineRadius = 997;
|
|
1583
|
+
const pts = [];
|
|
1584
|
+
for (let i = 0; i < pointCount; i++) {
|
|
1585
|
+
const sample = sortedPoints[i];
|
|
1586
|
+
const angleDeg = sample.azDeg - (theme?.profile?.angleRotateZDeg ?? 0);
|
|
1587
|
+
const a = THREE6__namespace.MathUtils.degToRad(angleDeg);
|
|
1588
|
+
const alt = THREE6__namespace.MathUtils.degToRad(sample.altDeg);
|
|
1589
|
+
const rc = Math.cos(alt);
|
|
1590
|
+
pts.push(new THREE6__namespace.Vector3(
|
|
1591
|
+
lineRadius * rc * Math.cos(a),
|
|
1592
|
+
lineRadius * Math.sin(alt),
|
|
1593
|
+
lineRadius * rc * Math.sin(a)
|
|
1594
|
+
));
|
|
1595
|
+
}
|
|
1596
|
+
if (pts.length > 0) pts.push(pts[0].clone());
|
|
1597
|
+
const geo = new THREE6__namespace.BufferGeometry().setFromPoints(pts);
|
|
1598
|
+
const mat = createSmartMaterial({
|
|
1599
|
+
uniforms: {
|
|
1600
|
+
color: { value: lineColor },
|
|
1601
|
+
alpha: { value: 0.95 }
|
|
1602
|
+
},
|
|
1603
|
+
vertexShaderBody: `
|
|
1604
|
+
uniform vec3 color;
|
|
1605
|
+
varying vec3 vColor;
|
|
1606
|
+
void main() {
|
|
1607
|
+
vColor = color;
|
|
1608
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1609
|
+
gl_Position = smartProject(mvPosition);
|
|
1610
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1611
|
+
}
|
|
1612
|
+
`,
|
|
1613
|
+
fragmentShader: `
|
|
1614
|
+
uniform float alpha;
|
|
1615
|
+
varying vec3 vColor;
|
|
1616
|
+
void main() {
|
|
1617
|
+
float alphaMask = getMaskAlpha();
|
|
1618
|
+
if (alphaMask < 0.01) discard;
|
|
1619
|
+
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
1620
|
+
}
|
|
1621
|
+
`,
|
|
1622
|
+
transparent: true,
|
|
1623
|
+
depthWrite: false,
|
|
1624
|
+
depthTest: true
|
|
1625
|
+
});
|
|
1626
|
+
const line = new THREE6__namespace.Line(geo, mat);
|
|
1627
|
+
line.material.linewidth = lineThickness;
|
|
1628
|
+
line.frustumCulled = false;
|
|
1629
|
+
line.renderOrder = 3;
|
|
1630
|
+
horizonLine = line;
|
|
1631
|
+
groundGroup.add(line);
|
|
1632
|
+
}
|
|
1633
|
+
function sampleActiveHorizonAltDeg(azDeg) {
|
|
1634
|
+
const profile = activeHorizonProfile;
|
|
1635
|
+
if (profile.mode !== 1 || profile.pointCount < 2) return profile.baseAltDeg;
|
|
1636
|
+
const query = ((azDeg + THREE6__namespace.MathUtils.radToDeg(profile.rotateRad)) % 360 + 360) % 360;
|
|
1637
|
+
const n = profile.pointCount;
|
|
1638
|
+
const firstAz = profile.azDeg[0];
|
|
1639
|
+
const firstAlt = profile.altDeg[0];
|
|
1640
|
+
for (let i = 1; i < n; i++) {
|
|
1641
|
+
const prevAz2 = profile.azDeg[i - 1];
|
|
1642
|
+
const prevAlt2 = profile.altDeg[i - 1];
|
|
1643
|
+
const curAz = profile.azDeg[i];
|
|
1644
|
+
const curAlt = profile.altDeg[i];
|
|
1645
|
+
if (query >= prevAz2 && query <= curAz) {
|
|
1646
|
+
const t2 = (query - prevAz2) / Math.max(1e-4, curAz - prevAz2);
|
|
1647
|
+
return mix(prevAlt2, curAlt, t2);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const prevAz = profile.azDeg[n - 1];
|
|
1651
|
+
const prevAlt = profile.altDeg[n - 1];
|
|
1652
|
+
const wrappedQuery = query < firstAz ? query + 360 : query;
|
|
1653
|
+
const t = (wrappedQuery - prevAz) / Math.max(1e-4, firstAz + 360 - prevAz);
|
|
1654
|
+
return mix(prevAlt, firstAlt, t);
|
|
1655
|
+
}
|
|
1656
|
+
function runHorizonDiagnostics(nowMs) {
|
|
1657
|
+
if (nowMs - lastHorizonDiagTs < 1200) return;
|
|
1658
|
+
lastHorizonDiagTs = nowMs;
|
|
1659
|
+
const points = [];
|
|
1660
|
+
const r = 997;
|
|
1661
|
+
const scale = globalUniforms.uScale.value;
|
|
1662
|
+
const aspect = Math.max(1e-4, globalUniforms.uAspect.value);
|
|
1663
|
+
for (let az = 0; az < 360; az += 2) {
|
|
1664
|
+
const altDeg = sampleActiveHorizonAltDeg(az);
|
|
1665
|
+
const azRad = THREE6__namespace.MathUtils.degToRad(az);
|
|
1666
|
+
const altRad = THREE6__namespace.MathUtils.degToRad(altDeg);
|
|
1667
|
+
const rc = Math.cos(altRad);
|
|
1668
|
+
const worldPos = new THREE6__namespace.Vector3(
|
|
1669
|
+
r * rc * Math.cos(azRad),
|
|
1670
|
+
r * Math.sin(altRad),
|
|
1671
|
+
r * rc * Math.sin(azRad)
|
|
1672
|
+
);
|
|
1673
|
+
const p = smartProjectJS(worldPos);
|
|
1674
|
+
if (currentProjection.isClipped(p.z)) continue;
|
|
1675
|
+
const x = p.x * scale / aspect;
|
|
1676
|
+
const y = p.y * scale;
|
|
1677
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
1678
|
+
if (Math.abs(x) > 1) continue;
|
|
1679
|
+
points.push({ x, y });
|
|
1680
|
+
}
|
|
1681
|
+
if (points.length < 16) {
|
|
1682
|
+
console.debug(`[HorizonDiag] insufficient visible horizon samples at fov=${state.fov.toFixed(1)}`);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const binCount = 12;
|
|
1686
|
+
const maxY = new Array(binCount).fill(-Infinity);
|
|
1687
|
+
for (const p of points) {
|
|
1688
|
+
const ax = Math.min(0.999, Math.abs(p.x));
|
|
1689
|
+
const idx = Math.floor(ax * binCount);
|
|
1690
|
+
maxY[idx] = Math.max(maxY[idx], p.y);
|
|
1691
|
+
}
|
|
1692
|
+
const compact = maxY.map((v) => Number.isFinite(v) ? Number(v.toFixed(3)) : null);
|
|
1693
|
+
let dropCount = 0;
|
|
1694
|
+
for (let i = 1; i < binCount; i++) {
|
|
1695
|
+
const prev = maxY[i - 1];
|
|
1696
|
+
const cur = maxY[i];
|
|
1697
|
+
if (!Number.isFinite(prev) || !Number.isFinite(cur)) continue;
|
|
1698
|
+
if (cur < prev - 0.02) dropCount++;
|
|
1699
|
+
}
|
|
1700
|
+
const flatten = groundMaterial?.uniforms?.uZenithFlatten?.value;
|
|
1701
|
+
const blend = currentProjection instanceof BlendedProjection ? currentProjection.getBlend() : -1;
|
|
1702
|
+
console.debug(
|
|
1703
|
+
`[HorizonDiag] fov=${state.fov.toFixed(1)} latDeg=${THREE6__namespace.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)}`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1033
1706
|
function createGround() {
|
|
1034
1707
|
groundGroup.clear();
|
|
1035
1708
|
const radius = 995;
|
|
1036
|
-
const geometry = new
|
|
1709
|
+
const geometry = new THREE6__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
1037
1710
|
const material = createSmartMaterial({
|
|
1038
1711
|
uniforms: {
|
|
1039
|
-
color: { value: new
|
|
1040
|
-
fogColor: { value: new
|
|
1712
|
+
color: { value: new THREE6__namespace.Color(65794) },
|
|
1713
|
+
fogColor: { value: new THREE6__namespace.Color(663098) },
|
|
1714
|
+
uFogIntensity: { value: 0.6 },
|
|
1715
|
+
uFogVisible: { value: 1 },
|
|
1716
|
+
uMinBrightness: { value: 0 },
|
|
1717
|
+
uHorizonMode: { value: 0 },
|
|
1718
|
+
uHorizonPointCount: { value: 0 },
|
|
1719
|
+
uHorizonAzDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1720
|
+
uHorizonAltDeg: { value: new Array(MAX_HORIZON_POINTS).fill(0) },
|
|
1721
|
+
uHorizonRotateRad: { value: 0 },
|
|
1722
|
+
uHorizonRadius: { value: radius },
|
|
1723
|
+
uBaseAltDeg: { value: 3 },
|
|
1724
|
+
uZenithFlatten: { value: 0 }
|
|
1041
1725
|
},
|
|
1042
1726
|
vertexShaderBody: `
|
|
1043
1727
|
varying vec3 vPos;
|
|
1044
1728
|
varying vec3 vWorldPos;
|
|
1729
|
+
varying float vViewDirZ;
|
|
1045
1730
|
void main() {
|
|
1046
1731
|
vPos = position;
|
|
1047
1732
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1048
1733
|
gl_Position = smartProject(mvPosition);
|
|
1049
1734
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1050
1735
|
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
1736
|
+
vViewDirZ = normalize(mvPosition.xyz).z;
|
|
1051
1737
|
}
|
|
1052
1738
|
`,
|
|
1053
1739
|
fragmentShader: `
|
|
1054
1740
|
uniform vec3 color;
|
|
1055
1741
|
uniform vec3 fogColor;
|
|
1742
|
+
uniform float uFogIntensity;
|
|
1743
|
+
uniform float uFogVisible;
|
|
1744
|
+
uniform float uMinBrightness;
|
|
1745
|
+
uniform int uHorizonMode;
|
|
1746
|
+
uniform int uHorizonPointCount;
|
|
1747
|
+
uniform float uHorizonAzDeg[64];
|
|
1748
|
+
uniform float uHorizonAltDeg[64];
|
|
1749
|
+
uniform float uHorizonRotateRad;
|
|
1750
|
+
uniform float uHorizonRadius;
|
|
1751
|
+
uniform float uBaseAltDeg;
|
|
1752
|
+
uniform float uZenithFlatten;
|
|
1056
1753
|
varying vec3 vPos;
|
|
1057
1754
|
varying vec3 vWorldPos;
|
|
1755
|
+
varying float vViewDirZ;
|
|
1756
|
+
|
|
1757
|
+
float samplePolygonalAltDeg(float azDeg) {
|
|
1758
|
+
if (uHorizonPointCount < 2) return 0.0;
|
|
1759
|
+
float z = mod(azDeg, 360.0);
|
|
1760
|
+
if (z < 0.0) z += 360.0;
|
|
1761
|
+
|
|
1762
|
+
float prevAz = uHorizonAzDeg[0];
|
|
1763
|
+
float prevAlt = uHorizonAltDeg[0];
|
|
1764
|
+
for (int i = 1; i < 64; i++) {
|
|
1765
|
+
if (i >= uHorizonPointCount) break;
|
|
1766
|
+
float curAz = uHorizonAzDeg[i];
|
|
1767
|
+
float curAlt = uHorizonAltDeg[i];
|
|
1768
|
+
if (z >= prevAz && z <= curAz) {
|
|
1769
|
+
float t = (z - prevAz) / max(0.0001, curAz - prevAz);
|
|
1770
|
+
return mix(prevAlt, curAlt, t);
|
|
1771
|
+
}
|
|
1772
|
+
prevAz = curAz;
|
|
1773
|
+
prevAlt = curAlt;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
float firstAz = uHorizonAzDeg[0] + 360.0;
|
|
1777
|
+
float firstAlt = uHorizonAltDeg[0];
|
|
1778
|
+
float zw = z;
|
|
1779
|
+
if (zw < uHorizonAzDeg[0]) zw += 360.0;
|
|
1780
|
+
float t = (zw - prevAz) / max(0.0001, firstAz - prevAz);
|
|
1781
|
+
return mix(prevAlt, firstAlt, t);
|
|
1782
|
+
}
|
|
1058
1783
|
|
|
1059
1784
|
void main() {
|
|
1060
1785
|
float alphaMask = getMaskAlpha();
|
|
1061
1786
|
if (alphaMask < 0.01) discard;
|
|
1787
|
+
|
|
1788
|
+
// Keep ground visibility aligned with the active projection clip.
|
|
1789
|
+
float clipZ = -0.1;
|
|
1790
|
+
if (uProjectionType == 1) {
|
|
1791
|
+
clipZ = 0.1;
|
|
1792
|
+
} else if (uProjectionType == 2) {
|
|
1793
|
+
clipZ = mix(-0.1, 0.1, clamp(uBlend, 0.0, 1.0));
|
|
1794
|
+
}
|
|
1795
|
+
if (vViewDirZ > clipZ) discard;
|
|
1062
1796
|
|
|
1063
|
-
// Procedural Horizon (Mountains)
|
|
1064
1797
|
float angle = atan(vPos.z, vPos.x);
|
|
1065
|
-
|
|
1066
|
-
// FBM-like terrain with increased amplitude
|
|
1067
|
-
float h = 0.0;
|
|
1068
|
-
h += sin(angle * 6.0) * 35.0;
|
|
1069
|
-
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1070
|
-
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1071
|
-
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1072
|
-
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1798
|
+
float terrainHeight;
|
|
1073
1799
|
|
|
1074
|
-
|
|
1800
|
+
if (uHorizonMode == 1 && uHorizonPointCount >= 2) {
|
|
1801
|
+
float azDeg = mod(degrees(angle) + 360.0 + degrees(uHorizonRotateRad), 360.0);
|
|
1802
|
+
float altDeg = samplePolygonalAltDeg(azDeg);
|
|
1803
|
+
terrainHeight = uHorizonRadius * sin(radians(altDeg));
|
|
1804
|
+
} else {
|
|
1805
|
+
// Procedural Horizon (Mountains)
|
|
1806
|
+
float h = 0.0;
|
|
1807
|
+
h += sin(angle * 6.0) * 35.0;
|
|
1808
|
+
h += sin(angle * 13.0 + 1.0) * 18.0;
|
|
1809
|
+
h += sin(angle * 29.0 + 2.0) * 8.0;
|
|
1810
|
+
h += sin(angle * 63.0 + 4.0) * 3.0;
|
|
1811
|
+
h += sin(angle * 97.0 + 5.0) * 1.5;
|
|
1812
|
+
terrainHeight = h + 12.0;
|
|
1813
|
+
}
|
|
1814
|
+
float circularHeight = uHorizonRadius * sin(radians(uBaseAltDeg));
|
|
1815
|
+
terrainHeight = mix(terrainHeight, circularHeight, clamp(uZenithFlatten, 0.0, 1.0));
|
|
1075
1816
|
|
|
1076
1817
|
if (vPos.y > terrainHeight) discard;
|
|
1077
1818
|
|
|
1078
1819
|
// Atmospheric rim glow just below terrain peaks
|
|
1079
1820
|
float rimDist = terrainHeight - vPos.y;
|
|
1080
|
-
float rim = exp(-rimDist * 0.15) * 0.4;
|
|
1821
|
+
float rim = exp(-rimDist * 0.15) * 0.4 * uFogVisible;
|
|
1081
1822
|
vec3 rimColor = fogColor * 1.5;
|
|
1082
1823
|
|
|
1083
1824
|
// Atmospheric haze \u2014 stronger near horizon
|
|
1084
1825
|
float fogFactor = smoothstep(-120.0, terrainHeight, vPos.y);
|
|
1085
|
-
vec3 finalCol = mix(color, fogColor, fogFactor *
|
|
1826
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * uFogIntensity * uFogVisible);
|
|
1086
1827
|
|
|
1087
1828
|
// Add rim glow near terrain peaks
|
|
1088
1829
|
finalCol += rimColor * rim;
|
|
1830
|
+
finalCol = max(finalCol, color * uMinBrightness);
|
|
1089
1831
|
|
|
1090
1832
|
gl_FragColor = vec4(finalCol, 1.0);
|
|
1091
1833
|
}
|
|
1092
1834
|
`,
|
|
1093
|
-
side:
|
|
1835
|
+
side: THREE6__namespace.BackSide,
|
|
1094
1836
|
transparent: false,
|
|
1095
1837
|
depthWrite: true,
|
|
1096
1838
|
depthTest: true
|
|
1097
1839
|
});
|
|
1098
|
-
|
|
1840
|
+
groundMaterial = material;
|
|
1841
|
+
const ground = new THREE6__namespace.Mesh(geometry, material);
|
|
1099
1842
|
groundGroup.add(ground);
|
|
1843
|
+
applyGroundTheme(currentConfig);
|
|
1100
1844
|
}
|
|
1845
|
+
let skyBackgroundMesh = null;
|
|
1101
1846
|
let atmosphereMesh = null;
|
|
1847
|
+
let moonMesh = null;
|
|
1848
|
+
let moonGlowMesh = null;
|
|
1849
|
+
let sunDiscMesh = null;
|
|
1850
|
+
let sunHaloMesh = null;
|
|
1851
|
+
let milkyWayMesh = null;
|
|
1852
|
+
let editHoverMesh = null;
|
|
1853
|
+
let editHoverTargetPos = null;
|
|
1854
|
+
let editDropFlash = 0;
|
|
1855
|
+
function createSkyBackground() {
|
|
1856
|
+
const geo = new THREE6__namespace.SphereGeometry(2400, 32, 32);
|
|
1857
|
+
const mat = createSmartMaterial({
|
|
1858
|
+
uniforms: {},
|
|
1859
|
+
vertexShaderBody: `
|
|
1860
|
+
varying vec3 vWorldNormal;
|
|
1861
|
+
void main() {
|
|
1862
|
+
vWorldNormal = normalize(position);
|
|
1863
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
1864
|
+
gl_Position = smartProject(mv);
|
|
1865
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1866
|
+
}
|
|
1867
|
+
`,
|
|
1868
|
+
fragmentShader: `
|
|
1869
|
+
varying vec3 vWorldNormal;
|
|
1870
|
+
void main() {
|
|
1871
|
+
float h = clamp(normalize(vWorldNormal).y, -1.0, 1.0);
|
|
1872
|
+
|
|
1873
|
+
// Scotopic-inspired 5-stop gradient.
|
|
1874
|
+
// Night sky: blue channel ~2.6x red, derived from CIE (x=0.25, y=0.25).
|
|
1875
|
+
vec3 cZenith = vec3(0.010, 0.022, 0.055);
|
|
1876
|
+
vec3 cUpper = vec3(0.015, 0.033, 0.080);
|
|
1877
|
+
vec3 cMid = vec3(0.022, 0.048, 0.108);
|
|
1878
|
+
vec3 cLower = vec3(0.035, 0.072, 0.148);
|
|
1879
|
+
vec3 cHorizon = vec3(0.052, 0.100, 0.190);
|
|
1880
|
+
|
|
1881
|
+
float t1 = smoothstep(0.0, 0.30, h);
|
|
1882
|
+
float t2 = smoothstep(0.3, 0.60, h);
|
|
1883
|
+
float t3 = smoothstep(0.6, 0.85, h);
|
|
1884
|
+
float t4 = smoothstep(0.85, 1.00, h);
|
|
1885
|
+
|
|
1886
|
+
vec3 col = cHorizon;
|
|
1887
|
+
col = mix(col, cLower, t1);
|
|
1888
|
+
col = mix(col, cMid, t2);
|
|
1889
|
+
col = mix(col, cUpper, t3);
|
|
1890
|
+
col = mix(col, cZenith, t4);
|
|
1891
|
+
|
|
1892
|
+
// Rayleigh limb brightening at horizon
|
|
1893
|
+
float limb = exp(-18.0 * abs(h)) * smoothstep(-0.05, 0.06, h);
|
|
1894
|
+
col += vec3(0.012, 0.024, 0.050) * limb;
|
|
1895
|
+
|
|
1896
|
+
// Below ground: fade to near-black
|
|
1897
|
+
float below = smoothstep(-0.04, -0.18, h);
|
|
1898
|
+
col = mix(col, vec3(0.002, 0.003, 0.006), below);
|
|
1899
|
+
|
|
1900
|
+
gl_FragColor = vec4(col, 1.0);
|
|
1901
|
+
}
|
|
1902
|
+
`,
|
|
1903
|
+
transparent: false,
|
|
1904
|
+
depthWrite: false,
|
|
1905
|
+
depthTest: false,
|
|
1906
|
+
side: THREE6__namespace.BackSide
|
|
1907
|
+
});
|
|
1908
|
+
skyBackgroundMesh = new THREE6__namespace.Mesh(geo, mat);
|
|
1909
|
+
skyBackgroundMesh.renderOrder = -2;
|
|
1910
|
+
skyBackgroundMesh.frustumCulled = false;
|
|
1911
|
+
scene.add(skyBackgroundMesh);
|
|
1912
|
+
}
|
|
1102
1913
|
function createAtmosphere() {
|
|
1103
|
-
const geometry = new
|
|
1914
|
+
const geometry = new THREE6__namespace.SphereGeometry(990, 64, 64);
|
|
1104
1915
|
const material = createSmartMaterial({
|
|
1916
|
+
uniforms: {
|
|
1917
|
+
uThemeFogVisible: { value: 1 },
|
|
1918
|
+
uThemeFogTopSin: { value: 0.95 },
|
|
1919
|
+
uThemeFogBottomSin: { value: -1 },
|
|
1920
|
+
uThemeFogIntensity: { value: 1 },
|
|
1921
|
+
uThemeMinBrightness: { value: 0 }
|
|
1922
|
+
},
|
|
1105
1923
|
vertexShaderBody: `
|
|
1106
1924
|
varying vec3 vWorldNormal;
|
|
1107
1925
|
void main() {
|
|
@@ -1117,6 +1935,11 @@ function createEngine({
|
|
|
1117
1935
|
uniform float uAtmDark;
|
|
1118
1936
|
uniform vec3 uColorHorizon;
|
|
1119
1937
|
uniform vec3 uColorZenith;
|
|
1938
|
+
uniform float uThemeFogVisible;
|
|
1939
|
+
uniform float uThemeFogTopSin;
|
|
1940
|
+
uniform float uThemeFogBottomSin;
|
|
1941
|
+
uniform float uThemeFogIntensity;
|
|
1942
|
+
uniform float uThemeMinBrightness;
|
|
1120
1943
|
|
|
1121
1944
|
void main() {
|
|
1122
1945
|
float alphaMask = getMaskAlpha();
|
|
@@ -1130,6 +1953,10 @@ function createEngine({
|
|
|
1130
1953
|
|
|
1131
1954
|
// Non-linear mix for realistic sky falloff
|
|
1132
1955
|
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
1956
|
+
float hazeBand = smoothstep(uThemeFogBottomSin, uThemeFogTopSin, h);
|
|
1957
|
+
float hazeFadeEnd = max(uThemeFogTopSin + 0.001, min(1.0, uThemeFogTopSin + 0.25));
|
|
1958
|
+
hazeBand *= (1.0 - smoothstep(uThemeFogTopSin, hazeFadeEnd, h));
|
|
1959
|
+
float fogTheme = uThemeFogVisible * uThemeFogIntensity;
|
|
1133
1960
|
|
|
1134
1961
|
// 2. Teal tint at mid-altitudes (subtle colour variation)
|
|
1135
1962
|
float midBand = exp(-6.0 * pow(h - 0.3, 2.0));
|
|
@@ -1137,26 +1964,393 @@ function createEngine({
|
|
|
1137
1964
|
|
|
1138
1965
|
// 3. Primary horizon glow band (wider than before)
|
|
1139
1966
|
float horizonBand = exp(-10.0 * abs(h - 0.02));
|
|
1140
|
-
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
1967
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
1141
1968
|
|
|
1142
1969
|
// 4. Warm secondary glow (light pollution / sodium scatter)
|
|
1143
1970
|
float warmGlow = exp(-8.0 * abs(h));
|
|
1144
|
-
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow;
|
|
1971
|
+
skyColor += vec3(0.4, 0.25, 0.15) * warmGlow * 0.3 * uAtmGlow * fogTheme * max(0.15, hazeBand);
|
|
1972
|
+
skyColor = max(skyColor, uColorZenith * (0.2 * uThemeMinBrightness));
|
|
1145
1973
|
|
|
1146
1974
|
gl_FragColor = vec4(skyColor, 1.0);
|
|
1147
1975
|
}
|
|
1148
1976
|
`,
|
|
1149
|
-
side:
|
|
1977
|
+
side: THREE6__namespace.BackSide,
|
|
1150
1978
|
depthWrite: false,
|
|
1151
1979
|
depthTest: true
|
|
1152
1980
|
});
|
|
1153
|
-
const atm = new
|
|
1981
|
+
const atm = new THREE6__namespace.Mesh(geometry, material);
|
|
1154
1982
|
atmosphereMesh = atm;
|
|
1155
1983
|
groundGroup.add(atm);
|
|
1156
1984
|
}
|
|
1157
|
-
|
|
1985
|
+
function createMoon() {
|
|
1986
|
+
const moonDir = new THREE6__namespace.Vector3(-0.38, 0.62, -0.68).normalize();
|
|
1987
|
+
const moonWorldPos = moonDir.clone().multiplyScalar(2e3);
|
|
1988
|
+
const glowGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
1989
|
+
const glowMat = createSmartMaterial({
|
|
1990
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
1991
|
+
vertexShaderBody: `
|
|
1992
|
+
uniform float uMoonSize;
|
|
1993
|
+
varying vec2 vUv;
|
|
1994
|
+
void main() {
|
|
1995
|
+
vUv = uv;
|
|
1996
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1997
|
+
vec4 projected = smartProject(mvPos);
|
|
1998
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
1999
|
+
vec2 offset = position.xy * uMoonSize * uScale * 2.4;
|
|
2000
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2001
|
+
vScreenPos = projected.xy / projected.w;
|
|
2002
|
+
gl_Position = projected;
|
|
2003
|
+
}
|
|
2004
|
+
`,
|
|
2005
|
+
fragmentShader: `
|
|
2006
|
+
varying vec2 vUv;
|
|
2007
|
+
void main() {
|
|
2008
|
+
float alphaMask = getMaskAlpha();
|
|
2009
|
+
if (alphaMask < 0.01) discard;
|
|
2010
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2011
|
+
float d = length(p);
|
|
2012
|
+
if (d > 1.0) discard;
|
|
2013
|
+
float halo = exp(-5.0 * d * d) * 0.07;
|
|
2014
|
+
halo += exp(-2.5 * max(0.0, d - 0.42)) * 0.045;
|
|
2015
|
+
if (halo < 0.003) discard;
|
|
2016
|
+
gl_FragColor = vec4(vec3(0.78, 0.88, 1.0) * halo, halo * alphaMask);
|
|
2017
|
+
}
|
|
2018
|
+
`,
|
|
2019
|
+
transparent: true,
|
|
2020
|
+
depthWrite: false,
|
|
2021
|
+
depthTest: true,
|
|
2022
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2023
|
+
});
|
|
2024
|
+
moonGlowMesh = new THREE6__namespace.Mesh(glowGeo, glowMat);
|
|
2025
|
+
moonGlowMesh.position.copy(moonWorldPos);
|
|
2026
|
+
moonGlowMesh.frustumCulled = false;
|
|
2027
|
+
moonGlowMesh.renderOrder = 2;
|
|
2028
|
+
scene.add(moonGlowMesh);
|
|
2029
|
+
const discGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2030
|
+
const discMat = createSmartMaterial({
|
|
2031
|
+
uniforms: { uMoonSize: { value: 0.082 } },
|
|
2032
|
+
vertexShaderBody: `
|
|
2033
|
+
uniform float uMoonSize;
|
|
2034
|
+
varying vec2 vUv;
|
|
2035
|
+
void main() {
|
|
2036
|
+
vUv = uv;
|
|
2037
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2038
|
+
vec4 projected = smartProject(mvPos);
|
|
2039
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2040
|
+
vec2 offset = position.xy * uMoonSize * uScale;
|
|
2041
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2042
|
+
vScreenPos = projected.xy / projected.w;
|
|
2043
|
+
gl_Position = projected;
|
|
2044
|
+
}
|
|
2045
|
+
`,
|
|
2046
|
+
fragmentShader: `
|
|
2047
|
+
varying vec2 vUv;
|
|
2048
|
+
void main() {
|
|
2049
|
+
float alphaMask = getMaskAlpha();
|
|
2050
|
+
if (alphaMask < 0.01) discard;
|
|
2051
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2052
|
+
float d = length(p);
|
|
2053
|
+
if (d > 1.0) discard;
|
|
2054
|
+
|
|
2055
|
+
float edge = smoothstep(1.0, 0.90, d);
|
|
2056
|
+
|
|
2057
|
+
// Phase: sunlight from upper-right (gibbous moon)
|
|
2058
|
+
vec2 sunDir2D = normalize(vec2(0.55, 0.45));
|
|
2059
|
+
float phaseRaw = dot(normalize(p + vec2(0.0001)), sunDir2D);
|
|
2060
|
+
float lit = smoothstep(-0.18, 0.32, phaseRaw);
|
|
2061
|
+
|
|
2062
|
+
// Limb darkening (classical sqrt law)
|
|
2063
|
+
float cosTheta = sqrt(max(0.001, 1.0 - d * d));
|
|
2064
|
+
float limb = cosTheta * 0.42 + 0.58;
|
|
2065
|
+
|
|
2066
|
+
// Procedural surface texture
|
|
2067
|
+
float angle = atan(p.y, p.x);
|
|
2068
|
+
float r = d;
|
|
2069
|
+
float detail = sin(angle * 5.0 + 2.1) * sin(r * 8.3) * 0.038
|
|
2070
|
+
+ sin(angle * 11.0 - 1.3) * sin(r * 13.0) * 0.022
|
|
2071
|
+
+ sin(angle * 2.0 + 0.8) * (1.0 - r) * 0.055
|
|
2072
|
+
+ sin(angle * 17.0 + r * 6.5) * 0.014
|
|
2073
|
+
+ sin(angle * 23.0 - r * 11.0) * 0.009;
|
|
2074
|
+
|
|
2075
|
+
// Mare (dark maria) patches
|
|
2076
|
+
float mare1 = 1.0 - smoothstep(0.0, 0.30, length(p - vec2(-0.20, 0.22)));
|
|
2077
|
+
float mare2 = 1.0 - smoothstep(0.0, 0.20, length(p - vec2( 0.10, 0.30)));
|
|
2078
|
+
float mare3 = 1.0 - smoothstep(0.0, 0.24, length(p - vec2( 0.17,-0.06)));
|
|
2079
|
+
float mare4 = 1.0 - smoothstep(0.0, 0.14, length(p - vec2(-0.30,-0.20)));
|
|
2080
|
+
float totalMare = clamp(mare1*0.50 + mare2*0.38 + mare3*0.32 + mare4*0.28, 0.0, 0.58);
|
|
2081
|
+
|
|
2082
|
+
vec3 highland = vec3(0.88, 0.85, 0.80);
|
|
2083
|
+
vec3 mareColor = vec3(0.40, 0.39, 0.37);
|
|
2084
|
+
vec3 moonBase = clamp(mix(highland, mareColor, totalMare) + detail, 0.0, 1.0);
|
|
2085
|
+
|
|
2086
|
+
vec3 litSurface = moonBase * limb;
|
|
2087
|
+
vec3 earthshine = vec3(0.038, 0.052, 0.078);
|
|
2088
|
+
vec3 finalColor = mix(earthshine, litSurface, lit);
|
|
2089
|
+
|
|
2090
|
+
gl_FragColor = vec4(finalColor * edge, edge * alphaMask);
|
|
2091
|
+
}
|
|
2092
|
+
`,
|
|
2093
|
+
transparent: true,
|
|
2094
|
+
depthWrite: true,
|
|
2095
|
+
depthTest: true,
|
|
2096
|
+
blending: THREE6__namespace.NormalBlending
|
|
2097
|
+
});
|
|
2098
|
+
moonMesh = new THREE6__namespace.Mesh(discGeo, discMat);
|
|
2099
|
+
moonMesh.position.copy(moonWorldPos);
|
|
2100
|
+
moonMesh.frustumCulled = false;
|
|
2101
|
+
moonMesh.renderOrder = 3;
|
|
2102
|
+
scene.add(moonMesh);
|
|
2103
|
+
}
|
|
2104
|
+
function createSun() {
|
|
2105
|
+
const sunDir = new THREE6__namespace.Vector3(-1, -0.08, 0).normalize();
|
|
2106
|
+
const sunWorldPos = sunDir.clone().multiplyScalar(2e3);
|
|
2107
|
+
const haloGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2108
|
+
const haloMat = createSmartMaterial({
|
|
2109
|
+
uniforms: { uSunHaloSize: { value: 0.46 } },
|
|
2110
|
+
vertexShaderBody: `
|
|
2111
|
+
uniform float uSunHaloSize;
|
|
2112
|
+
varying vec2 vUv;
|
|
2113
|
+
void main() {
|
|
2114
|
+
vUv = uv;
|
|
2115
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2116
|
+
vec4 projected = smartProject(mvPos);
|
|
2117
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2118
|
+
vec2 offset = position.xy * uSunHaloSize * uScale;
|
|
2119
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2120
|
+
vScreenPos = projected.xy / projected.w;
|
|
2121
|
+
gl_Position = projected;
|
|
2122
|
+
}
|
|
2123
|
+
`,
|
|
2124
|
+
fragmentShader: `
|
|
2125
|
+
varying vec2 vUv;
|
|
2126
|
+
void main() {
|
|
2127
|
+
float alphaMask = getMaskAlpha();
|
|
2128
|
+
if (alphaMask < 0.01) discard;
|
|
2129
|
+
|
|
2130
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2131
|
+
float d = length(p);
|
|
2132
|
+
if (d > 1.0) discard;
|
|
2133
|
+
|
|
2134
|
+
// Asymmetric falloff: spread wider horizontally than vertically
|
|
2135
|
+
float asymDist = length(vec2(p.x * 0.55, p.y));
|
|
2136
|
+
|
|
2137
|
+
// Radial glow: warm near centre, fading outward
|
|
2138
|
+
float glow = exp(-2.8 * asymDist * asymDist) * 1.0;
|
|
2139
|
+
glow += exp(-1.0 * asymDist) * 0.35;
|
|
2140
|
+
|
|
2141
|
+
// Crepuscular rays: fan out from bottom, visible above sun centre
|
|
2142
|
+
float rayMask = smoothstep(-0.05, 0.35, p.y);
|
|
2143
|
+
float rayFade = max(0.0, 1.0 - d) * (1.0 - d);
|
|
2144
|
+
float rayAngle = atan(p.x, max(0.0001, p.y)); // angle from vertical
|
|
2145
|
+
float rays = pow(abs(sin(rayAngle * 7.0 + 0.30)), 9.0) * 0.10
|
|
2146
|
+
+ pow(abs(sin(rayAngle * 13.0 - 1.10)), 14.0) * 0.07
|
|
2147
|
+
+ pow(abs(sin(rayAngle * 19.0 + 2.30)), 11.0) * 0.05;
|
|
2148
|
+
rays *= rayMask * rayFade;
|
|
2149
|
+
|
|
2150
|
+
// Colour: white-yellow \u2192 orange \u2192 hot-pink \u2192 purple
|
|
2151
|
+
vec3 cYellow = vec3(1.00, 0.88, 0.52);
|
|
2152
|
+
vec3 cOrange = vec3(1.00, 0.42, 0.10);
|
|
2153
|
+
vec3 cPink = vec3(0.90, 0.22, 0.52);
|
|
2154
|
+
vec3 cPurple = vec3(0.38, 0.12, 0.48);
|
|
2155
|
+
vec3 col = mix(cYellow, cOrange, smoothstep(0.00, 0.40, asymDist));
|
|
2156
|
+
col = mix(col, cPink, smoothstep(0.35, 0.72, asymDist));
|
|
2157
|
+
col = mix(col, cPurple, smoothstep(0.65, 1.00, asymDist));
|
|
2158
|
+
|
|
2159
|
+
float total = (glow + rays) * alphaMask;
|
|
2160
|
+
if (total < 0.005) discard;
|
|
2161
|
+
gl_FragColor = vec4(col * total, total);
|
|
2162
|
+
}
|
|
2163
|
+
`,
|
|
2164
|
+
transparent: true,
|
|
2165
|
+
depthWrite: false,
|
|
2166
|
+
depthTest: true,
|
|
2167
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2168
|
+
});
|
|
2169
|
+
sunHaloMesh = new THREE6__namespace.Mesh(haloGeo, haloMat);
|
|
2170
|
+
sunHaloMesh.position.copy(sunWorldPos);
|
|
2171
|
+
sunHaloMesh.frustumCulled = false;
|
|
2172
|
+
sunHaloMesh.renderOrder = 1;
|
|
2173
|
+
scene.add(sunHaloMesh);
|
|
2174
|
+
const discGeo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2175
|
+
const discMat = createSmartMaterial({
|
|
2176
|
+
uniforms: { uSunSize: { value: 0.09 } },
|
|
2177
|
+
vertexShaderBody: `
|
|
2178
|
+
uniform float uSunSize;
|
|
2179
|
+
varying vec2 vUv;
|
|
2180
|
+
void main() {
|
|
2181
|
+
vUv = uv;
|
|
2182
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2183
|
+
vec4 projected = smartProject(mvPos);
|
|
2184
|
+
if (projected.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2185
|
+
vec2 offset = position.xy * uSunSize * uScale;
|
|
2186
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
2187
|
+
vScreenPos = projected.xy / projected.w;
|
|
2188
|
+
gl_Position = projected;
|
|
2189
|
+
}
|
|
2190
|
+
`,
|
|
2191
|
+
fragmentShader: `
|
|
2192
|
+
varying vec2 vUv;
|
|
2193
|
+
void main() {
|
|
2194
|
+
float alphaMask = getMaskAlpha();
|
|
2195
|
+
if (alphaMask < 0.01) discard;
|
|
2196
|
+
|
|
2197
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2198
|
+
float d = length(p);
|
|
2199
|
+
if (d > 1.0) discard;
|
|
2200
|
+
|
|
2201
|
+
float edge = smoothstep(1.0, 0.86, d);
|
|
2202
|
+
|
|
2203
|
+
// Photosphere limb darkening: bright white core \u2192 orange limb
|
|
2204
|
+
float core = smoothstep(0.28, 0.00, d);
|
|
2205
|
+
float mid = smoothstep(0.68, 0.22, d) * (1.0 - core);
|
|
2206
|
+
float limb = (1.0 - smoothstep(0.70, 1.00, d)) * (1.0 - core - mid);
|
|
2207
|
+
|
|
2208
|
+
vec3 cCore = vec3(1.00, 0.97, 0.88); // hot white
|
|
2209
|
+
vec3 cMid = vec3(1.00, 0.80, 0.38); // yellow
|
|
2210
|
+
vec3 cLimb = vec3(1.00, 0.52, 0.08); // deep orange
|
|
2211
|
+
|
|
2212
|
+
vec3 col = cCore * (core + 0.12) + cMid * mid + cLimb * limb;
|
|
2213
|
+
col = clamp(col, 0.0, 1.5); // allow slight overbright
|
|
2214
|
+
|
|
2215
|
+
gl_FragColor = vec4(col * edge, edge * alphaMask);
|
|
2216
|
+
}
|
|
2217
|
+
`,
|
|
2218
|
+
transparent: true,
|
|
2219
|
+
depthWrite: true,
|
|
2220
|
+
depthTest: true,
|
|
2221
|
+
blending: THREE6__namespace.NormalBlending
|
|
2222
|
+
});
|
|
2223
|
+
sunDiscMesh = new THREE6__namespace.Mesh(discGeo, discMat);
|
|
2224
|
+
sunDiscMesh.position.copy(sunWorldPos);
|
|
2225
|
+
sunDiscMesh.frustumCulled = false;
|
|
2226
|
+
sunDiscMesh.renderOrder = 3;
|
|
2227
|
+
scene.add(sunDiscMesh);
|
|
2228
|
+
}
|
|
2229
|
+
function createMilkyWay() {
|
|
2230
|
+
if (milkyWayMesh) {
|
|
2231
|
+
scene.remove(milkyWayMesh);
|
|
2232
|
+
milkyWayMesh.geometry.dispose();
|
|
2233
|
+
milkyWayMesh.material.dispose();
|
|
2234
|
+
milkyWayMesh = null;
|
|
2235
|
+
}
|
|
2236
|
+
const geo = new THREE6__namespace.PlaneGeometry(1100, 380, 4, 4);
|
|
2237
|
+
const mat = createSmartMaterial({
|
|
2238
|
+
uniforms: {},
|
|
2239
|
+
vertexShaderBody: `
|
|
2240
|
+
varying vec2 vUv;
|
|
2241
|
+
void main() {
|
|
2242
|
+
vUv = uv;
|
|
2243
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
2244
|
+
gl_Position = smartProject(mv);
|
|
2245
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
2246
|
+
}
|
|
2247
|
+
`,
|
|
2248
|
+
fragmentShader: `
|
|
2249
|
+
varying vec2 vUv;
|
|
2250
|
+
|
|
2251
|
+
// --- Noise helpers ---
|
|
2252
|
+
float hash(vec2 p) {
|
|
2253
|
+
p = fract(p * vec2(127.1, 311.7));
|
|
2254
|
+
p += dot(p, p + 19.19);
|
|
2255
|
+
return fract(p.x * p.y);
|
|
2256
|
+
}
|
|
2257
|
+
float vnoise(vec2 p) {
|
|
2258
|
+
vec2 i = floor(p); vec2 f = fract(p);
|
|
2259
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
2260
|
+
return mix(
|
|
2261
|
+
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
|
|
2262
|
+
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), f.x), f.y
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
float fbm(vec2 p) {
|
|
2266
|
+
float v = 0.0; float a = 0.5;
|
|
2267
|
+
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
|
|
2268
|
+
for (int i = 0; i < 7; i++) { v += a * vnoise(p); p = m * p; a *= 0.5; }
|
|
2269
|
+
return v;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
void main() {
|
|
2273
|
+
float alphaMask = getMaskAlpha();
|
|
2274
|
+
if (alphaMask < 0.01) discard;
|
|
2275
|
+
|
|
2276
|
+
vec2 uv = vUv * 2.0 - 1.0; // -1..1 centred
|
|
2277
|
+
|
|
2278
|
+
// Galactic band: tight Gaussian falloff vertically
|
|
2279
|
+
float bandMask = exp(-uv.y * uv.y * 10.0);
|
|
2280
|
+
|
|
2281
|
+
// Warp UV for organic turbulence (two layers of distortion)
|
|
2282
|
+
vec2 q = vec2(fbm(uv * 1.5),
|
|
2283
|
+
fbm(uv * 1.5 + vec2(5.2, 1.3)));
|
|
2284
|
+
vec2 r = vec2(fbm(uv * 1.0 + 4.0 * q + vec2(1.7, 9.2)),
|
|
2285
|
+
fbm(uv * 1.0 + 4.0 * q + vec2(8.3, 2.8)));
|
|
2286
|
+
|
|
2287
|
+
float nebula = fbm(uv * 2.0 + 2.0 * r);
|
|
2288
|
+
float detail = fbm(uv * 5.0 + r * 3.0 + vec2(3.1, 2.7));
|
|
2289
|
+
float fine = fbm(uv * 10.0 + vec2(1.0, 5.0));
|
|
2290
|
+
|
|
2291
|
+
// Base density
|
|
2292
|
+
float density = smoothstep(0.30, 0.80, nebula) * bandMask;
|
|
2293
|
+
density += smoothstep(0.45, 0.85, detail) * bandMask * 0.35;
|
|
2294
|
+
|
|
2295
|
+
// Dust lanes \u2014 dark patches carved into the band
|
|
2296
|
+
float dust = fbm(uv * 3.5 + vec2(11.0, 7.0));
|
|
2297
|
+
density *= (1.0 - smoothstep(0.52, 0.62, dust) * 0.7 * bandMask);
|
|
2298
|
+
|
|
2299
|
+
// Galactic core boost toward horizontal centre
|
|
2300
|
+
float galCore = exp(-uv.x * uv.x * 1.2) * bandMask;
|
|
2301
|
+
|
|
2302
|
+
// --- Color palette ---
|
|
2303
|
+
vec3 deepBlue = vec3(0.10, 0.15, 0.45);
|
|
2304
|
+
vec3 midBlue = vec3(0.25, 0.30, 0.65);
|
|
2305
|
+
vec3 purple = vec3(0.40, 0.20, 0.60);
|
|
2306
|
+
vec3 coreWarm = vec3(0.85, 0.80, 0.65); // warm star-cluster glow
|
|
2307
|
+
vec3 pinkNeb = vec3(0.65, 0.28, 0.50); // emission nebula pink
|
|
2308
|
+
|
|
2309
|
+
float t1 = smoothstep(0.3, 0.7, nebula);
|
|
2310
|
+
float t2 = smoothstep(0.5, 0.8, detail);
|
|
2311
|
+
float t3 = smoothstep(0.55, 0.75, fine);
|
|
2312
|
+
|
|
2313
|
+
vec3 color = mix(deepBlue, midBlue, t1);
|
|
2314
|
+
color = mix(color, purple, t2 * 0.5);
|
|
2315
|
+
color = mix(color, pinkNeb, t3 * 0.25 * bandMask);
|
|
2316
|
+
color += coreWarm * galCore * 0.45 * density;
|
|
2317
|
+
|
|
2318
|
+
// Micro-star field \u2014 denser in the band
|
|
2319
|
+
float starThresh = mix(0.975, 0.940, bandMask);
|
|
2320
|
+
float starSeed = hash(floor(vUv * 500.0));
|
|
2321
|
+
float star = step(starThresh, starSeed);
|
|
2322
|
+
float starBright = hash(floor(vUv * 500.0) + 37.0);
|
|
2323
|
+
color += vec3(0.90, 0.95, 1.0) * star * (0.4 + 0.6 * starBright);
|
|
2324
|
+
density = max(density, star * bandMask * 0.5);
|
|
2325
|
+
|
|
2326
|
+
// Soft edge vignette
|
|
2327
|
+
float ex = smoothstep(0.0, 0.12, vUv.x) * smoothstep(1.0, 0.88, vUv.x);
|
|
2328
|
+
float ey = smoothstep(0.0, 0.18, vUv.y) * smoothstep(1.0, 0.82, vUv.y);
|
|
2329
|
+
|
|
2330
|
+
float alpha = density * ex * ey * alphaMask * 0.80;
|
|
2331
|
+
if (alpha < 0.004) discard;
|
|
2332
|
+
gl_FragColor = vec4(color, alpha);
|
|
2333
|
+
}
|
|
2334
|
+
`,
|
|
2335
|
+
transparent: true,
|
|
2336
|
+
depthWrite: false,
|
|
2337
|
+
depthTest: true,
|
|
2338
|
+
side: THREE6__namespace.DoubleSide,
|
|
2339
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2340
|
+
});
|
|
2341
|
+
milkyWayMesh = new THREE6__namespace.Mesh(geo, mat);
|
|
2342
|
+
const mwDir = new THREE6__namespace.Vector3(-0.62, 0.6, -0.5).normalize();
|
|
2343
|
+
milkyWayMesh.position.copy(mwDir.clone().multiplyScalar(920));
|
|
2344
|
+
milkyWayMesh.lookAt(0, 0, 0);
|
|
2345
|
+
milkyWayMesh.rotateY(Math.PI);
|
|
2346
|
+
milkyWayMesh.frustumCulled = false;
|
|
2347
|
+
milkyWayMesh.renderOrder = 1;
|
|
2348
|
+
scene.add(milkyWayMesh);
|
|
2349
|
+
}
|
|
2350
|
+
const backdropGroup = new THREE6__namespace.Group();
|
|
1158
2351
|
scene.add(backdropGroup);
|
|
1159
|
-
|
|
2352
|
+
let backdropStarsMaterial = null;
|
|
2353
|
+
function createBackdropStars(count = 5e3) {
|
|
1160
2354
|
backdropGroup.clear();
|
|
1161
2355
|
while (backdropGroup.children.length > 0) {
|
|
1162
2356
|
const c = backdropGroup.children[0];
|
|
@@ -1164,7 +2358,7 @@ function createEngine({
|
|
|
1164
2358
|
if (c.geometry) c.geometry.dispose();
|
|
1165
2359
|
if (c.material) c.material.dispose();
|
|
1166
2360
|
}
|
|
1167
|
-
const geometry = new
|
|
2361
|
+
const geometry = new THREE6__namespace.BufferGeometry();
|
|
1168
2362
|
const positions = [];
|
|
1169
2363
|
const sizes = [];
|
|
1170
2364
|
const colors = [];
|
|
@@ -1199,14 +2393,17 @@ function createEngine({
|
|
|
1199
2393
|
}
|
|
1200
2394
|
colors.push(cr, cg, cb);
|
|
1201
2395
|
}
|
|
1202
|
-
geometry.setAttribute("position", new
|
|
1203
|
-
geometry.setAttribute("size", new
|
|
1204
|
-
geometry.setAttribute("color", new
|
|
2396
|
+
geometry.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(positions, 3));
|
|
2397
|
+
geometry.setAttribute("size", new THREE6__namespace.Float32BufferAttribute(sizes, 1));
|
|
2398
|
+
geometry.setAttribute("color", new THREE6__namespace.Float32BufferAttribute(colors, 3));
|
|
1205
2399
|
const material = createSmartMaterial({
|
|
1206
2400
|
uniforms: {
|
|
1207
2401
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1208
2402
|
uScale: globalUniforms.uScale,
|
|
1209
|
-
uTime: globalUniforms.uTime
|
|
2403
|
+
uTime: globalUniforms.uTime,
|
|
2404
|
+
uBackdropGain: { value: 1 },
|
|
2405
|
+
uBackdropEnergy: { value: 2.2 },
|
|
2406
|
+
uBackdropSizeExp: { value: 0.9 }
|
|
1210
2407
|
},
|
|
1211
2408
|
vertexShaderBody: `
|
|
1212
2409
|
attribute float size;
|
|
@@ -1217,6 +2414,9 @@ function createEngine({
|
|
|
1217
2414
|
uniform float uAtmExtinction;
|
|
1218
2415
|
uniform float uAtmTwinkle;
|
|
1219
2416
|
uniform float uTime;
|
|
2417
|
+
uniform float uBackdropGain;
|
|
2418
|
+
uniform float uBackdropEnergy;
|
|
2419
|
+
uniform float uBackdropSizeExp;
|
|
1220
2420
|
|
|
1221
2421
|
void main() {
|
|
1222
2422
|
vec3 nPos = normalize(position);
|
|
@@ -1232,15 +2432,16 @@ function createEngine({
|
|
|
1232
2432
|
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
1233
2433
|
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
1234
2434
|
|
|
1235
|
-
vColor = color *
|
|
2435
|
+
vColor = color * uBackdropEnergy * extinction * horizonFade * scintillation * uBackdropGain;
|
|
1236
2436
|
|
|
1237
2437
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1238
2438
|
gl_Position = smartProject(mvPosition);
|
|
1239
2439
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1240
2440
|
|
|
1241
|
-
float zoomScale = pow(uScale, 0.
|
|
2441
|
+
float zoomScale = pow(max(uScale, 0.0001), uBackdropSizeExp);
|
|
1242
2442
|
float perceptualSize = pow(size, 0.55);
|
|
1243
|
-
|
|
2443
|
+
float sizeGain = mix(0.78, 1.0, uBackdropGain);
|
|
2444
|
+
gl_PointSize = clamp(perceptualSize * zoomScale * sizeGain * 0.5 * pixelRatio * (800.0 / length(mvPosition.xyz)) * horizonFade, 0.5, 20.0);
|
|
1244
2445
|
}
|
|
1245
2446
|
`,
|
|
1246
2447
|
fragmentShader: `
|
|
@@ -1264,27 +2465,84 @@ function createEngine({
|
|
|
1264
2465
|
transparent: true,
|
|
1265
2466
|
depthWrite: false,
|
|
1266
2467
|
depthTest: true,
|
|
1267
|
-
blending:
|
|
2468
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1268
2469
|
});
|
|
1269
|
-
|
|
2470
|
+
backdropStarsMaterial = material;
|
|
2471
|
+
const points = new THREE6__namespace.Points(geometry, material);
|
|
1270
2472
|
points.frustumCulled = false;
|
|
1271
2473
|
backdropGroup.add(points);
|
|
1272
2474
|
}
|
|
2475
|
+
function createEditHoverRing() {
|
|
2476
|
+
const geo = new THREE6__namespace.PlaneGeometry(1, 1);
|
|
2477
|
+
const mat = createSmartMaterial({
|
|
2478
|
+
uniforms: {
|
|
2479
|
+
uRingSize: { value: 0.06 },
|
|
2480
|
+
uRingAlpha: { value: 0 },
|
|
2481
|
+
uRingColor: { value: new THREE6__namespace.Color(0.55, 0.88, 1) }
|
|
2482
|
+
},
|
|
2483
|
+
vertexShaderBody: `
|
|
2484
|
+
uniform float uRingSize;
|
|
2485
|
+
varying vec2 vUv;
|
|
2486
|
+
void main() {
|
|
2487
|
+
vUv = uv;
|
|
2488
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2489
|
+
vec4 proj = smartProject(mvPos);
|
|
2490
|
+
if (proj.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2491
|
+
vec2 offset = position.xy * uRingSize * uScale;
|
|
2492
|
+
proj.xy += offset / vec2(uAspect, 1.0);
|
|
2493
|
+
vScreenPos = proj.xy / proj.w;
|
|
2494
|
+
gl_Position = proj;
|
|
2495
|
+
}
|
|
2496
|
+
`,
|
|
2497
|
+
fragmentShader: `
|
|
2498
|
+
varying vec2 vUv;
|
|
2499
|
+
uniform float uRingAlpha;
|
|
2500
|
+
uniform vec3 uRingColor;
|
|
2501
|
+
void main() {
|
|
2502
|
+
float alphaMask = getMaskAlpha();
|
|
2503
|
+
if (alphaMask < 0.01) discard;
|
|
2504
|
+
vec2 p = vUv * 2.0 - 1.0;
|
|
2505
|
+
float d = length(p);
|
|
2506
|
+
float ring = smoothstep(0.52, 0.62, d) * (1.0 - smoothstep(0.80, 0.92, d));
|
|
2507
|
+
float glow = (1.0 - smoothstep(0.55, 0.98, d)) * 0.18;
|
|
2508
|
+
float a = (ring + glow) * uRingAlpha * alphaMask;
|
|
2509
|
+
if (a < 0.005) discard;
|
|
2510
|
+
gl_FragColor = vec4(uRingColor * (ring * 1.2 + glow), a);
|
|
2511
|
+
}
|
|
2512
|
+
`,
|
|
2513
|
+
transparent: true,
|
|
2514
|
+
depthWrite: false,
|
|
2515
|
+
depthTest: false,
|
|
2516
|
+
side: THREE6__namespace.DoubleSide,
|
|
2517
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2518
|
+
});
|
|
2519
|
+
editHoverMesh = new THREE6__namespace.Mesh(geo, mat);
|
|
2520
|
+
editHoverMesh.renderOrder = 500;
|
|
2521
|
+
editHoverMesh.frustumCulled = false;
|
|
2522
|
+
scene.add(editHoverMesh);
|
|
2523
|
+
}
|
|
2524
|
+
createSkyBackground();
|
|
1273
2525
|
createGround();
|
|
1274
2526
|
createAtmosphere();
|
|
2527
|
+
createMoon();
|
|
2528
|
+
createSun();
|
|
2529
|
+
createMilkyWay();
|
|
1275
2530
|
createBackdropStars();
|
|
1276
|
-
|
|
2531
|
+
createEditHoverRing();
|
|
2532
|
+
const raycaster = new THREE6__namespace.Raycaster();
|
|
1277
2533
|
raycaster.params.Points.threshold = 5;
|
|
1278
|
-
new
|
|
1279
|
-
const root = new
|
|
2534
|
+
new THREE6__namespace.Vector2();
|
|
2535
|
+
const root = new THREE6__namespace.Group();
|
|
1280
2536
|
scene.add(root);
|
|
1281
2537
|
const nodeById = /* @__PURE__ */ new Map();
|
|
1282
2538
|
const starIndexToId = [];
|
|
2539
|
+
const starIdToIndex = /* @__PURE__ */ new Map();
|
|
1283
2540
|
const dynamicLabels = [];
|
|
2541
|
+
const labelManager = new LabelManager();
|
|
1284
2542
|
const hoverLabelMat = createSmartMaterial({
|
|
1285
2543
|
uniforms: {
|
|
1286
2544
|
uMap: { value: null },
|
|
1287
|
-
uSize: { value: new
|
|
2545
|
+
uSize: { value: new THREE6__namespace.Vector2(1, 1) },
|
|
1288
2546
|
uAlpha: { value: 0 },
|
|
1289
2547
|
uAngle: { value: 0 }
|
|
1290
2548
|
},
|
|
@@ -1322,7 +2580,7 @@ function createEngine({
|
|
|
1322
2580
|
depthTest: false
|
|
1323
2581
|
// Always on top of stars
|
|
1324
2582
|
});
|
|
1325
|
-
const hoverLabelMesh = new
|
|
2583
|
+
const hoverLabelMesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), hoverLabelMat);
|
|
1326
2584
|
hoverLabelMesh.visible = false;
|
|
1327
2585
|
hoverLabelMesh.renderOrder = 999;
|
|
1328
2586
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -1346,7 +2604,9 @@ function createEngine({
|
|
|
1346
2604
|
}
|
|
1347
2605
|
nodeById.clear();
|
|
1348
2606
|
starIndexToId.length = 0;
|
|
2607
|
+
starIdToIndex.clear();
|
|
1349
2608
|
dynamicLabels.length = 0;
|
|
2609
|
+
labelManager.clear();
|
|
1350
2610
|
constellationLines = null;
|
|
1351
2611
|
boundaryLines = null;
|
|
1352
2612
|
starPoints = null;
|
|
@@ -1368,49 +2628,132 @@ function createEngine({
|
|
|
1368
2628
|
ctx.textAlign = "center";
|
|
1369
2629
|
ctx.textBaseline = "middle";
|
|
1370
2630
|
ctx.fillText(text, w / 2, h / 2);
|
|
1371
|
-
const tex = new
|
|
1372
|
-
tex.minFilter =
|
|
2631
|
+
const tex = new THREE6__namespace.CanvasTexture(canvas);
|
|
2632
|
+
tex.minFilter = THREE6__namespace.LinearFilter;
|
|
1373
2633
|
return { tex, aspect: w / h };
|
|
1374
2634
|
}
|
|
1375
2635
|
function getPosition(n) {
|
|
1376
2636
|
if (currentConfig?.arrangement) {
|
|
1377
2637
|
const arr = currentConfig.arrangement[n.id];
|
|
1378
2638
|
if (arr) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const y = arr.position[1];
|
|
2639
|
+
const [px, py, pz] = arr.position;
|
|
2640
|
+
if (pz === 0) {
|
|
1382
2641
|
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
2642
|
+
const len3d = Math.sqrt(px * px + py * py);
|
|
2643
|
+
if (len3d < radius * 0.99) {
|
|
2644
|
+
const r_norm = Math.min(1, len3d / radius);
|
|
2645
|
+
const phi = Math.atan2(py, px);
|
|
2646
|
+
const theta = r_norm * (Math.PI / 2);
|
|
2647
|
+
return new THREE6__namespace.Vector3(
|
|
2648
|
+
Math.sin(theta) * Math.cos(phi),
|
|
2649
|
+
Math.cos(theta),
|
|
2650
|
+
Math.sin(theta) * Math.sin(phi)
|
|
2651
|
+
).multiplyScalar(radius);
|
|
2652
|
+
}
|
|
1391
2653
|
}
|
|
1392
|
-
return new
|
|
2654
|
+
return new THREE6__namespace.Vector3(px, py, pz);
|
|
1393
2655
|
}
|
|
1394
2656
|
}
|
|
1395
|
-
return new
|
|
2657
|
+
return new THREE6__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
1396
2658
|
}
|
|
1397
2659
|
function getBoundaryPoint(angle, t, radius) {
|
|
1398
2660
|
const y = 0.05 + t * (1 - 0.05);
|
|
1399
2661
|
const rY = Math.sqrt(1 - y * y);
|
|
1400
2662
|
const x = Math.cos(angle) * rY;
|
|
1401
2663
|
const z = Math.sin(angle) * rY;
|
|
1402
|
-
return new
|
|
2664
|
+
return new THREE6__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
2665
|
+
}
|
|
2666
|
+
function updateChapterLabelAnchors() {
|
|
2667
|
+
if (!starPoints) return;
|
|
2668
|
+
const attr = starPoints.geometry.attributes.position;
|
|
2669
|
+
if (!attr) return;
|
|
2670
|
+
const cameraUpWorld = new THREE6__namespace.Vector3(0, 1, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2671
|
+
const cameraRightWorld = new THREE6__namespace.Vector3(1, 0, 0).applyQuaternion(camera.quaternion).normalize();
|
|
2672
|
+
for (const item of dynamicLabels) {
|
|
2673
|
+
if (item.node.level !== 3) continue;
|
|
2674
|
+
const idx = starIdToIndex.get(item.node.id);
|
|
2675
|
+
if (idx === void 0) continue;
|
|
2676
|
+
const starPos = new THREE6__namespace.Vector3(attr.getX(idx), attr.getY(idx), attr.getZ(idx));
|
|
2677
|
+
const normal = starPos.clone().normalize();
|
|
2678
|
+
const tangent = cameraUpWorld.clone().sub(normal.clone().multiplyScalar(cameraUpWorld.dot(normal)));
|
|
2679
|
+
if (tangent.lengthSq() < 1e-6) {
|
|
2680
|
+
tangent.copy(cameraRightWorld).sub(normal.clone().multiplyScalar(cameraRightWorld.dot(normal)));
|
|
2681
|
+
}
|
|
2682
|
+
if (tangent.lengthSq() < 1e-6) continue;
|
|
2683
|
+
tangent.normalize();
|
|
2684
|
+
const starNorm = item.chapterStarSizeNorm ?? 0.5;
|
|
2685
|
+
const baseSize = item.chapterStarBaseSize ?? 3.5;
|
|
2686
|
+
const altitude = normal.y;
|
|
2687
|
+
const horizonFade = THREE6__namespace.MathUtils.smoothstep(altitude, -0.1, 0.05);
|
|
2688
|
+
const mvPos = starPos.clone().applyMatrix4(camera.matrixWorldInverse);
|
|
2689
|
+
const dist = Math.max(1, mvPos.length());
|
|
2690
|
+
const perceptualSize = Math.pow(baseSize, 0.7);
|
|
2691
|
+
const sizeBoost = 1 + Math.pow(baseSize, 0.5) * 0.08;
|
|
2692
|
+
const pointSize = THREE6__namespace.MathUtils.clamp(
|
|
2693
|
+
perceptualSize * sizeBoost * 20 * globalUniforms.uScale.value * renderer.getPixelRatio() * (2e3 / dist) * horizonFade,
|
|
2694
|
+
1,
|
|
2695
|
+
600
|
|
2696
|
+
);
|
|
2697
|
+
item.chapterGlowRadiusPx = pointSize * 0.6;
|
|
2698
|
+
const viewportH = Math.max(1, renderer.domElement.clientHeight);
|
|
2699
|
+
const fovRad = state.fov * Math.PI / 180;
|
|
2700
|
+
const worldPerPixel = 2 * dist * Math.tan(fovRad * 0.5) / viewportH;
|
|
2701
|
+
let labelHalfDiagPx = 18;
|
|
2702
|
+
const mat = item.obj.material;
|
|
2703
|
+
if (mat instanceof THREE6__namespace.ShaderMaterial && mat.uniforms?.uSize?.value instanceof THREE6__namespace.Vector2) {
|
|
2704
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2705
|
+
const revealT = THREE6__namespace.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2706
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2707
|
+
const fadeOutScale = 1 + (1 - revealT) * 0.06;
|
|
2708
|
+
const zoomTextBoost = THREE6__namespace.MathUtils.lerp(1.4, 0.55, THREE6__namespace.MathUtils.smoothstep(state.fov, 8, 46));
|
|
2709
|
+
const starTextBoost = THREE6__namespace.MathUtils.lerp(0.9, 1.35, starNorm);
|
|
2710
|
+
const scaleMul = zoomTextBoost * starTextBoost * revealScale * fadeOutScale;
|
|
2711
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2712
|
+
const targetX = item.initialScale.x * scaleMul;
|
|
2713
|
+
const targetY = item.initialScale.y * scaleMul;
|
|
2714
|
+
uSize.x = THREE6__namespace.MathUtils.lerp(uSize.x, targetX, 0.2);
|
|
2715
|
+
uSize.y = THREE6__namespace.MathUtils.lerp(uSize.y, targetY, 0.2);
|
|
2716
|
+
const size = mat.uniforms.uSize.value;
|
|
2717
|
+
const pixelH = size.y * viewportH * 0.8;
|
|
2718
|
+
const pixelW = size.x * viewportH * 0.8;
|
|
2719
|
+
labelHalfDiagPx = Math.max(6, Math.max(pixelH, pixelW * 0.45) * 0.5);
|
|
2720
|
+
}
|
|
2721
|
+
const edgeMarginPx = THREE6__namespace.MathUtils.lerp(1, 3, starNorm);
|
|
2722
|
+
const requiredPx = item.chapterGlowRadiusPx + edgeMarginPx + labelHalfDiagPx;
|
|
2723
|
+
const zoomPush = 1 + (1 - THREE6__namespace.MathUtils.smoothstep(state.fov, 8, 30)) * 0.8;
|
|
2724
|
+
const starPush = THREE6__namespace.MathUtils.lerp(0.95, 1.2, starNorm);
|
|
2725
|
+
const offset = THREE6__namespace.MathUtils.clamp(requiredPx * worldPerPixel * zoomPush * starPush, 3, 76);
|
|
2726
|
+
item.obj.position.copy(starPos);
|
|
2727
|
+
item.obj.position.addScaledVector(tangent, offset);
|
|
2728
|
+
item.obj.position.addScaledVector(normal, 2.5);
|
|
2729
|
+
item.chapterStarWorldPos = starPos.clone();
|
|
2730
|
+
}
|
|
2731
|
+
for (const item of dynamicLabels) {
|
|
2732
|
+
const level = item.node.level;
|
|
2733
|
+
if (level !== 2 && level !== 2.5) continue;
|
|
2734
|
+
const mat = item.obj.material;
|
|
2735
|
+
if (!(mat instanceof THREE6__namespace.ShaderMaterial) || !(mat.uniforms?.uSize?.value instanceof THREE6__namespace.Vector2)) continue;
|
|
2736
|
+
const entryFov = 22;
|
|
2737
|
+
const zoomBoost = THREE6__namespace.MathUtils.lerp(1.3, 0.5, THREE6__namespace.MathUtils.smoothstep(state.fov, 8, entryFov));
|
|
2738
|
+
const uAlpha = typeof mat.uniforms.uAlpha?.value === "number" ? mat.uniforms.uAlpha.value : 0;
|
|
2739
|
+
const revealT = THREE6__namespace.MathUtils.smoothstep(uAlpha, 0, 1);
|
|
2740
|
+
const revealScale = 0.82 + 0.28 * revealT;
|
|
2741
|
+
const scaleMul = zoomBoost * revealScale;
|
|
2742
|
+
const uSize = mat.uniforms.uSize.value;
|
|
2743
|
+
uSize.x = THREE6__namespace.MathUtils.lerp(uSize.x, item.initialScale.x * scaleMul, 0.2);
|
|
2744
|
+
uSize.y = THREE6__namespace.MathUtils.lerp(uSize.y, item.initialScale.y * scaleMul, 0.2);
|
|
2745
|
+
}
|
|
1403
2746
|
}
|
|
1404
2747
|
function buildFromModel(model, cfg) {
|
|
1405
2748
|
clearRoot();
|
|
1406
2749
|
bookIdToIndex.clear();
|
|
1407
2750
|
testamentToIndex.clear();
|
|
1408
2751
|
divisionToIndex.clear();
|
|
1409
|
-
scene.background = cfg.background && cfg.background !== "transparent" ? new
|
|
2752
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE6__namespace.Color(cfg.background) : null;
|
|
1410
2753
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
1411
2754
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
1412
2755
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
1413
|
-
|
|
2756
|
+
{
|
|
1414
2757
|
const divMap = /* @__PURE__ */ new Map();
|
|
1415
2758
|
for (const n of laidOut.nodes) {
|
|
1416
2759
|
if (n.level === 2 && n.parent) {
|
|
@@ -1420,7 +2763,7 @@ function createEngine({
|
|
|
1420
2763
|
}
|
|
1421
2764
|
}
|
|
1422
2765
|
for (const [divId, books] of divMap.entries()) {
|
|
1423
|
-
const centroid = new
|
|
2766
|
+
const centroid = new THREE6__namespace.Vector3();
|
|
1424
2767
|
let count = 0;
|
|
1425
2768
|
for (const b of books) {
|
|
1426
2769
|
const p = getPosition(b);
|
|
@@ -1441,20 +2784,25 @@ function createEngine({
|
|
|
1441
2784
|
const starChapterIndices = [];
|
|
1442
2785
|
const starTestamentIndices = [];
|
|
1443
2786
|
const starDivisionIndices = [];
|
|
2787
|
+
const chapterLineCutById = /* @__PURE__ */ new Map();
|
|
2788
|
+
const chapterStarSizeById = /* @__PURE__ */ new Map();
|
|
2789
|
+
const chapterWeightNormById = /* @__PURE__ */ new Map();
|
|
2790
|
+
let minChapterStarSize = Infinity;
|
|
2791
|
+
let maxChapterStarSize = -Infinity;
|
|
1444
2792
|
const SPECTRAL_COLORS = [
|
|
1445
|
-
new
|
|
2793
|
+
new THREE6__namespace.Color(14544639),
|
|
1446
2794
|
// O - Blueish White
|
|
1447
|
-
new
|
|
2795
|
+
new THREE6__namespace.Color(15660287),
|
|
1448
2796
|
// B - White
|
|
1449
|
-
new
|
|
2797
|
+
new THREE6__namespace.Color(16317695),
|
|
1450
2798
|
// A - White
|
|
1451
|
-
new
|
|
2799
|
+
new THREE6__namespace.Color(16777208),
|
|
1452
2800
|
// F - White
|
|
1453
|
-
new
|
|
2801
|
+
new THREE6__namespace.Color(16775406),
|
|
1454
2802
|
// G - Yellowish White
|
|
1455
|
-
new
|
|
2803
|
+
new THREE6__namespace.Color(16773085),
|
|
1456
2804
|
// K - Pale Orange
|
|
1457
|
-
new
|
|
2805
|
+
new THREE6__namespace.Color(16771788)
|
|
1458
2806
|
// M - Light Orange
|
|
1459
2807
|
];
|
|
1460
2808
|
let minWeight = Infinity;
|
|
@@ -1474,15 +2822,38 @@ function createEngine({
|
|
|
1474
2822
|
}
|
|
1475
2823
|
for (const n of laidOut.nodes) {
|
|
1476
2824
|
if (n.level === 3) {
|
|
1477
|
-
const p = getPosition(n);
|
|
1478
|
-
starPositions.push(p.x, p.y, p.z);
|
|
1479
|
-
starIndexToId.push(n.id);
|
|
1480
2825
|
let baseSize = 3.5;
|
|
2826
|
+
let weightNorm = 0;
|
|
1481
2827
|
if (typeof n.weight === "number") {
|
|
1482
|
-
|
|
1483
|
-
|
|
2828
|
+
weightNorm = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
2829
|
+
const sizeExp = cfg.starSizeExponent ?? 4;
|
|
2830
|
+
const sizeScale = cfg.starSizeScale ?? 6;
|
|
2831
|
+
baseSize = Math.pow(weightNorm, sizeExp) * 22 * sizeScale;
|
|
1484
2832
|
}
|
|
2833
|
+
chapterStarSizeById.set(n.id, baseSize);
|
|
2834
|
+
chapterWeightNormById.set(n.id, weightNorm);
|
|
2835
|
+
minChapterStarSize = Math.min(minChapterStarSize, baseSize);
|
|
2836
|
+
maxChapterStarSize = Math.max(maxChapterStarSize, baseSize);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (!Number.isFinite(minChapterStarSize)) {
|
|
2840
|
+
minChapterStarSize = 1;
|
|
2841
|
+
maxChapterStarSize = 2;
|
|
2842
|
+
} else if (minChapterStarSize === maxChapterStarSize) {
|
|
2843
|
+
maxChapterStarSize = minChapterStarSize + 1;
|
|
2844
|
+
}
|
|
2845
|
+
for (const n of laidOut.nodes) {
|
|
2846
|
+
if (n.level === 3) {
|
|
2847
|
+
const p = getPosition(n);
|
|
2848
|
+
starPositions.push(p.x, p.y, p.z);
|
|
2849
|
+
starIdToIndex.set(n.id, starIndexToId.length);
|
|
2850
|
+
starIndexToId.push(n.id);
|
|
2851
|
+
const baseSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
1485
2852
|
starSizes.push(baseSize);
|
|
2853
|
+
chapterLineCutById.set(
|
|
2854
|
+
n.id,
|
|
2855
|
+
THREE6__namespace.MathUtils.clamp(2.5 + baseSize * 0.45, 3, 40)
|
|
2856
|
+
);
|
|
1486
2857
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
1487
2858
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
1488
2859
|
starColors.push(c.r, c.g, c.b);
|
|
@@ -1533,8 +2904,11 @@ function createEngine({
|
|
|
1533
2904
|
let baseScale = 0.05;
|
|
1534
2905
|
if (n.level === 1) baseScale = 0.08;
|
|
1535
2906
|
else if (n.level === 2) baseScale = 0.04;
|
|
1536
|
-
else if (n.level === 3)
|
|
1537
|
-
|
|
2907
|
+
else if (n.level === 3) {
|
|
2908
|
+
const wn2 = chapterWeightNormById.get(n.id) ?? 0;
|
|
2909
|
+
baseScale = THREE6__namespace.MathUtils.lerp(0.019, 0.039, wn2);
|
|
2910
|
+
}
|
|
2911
|
+
const size = new THREE6__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1538
2912
|
const mat = createSmartMaterial({
|
|
1539
2913
|
uniforms: {
|
|
1540
2914
|
uMap: { value: texRes.tex },
|
|
@@ -1573,39 +2947,59 @@ function createEngine({
|
|
|
1573
2947
|
`,
|
|
1574
2948
|
transparent: true,
|
|
1575
2949
|
depthWrite: false,
|
|
1576
|
-
depthTest: true
|
|
2950
|
+
depthTest: n.level === 3 ? false : true
|
|
1577
2951
|
});
|
|
1578
|
-
const mesh = new
|
|
2952
|
+
const mesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), mat);
|
|
1579
2953
|
let p = getPosition(n);
|
|
1580
2954
|
if (n.level === 1) {
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
2955
|
+
if (cfg.arrangement?.[n.id]) {
|
|
2956
|
+
const arr = cfg.arrangement[n.id];
|
|
2957
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
2958
|
+
} else {
|
|
2959
|
+
if (divisionPositions.has(n.id)) {
|
|
2960
|
+
p.copy(divisionPositions.get(n.id));
|
|
2961
|
+
}
|
|
2962
|
+
const r = layoutCfg.radius * 0.95;
|
|
2963
|
+
const angle = Math.atan2(p.z, p.x);
|
|
2964
|
+
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1583
2965
|
}
|
|
1584
|
-
const r = layoutCfg.radius * 0.95;
|
|
1585
|
-
const angle = Math.atan2(p.z, p.x);
|
|
1586
|
-
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1587
2966
|
} else if (n.level === 3) {
|
|
1588
|
-
|
|
1589
|
-
|
|
2967
|
+
const starSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
2968
|
+
const starNorm = THREE6__namespace.MathUtils.clamp(
|
|
2969
|
+
(starSize - minChapterStarSize) / (maxChapterStarSize - minChapterStarSize),
|
|
2970
|
+
0,
|
|
2971
|
+
1
|
|
2972
|
+
);
|
|
2973
|
+
const radialOffset = THREE6__namespace.MathUtils.lerp(16, 46, starNorm);
|
|
2974
|
+
p.addScaledVector(p.clone().normalize(), radialOffset);
|
|
1590
2975
|
}
|
|
1591
2976
|
mesh.position.set(p.x, p.y, p.z);
|
|
1592
2977
|
mesh.scale.set(size.x, size.y, 1);
|
|
1593
2978
|
mesh.frustumCulled = false;
|
|
1594
2979
|
mesh.userData = { id: n.id };
|
|
1595
2980
|
root.add(mesh);
|
|
1596
|
-
|
|
2981
|
+
const wn = n.level === 3 ? chapterWeightNormById.get(n.id) ?? 0 : 0;
|
|
2982
|
+
const chapterMaxFovBias = n.level === 3 ? THREE6__namespace.MathUtils.lerp(-4, 8, wn) : 0;
|
|
2983
|
+
dynamicLabels.push({
|
|
2984
|
+
obj: mesh,
|
|
2985
|
+
node: n,
|
|
2986
|
+
initialScale: size.clone(),
|
|
2987
|
+
maxFovBias: chapterMaxFovBias,
|
|
2988
|
+
chapterStarSizeNorm: n.level === 3 ? wn : void 0,
|
|
2989
|
+
chapterStarBaseSize: n.level === 3 ? chapterStarSizeById.get(n.id) ?? 3.5 : void 0
|
|
2990
|
+
});
|
|
1597
2991
|
}
|
|
1598
2992
|
}
|
|
1599
2993
|
}
|
|
1600
|
-
const starGeo = new
|
|
1601
|
-
starGeo.setAttribute("position", new
|
|
1602
|
-
starGeo.setAttribute("size", new
|
|
1603
|
-
starGeo.setAttribute("color", new
|
|
1604
|
-
starGeo.setAttribute("phase", new
|
|
1605
|
-
starGeo.setAttribute("bookIndex", new
|
|
1606
|
-
starGeo.setAttribute("chapterIndex", new
|
|
1607
|
-
starGeo.setAttribute("testamentIndex", new
|
|
1608
|
-
starGeo.setAttribute("divisionIndex", new
|
|
2994
|
+
const starGeo = new THREE6__namespace.BufferGeometry();
|
|
2995
|
+
starGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(starPositions, 3));
|
|
2996
|
+
starGeo.setAttribute("size", new THREE6__namespace.Float32BufferAttribute(starSizes, 1));
|
|
2997
|
+
starGeo.setAttribute("color", new THREE6__namespace.Float32BufferAttribute(starColors, 3));
|
|
2998
|
+
starGeo.setAttribute("phase", new THREE6__namespace.Float32BufferAttribute(starPhases, 1));
|
|
2999
|
+
starGeo.setAttribute("bookIndex", new THREE6__namespace.Float32BufferAttribute(starBookIndices, 1));
|
|
3000
|
+
starGeo.setAttribute("chapterIndex", new THREE6__namespace.Float32BufferAttribute(starChapterIndices, 1));
|
|
3001
|
+
starGeo.setAttribute("testamentIndex", new THREE6__namespace.Float32BufferAttribute(starTestamentIndices, 1));
|
|
3002
|
+
starGeo.setAttribute("divisionIndex", new THREE6__namespace.Float32BufferAttribute(starDivisionIndices, 1));
|
|
1609
3003
|
const starMat = createSmartMaterial({
|
|
1610
3004
|
uniforms: {
|
|
1611
3005
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -1614,7 +3008,7 @@ function createEngine({
|
|
|
1614
3008
|
uActiveBookIndex: { value: -1 },
|
|
1615
3009
|
uOrderRevealStrength: { value: 0 },
|
|
1616
3010
|
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1617
|
-
uPulseParams: { value: new
|
|
3011
|
+
uPulseParams: { value: new THREE6__namespace.Vector3(
|
|
1618
3012
|
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1619
3013
|
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1620
3014
|
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
@@ -1635,6 +3029,7 @@ function createEngine({
|
|
|
1635
3029
|
attribute float divisionIndex;
|
|
1636
3030
|
|
|
1637
3031
|
varying vec3 vColor;
|
|
3032
|
+
varying float vSize;
|
|
1638
3033
|
uniform float pixelRatio;
|
|
1639
3034
|
|
|
1640
3035
|
uniform float uTime;
|
|
@@ -1707,41 +3102,159 @@ function createEngine({
|
|
|
1707
3102
|
gl_Position = smartProject(mvPosition);
|
|
1708
3103
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
1709
3104
|
|
|
1710
|
-
float sizeBoost = 1.0 + activePulse * 0.
|
|
1711
|
-
|
|
1712
|
-
|
|
3105
|
+
float sizeBoost = 1.0 + activePulse * 0.15;
|
|
3106
|
+
// pow(size, 0.7) is gentler compression than 0.55 \u2014 preserves more of
|
|
3107
|
+
// the aggressive JS curve so large stars stay visually dominant.
|
|
3108
|
+
float perceptualSize = pow(size, 0.7);
|
|
3109
|
+
gl_PointSize = clamp((perceptualSize * sizeBoost * 20.0) * uScale * pixelRatio * (2000.0 / length(mvPosition.xyz)) * horizonFade, 1.0, 600.0);
|
|
3110
|
+
vSize = gl_PointSize;
|
|
1713
3111
|
}
|
|
1714
3112
|
`,
|
|
1715
3113
|
fragmentShader: `
|
|
1716
|
-
varying vec3 vColor;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
float
|
|
3114
|
+
varying vec3 vColor;
|
|
3115
|
+
varying float vSize;
|
|
3116
|
+
void main() {
|
|
3117
|
+
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
3118
|
+
float d = length(coord) * 2.0;
|
|
3119
|
+
if (d > 1.0) discard;
|
|
3120
|
+
|
|
3121
|
+
float alphaMask = getMaskAlpha();
|
|
3122
|
+
if (alphaMask < 0.01) discard;
|
|
3123
|
+
|
|
3124
|
+
// --- Multi-layer Gaussian star model ---
|
|
3125
|
+
// Tight white-hot core
|
|
3126
|
+
float core = exp(-d * d * 9.0);
|
|
3127
|
+
// Broader coloured inner halo
|
|
3128
|
+
float innerGlow = exp(-d * d * 3.0) * 0.45;
|
|
3129
|
+
// Wide faint bloom that fades smoothly to the disc edge
|
|
3130
|
+
float outerBloom = max(0.0, 1.0 - d * d) * 0.10;
|
|
3131
|
+
|
|
3132
|
+
float k = core + innerGlow + outerBloom;
|
|
3133
|
+
|
|
3134
|
+
// White-hot centre \u2192 spectral colour at the halo
|
|
3135
|
+
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.88);
|
|
3136
|
+
|
|
3137
|
+
// --- Size-dependent diffraction spikes ---
|
|
3138
|
+
// Only appear on larger (brighter) stars, matching real optics.
|
|
3139
|
+
float spikeFactor = smoothstep(10.0, 24.0, vSize);
|
|
3140
|
+
float spikeH = exp(-coord.y * coord.y * 180.0) * exp(-abs(coord.x) * 6.0);
|
|
3141
|
+
float spikeV = exp(-coord.x * coord.x * 180.0) * exp(-abs(coord.y) * 6.0);
|
|
3142
|
+
float spikes = (spikeH + spikeV) * 0.18 * spikeFactor;
|
|
1729
3143
|
|
|
1730
|
-
|
|
1731
|
-
vec3 finalColor = mix(vColor, vec3(1.0), core * 0.7);
|
|
1732
|
-
gl_FragColor = vec4(finalColor * k * alphaMask, 1.0);
|
|
3144
|
+
gl_FragColor = vec4(finalColor * (k + spikes) * alphaMask, 1.0);
|
|
1733
3145
|
}
|
|
1734
3146
|
`,
|
|
1735
3147
|
transparent: true,
|
|
1736
3148
|
depthWrite: false,
|
|
1737
3149
|
depthTest: true,
|
|
1738
|
-
blending:
|
|
3150
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1739
3151
|
});
|
|
1740
|
-
starPoints = new
|
|
3152
|
+
starPoints = new THREE6__namespace.Points(starGeo, starMat);
|
|
1741
3153
|
starPoints.frustumCulled = false;
|
|
1742
3154
|
root.add(starPoints);
|
|
1743
3155
|
const linePoints = [];
|
|
3156
|
+
const lineWeights = [];
|
|
3157
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1744
3158
|
const bookMap = /* @__PURE__ */ new Map();
|
|
3159
|
+
const parseBookKeyFromChapterId = (id) => {
|
|
3160
|
+
if (!id) return null;
|
|
3161
|
+
const parts = id.split(":");
|
|
3162
|
+
if (parts.length < 3 || parts[0] !== "C") return null;
|
|
3163
|
+
return parts[1] || null;
|
|
3164
|
+
};
|
|
3165
|
+
const weightScaleFromLabel = (weight) => {
|
|
3166
|
+
if (weight === "thin") return 0.65;
|
|
3167
|
+
if (weight === "bold") return 1.6;
|
|
3168
|
+
return 1;
|
|
3169
|
+
};
|
|
3170
|
+
const edgeKey = (aNodeId, bNodeId) => aNodeId < bNodeId ? `${aNodeId}|${bNodeId}` : `${bNodeId}|${aNodeId}`;
|
|
3171
|
+
const addTruncatedSegment = (aNodeId, bNodeId, weightScale) => {
|
|
3172
|
+
if (aNodeId === bNodeId) return;
|
|
3173
|
+
const k = edgeKey(aNodeId, bNodeId);
|
|
3174
|
+
if (seenEdges.has(k)) return;
|
|
3175
|
+
seenEdges.add(k);
|
|
3176
|
+
const aNode = nodeById.get(aNodeId);
|
|
3177
|
+
const bNode = nodeById.get(bNodeId);
|
|
3178
|
+
if (!aNode || !bNode) return;
|
|
3179
|
+
const p1 = getPosition(aNode);
|
|
3180
|
+
const p2 = getPosition(bNode);
|
|
3181
|
+
const dir = new THREE6__namespace.Vector3().subVectors(p2, p1);
|
|
3182
|
+
const len = dir.length();
|
|
3183
|
+
if (len < 1e-3) return;
|
|
3184
|
+
dir.divideScalar(len);
|
|
3185
|
+
let cutA = chapterLineCutById.get(aNodeId) ?? 4;
|
|
3186
|
+
let cutB = chapterLineCutById.get(bNodeId) ?? 4;
|
|
3187
|
+
const maxTotalCut = len * 0.8;
|
|
3188
|
+
const totalCut = cutA + cutB;
|
|
3189
|
+
if (totalCut > maxTotalCut && totalCut > 0) {
|
|
3190
|
+
const scale = maxTotalCut / totalCut;
|
|
3191
|
+
cutA *= scale;
|
|
3192
|
+
cutB *= scale;
|
|
3193
|
+
}
|
|
3194
|
+
const a = p1.clone().addScaledVector(dir, cutA);
|
|
3195
|
+
const b = p2.clone().addScaledVector(dir, -cutB);
|
|
3196
|
+
linePoints.push(a.x, a.y, a.z);
|
|
3197
|
+
linePoints.push(b.x, b.y, b.z);
|
|
3198
|
+
lineWeights.push(weightScale);
|
|
3199
|
+
};
|
|
3200
|
+
const customBooks = /* @__PURE__ */ new Set();
|
|
3201
|
+
const rawConstellations = cfg.constellations && Array.isArray(cfg.constellations.constellations) ? cfg.constellations.constellations : [];
|
|
3202
|
+
for (const c of rawConstellations) {
|
|
3203
|
+
const linePaths = Array.isArray(c?.linePaths) ? c.linePaths : [];
|
|
3204
|
+
const lineSegments = Array.isArray(c?.lineSegments) ? c.lineSegments : [];
|
|
3205
|
+
if (linePaths.length === 0 && lineSegments.length === 0) continue;
|
|
3206
|
+
const anchorBookKey = parseBookKeyFromChapterId(c?.anchors?.[0]);
|
|
3207
|
+
if (anchorBookKey) customBooks.add(anchorBookKey);
|
|
3208
|
+
for (const segDef of lineSegments) {
|
|
3209
|
+
let from;
|
|
3210
|
+
let to;
|
|
3211
|
+
let weightLabel;
|
|
3212
|
+
if (Array.isArray(segDef)) {
|
|
3213
|
+
const raw = segDef;
|
|
3214
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3215
|
+
weightLabel = raw[0];
|
|
3216
|
+
from = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3217
|
+
to = typeof raw[2] === "string" ? raw[2] : void 0;
|
|
3218
|
+
} else {
|
|
3219
|
+
from = typeof raw[0] === "string" ? raw[0] : void 0;
|
|
3220
|
+
to = typeof raw[1] === "string" ? raw[1] : void 0;
|
|
3221
|
+
}
|
|
3222
|
+
} else if (segDef) {
|
|
3223
|
+
from = typeof segDef.from === "string" ? segDef.from : void 0;
|
|
3224
|
+
to = typeof segDef.to === "string" ? segDef.to : void 0;
|
|
3225
|
+
weightLabel = typeof segDef.weight === "string" ? segDef.weight : void 0;
|
|
3226
|
+
}
|
|
3227
|
+
if (!from || !to) continue;
|
|
3228
|
+
const k1 = parseBookKeyFromChapterId(from);
|
|
3229
|
+
const k2 = parseBookKeyFromChapterId(to);
|
|
3230
|
+
if (k1) customBooks.add(k1);
|
|
3231
|
+
if (k2) customBooks.add(k2);
|
|
3232
|
+
addTruncatedSegment(from, to, weightScaleFromLabel(weightLabel));
|
|
3233
|
+
}
|
|
3234
|
+
for (const pathDef of linePaths) {
|
|
3235
|
+
let nodes = [];
|
|
3236
|
+
let weightLabel = void 0;
|
|
3237
|
+
if (Array.isArray(pathDef)) {
|
|
3238
|
+
const raw = pathDef;
|
|
3239
|
+
if (typeof raw[0] === "string" && (raw[0] === "thin" || raw[0] === "bold" || raw[0] === "normal")) {
|
|
3240
|
+
weightLabel = raw[0];
|
|
3241
|
+
nodes = raw.slice(1).filter((v) => typeof v === "string");
|
|
3242
|
+
} else {
|
|
3243
|
+
nodes = raw.filter((v) => typeof v === "string");
|
|
3244
|
+
}
|
|
3245
|
+
} else if (pathDef && Array.isArray(pathDef.nodes)) {
|
|
3246
|
+
nodes = pathDef.nodes.filter((v) => typeof v === "string");
|
|
3247
|
+
weightLabel = typeof pathDef.weight === "string" ? pathDef.weight : void 0;
|
|
3248
|
+
}
|
|
3249
|
+
if (nodes.length < 2) continue;
|
|
3250
|
+
const inferredBookKey = parseBookKeyFromChapterId(nodes[0]);
|
|
3251
|
+
if (inferredBookKey) customBooks.add(inferredBookKey);
|
|
3252
|
+
const w = weightScaleFromLabel(weightLabel);
|
|
3253
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
3254
|
+
addTruncatedSegment(nodes[i], nodes[i + 1], w);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
1745
3258
|
for (const n of laidOut.nodes) {
|
|
1746
3259
|
if (n.level === 3 && n.parent) {
|
|
1747
3260
|
const list = bookMap.get(n.parent) ?? [];
|
|
@@ -1752,24 +3265,27 @@ function createEngine({
|
|
|
1752
3265
|
for (const chapters of bookMap.values()) {
|
|
1753
3266
|
chapters.sort((a, b) => (a.meta?.chapter || 0) - (b.meta?.chapter || 0));
|
|
1754
3267
|
if (chapters.length < 2) continue;
|
|
3268
|
+
const bookKey = chapters[0]?.meta?.bookKey ?? null;
|
|
3269
|
+
if (bookKey && customBooks.has(bookKey)) continue;
|
|
1755
3270
|
for (let i = 0; i < chapters.length - 1; i++) {
|
|
1756
3271
|
const c1 = chapters[i];
|
|
1757
3272
|
const c2 = chapters[i + 1];
|
|
1758
3273
|
if (!c1 || !c2) continue;
|
|
1759
|
-
|
|
1760
|
-
const p2 = getPosition(c2);
|
|
1761
|
-
linePoints.push(p1.x, p1.y, p1.z);
|
|
1762
|
-
linePoints.push(p2.x, p2.y, p2.z);
|
|
3274
|
+
addTruncatedSegment(c1.id, c2.id, 1);
|
|
1763
3275
|
}
|
|
1764
3276
|
}
|
|
1765
3277
|
if (linePoints.length > 0) {
|
|
1766
3278
|
const quadPositions = [];
|
|
1767
3279
|
const quadUvs = [];
|
|
3280
|
+
const quadLineWeight = [];
|
|
3281
|
+
const quadSegmentIndex = [];
|
|
1768
3282
|
const quadIndices = [];
|
|
1769
3283
|
const lineWidth = 8;
|
|
3284
|
+
const segmentCount = linePoints.length / 6;
|
|
1770
3285
|
for (let i = 0; i < linePoints.length; i += 6) {
|
|
1771
3286
|
const ax = linePoints[i], ay = linePoints[i + 1], az = linePoints[i + 2];
|
|
1772
3287
|
const bx = linePoints[i + 3], by = linePoints[i + 4], bz = linePoints[i + 5];
|
|
3288
|
+
const segIndex = i / 6;
|
|
1773
3289
|
const dx = bx - ax, dy = by - ay, dz = bz - az;
|
|
1774
3290
|
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1775
3291
|
if (len < 1e-3) continue;
|
|
@@ -1794,23 +3310,36 @@ function createEngine({
|
|
|
1794
3310
|
quadUvs.push(1, -1);
|
|
1795
3311
|
quadPositions.push(bx + px * hw, by + py * hw, bz + pz * hw);
|
|
1796
3312
|
quadUvs.push(1, 1);
|
|
3313
|
+
const w = lineWeights[segIndex] ?? 1;
|
|
3314
|
+
quadLineWeight.push(w, w, w, w);
|
|
3315
|
+
quadSegmentIndex.push(segIndex, segIndex, segIndex, segIndex);
|
|
1797
3316
|
quadIndices.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx + 1, baseIdx + 3, baseIdx + 2);
|
|
1798
3317
|
}
|
|
1799
|
-
const lineGeo = new
|
|
1800
|
-
lineGeo.setAttribute("position", new
|
|
1801
|
-
lineGeo.setAttribute("lineUv", new
|
|
3318
|
+
const lineGeo = new THREE6__namespace.BufferGeometry();
|
|
3319
|
+
lineGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(quadPositions, 3));
|
|
3320
|
+
lineGeo.setAttribute("lineUv", new THREE6__namespace.Float32BufferAttribute(quadUvs, 2));
|
|
3321
|
+
lineGeo.setAttribute("lineWeight", new THREE6__namespace.Float32BufferAttribute(quadLineWeight, 1));
|
|
3322
|
+
lineGeo.setAttribute("segmentIndex", new THREE6__namespace.Float32BufferAttribute(quadSegmentIndex, 1));
|
|
1802
3323
|
lineGeo.setIndex(quadIndices);
|
|
1803
3324
|
const lineMat = createSmartMaterial({
|
|
1804
3325
|
uniforms: {
|
|
1805
|
-
color: { value: new
|
|
3326
|
+
color: { value: new THREE6__namespace.Color(11193599) },
|
|
1806
3327
|
uLineWidth: { value: 1.5 },
|
|
1807
|
-
uGlowIntensity: { value: 0.3 }
|
|
3328
|
+
uGlowIntensity: { value: 0.3 },
|
|
3329
|
+
uReveal: { value: 0 },
|
|
3330
|
+
uSegmentCount: { value: Math.max(1, segmentCount) }
|
|
1808
3331
|
},
|
|
1809
3332
|
vertexShaderBody: `
|
|
1810
3333
|
attribute vec2 lineUv;
|
|
3334
|
+
attribute float lineWeight;
|
|
3335
|
+
attribute float segmentIndex;
|
|
1811
3336
|
varying vec2 vLineUv;
|
|
3337
|
+
varying float vLineWeight;
|
|
3338
|
+
varying float vSegmentIndex;
|
|
1812
3339
|
void main() {
|
|
1813
3340
|
vLineUv = lineUv;
|
|
3341
|
+
vLineWeight = lineWeight;
|
|
3342
|
+
vSegmentIndex = segmentIndex;
|
|
1814
3343
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
1815
3344
|
gl_Position = smartProject(mvPosition);
|
|
1816
3345
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
@@ -1820,32 +3349,53 @@ function createEngine({
|
|
|
1820
3349
|
uniform vec3 color;
|
|
1821
3350
|
uniform float uLineWidth;
|
|
1822
3351
|
uniform float uGlowIntensity;
|
|
3352
|
+
uniform float uReveal;
|
|
3353
|
+
uniform float uSegmentCount;
|
|
1823
3354
|
varying vec2 vLineUv;
|
|
3355
|
+
varying float vLineWeight;
|
|
3356
|
+
varying float vSegmentIndex;
|
|
1824
3357
|
void main() {
|
|
1825
3358
|
float alphaMask = getMaskAlpha();
|
|
1826
3359
|
if (alphaMask < 0.01) discard;
|
|
1827
3360
|
|
|
3361
|
+
// Progressive line draw tuned closer to Stellarium feel:
|
|
3362
|
+
// - eased global reveal
|
|
3363
|
+
// - sequential segment staggering with slight overlap
|
|
3364
|
+
// - smooth growth of each segment endpoint
|
|
3365
|
+
float reveal = smoothstep(0.0, 1.0, uReveal);
|
|
3366
|
+
float segCount = max(uSegmentCount, 1.0);
|
|
3367
|
+
float segStart = vSegmentIndex / segCount;
|
|
3368
|
+
float segSpan = (1.25 / segCount) + 0.04;
|
|
3369
|
+
float localReveal = clamp((reveal - segStart) / segSpan, 0.0, 1.0);
|
|
3370
|
+
localReveal = smoothstep(0.0, 1.0, localReveal);
|
|
3371
|
+
|
|
3372
|
+
// Keep fragment only when x is before the animated endpoint.
|
|
3373
|
+
float endpointMask = 1.0 - smoothstep(localReveal - 0.03, localReveal + 0.02, vLineUv.x);
|
|
3374
|
+
// Fade in segment brightness as it begins drawing.
|
|
3375
|
+
float drawMask = endpointMask * smoothstep(0.0, 0.08, localReveal);
|
|
3376
|
+
if (drawMask < 0.001) discard;
|
|
3377
|
+
|
|
1828
3378
|
float dist = abs(vLineUv.y);
|
|
1829
3379
|
|
|
1830
3380
|
// Anti-aliased core line
|
|
1831
|
-
float hw = uLineWidth * 0.05;
|
|
3381
|
+
float hw = (uLineWidth * vLineWeight) * 0.05;
|
|
1832
3382
|
float base = smoothstep(hw + 0.08, hw - 0.08, dist);
|
|
1833
3383
|
|
|
1834
3384
|
// Soft glow extending outward
|
|
1835
|
-
float glow = (1.0 - dist) * uGlowIntensity;
|
|
3385
|
+
float glow = (1.0 - dist) * uGlowIntensity * vLineWeight;
|
|
1836
3386
|
|
|
1837
3387
|
float alpha = max(glow, base);
|
|
1838
3388
|
if (alpha < 0.005) discard;
|
|
1839
3389
|
|
|
1840
|
-
gl_FragColor = vec4(color, alpha * alphaMask);
|
|
3390
|
+
gl_FragColor = vec4(color, alpha * alphaMask * drawMask);
|
|
1841
3391
|
}
|
|
1842
3392
|
`,
|
|
1843
3393
|
transparent: true,
|
|
1844
3394
|
depthWrite: false,
|
|
1845
|
-
blending:
|
|
1846
|
-
side:
|
|
3395
|
+
blending: THREE6__namespace.AdditiveBlending,
|
|
3396
|
+
side: THREE6__namespace.DoubleSide
|
|
1847
3397
|
});
|
|
1848
|
-
constellationLines = new
|
|
3398
|
+
constellationLines = new THREE6__namespace.Mesh(lineGeo, lineMat);
|
|
1849
3399
|
constellationLines.frustumCulled = false;
|
|
1850
3400
|
root.add(constellationLines);
|
|
1851
3401
|
}
|
|
@@ -1858,7 +3408,7 @@ function createEngine({
|
|
|
1858
3408
|
if (groupList) {
|
|
1859
3409
|
groupList.forEach((g, idx) => {
|
|
1860
3410
|
const groupId = `G:${bookId}:${idx}`;
|
|
1861
|
-
let p = new
|
|
3411
|
+
let p = new THREE6__namespace.Vector3();
|
|
1862
3412
|
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1863
3413
|
const arr = cfg.arrangement[groupId];
|
|
1864
3414
|
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
@@ -1877,7 +3427,7 @@ function createEngine({
|
|
|
1877
3427
|
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1878
3428
|
if (texRes) {
|
|
1879
3429
|
const baseScale = 0.036;
|
|
1880
|
-
const size = new
|
|
3430
|
+
const size = new THREE6__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1881
3431
|
const mat = createSmartMaterial({
|
|
1882
3432
|
uniforms: {
|
|
1883
3433
|
uMap: { value: texRes.tex },
|
|
@@ -1918,7 +3468,7 @@ function createEngine({
|
|
|
1918
3468
|
depthWrite: false,
|
|
1919
3469
|
depthTest: true
|
|
1920
3470
|
});
|
|
1921
|
-
const mesh = new
|
|
3471
|
+
const mesh = new THREE6__namespace.Mesh(new THREE6__namespace.PlaneGeometry(1, 1), mat);
|
|
1922
3472
|
mesh.position.copy(p);
|
|
1923
3473
|
mesh.scale.set(size.x, size.y, 1);
|
|
1924
3474
|
mesh.frustumCulled = false;
|
|
@@ -1940,14 +3490,14 @@ function createEngine({
|
|
|
1940
3490
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1941
3491
|
if (boundaries.length > 0) {
|
|
1942
3492
|
const boundaryMat = createSmartMaterial({
|
|
1943
|
-
uniforms: { color: { value: new
|
|
3493
|
+
uniforms: { color: { value: new THREE6__namespace.Color(5601177) } },
|
|
1944
3494
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
1945
3495
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1946
3496
|
transparent: true,
|
|
1947
3497
|
depthWrite: false,
|
|
1948
|
-
blending:
|
|
3498
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
1949
3499
|
});
|
|
1950
|
-
const boundaryGeo = new
|
|
3500
|
+
const boundaryGeo = new THREE6__namespace.BufferGeometry();
|
|
1951
3501
|
const bPoints = [];
|
|
1952
3502
|
boundaries.forEach((angle) => {
|
|
1953
3503
|
const steps = 32;
|
|
@@ -1960,8 +3510,8 @@ function createEngine({
|
|
|
1960
3510
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1961
3511
|
}
|
|
1962
3512
|
});
|
|
1963
|
-
boundaryGeo.setAttribute("position", new
|
|
1964
|
-
boundaryLines = new
|
|
3513
|
+
boundaryGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(bPoints, 3));
|
|
3514
|
+
boundaryLines = new THREE6__namespace.LineSegments(boundaryGeo, boundaryMat);
|
|
1965
3515
|
boundaryLines.frustumCulled = false;
|
|
1966
3516
|
root.add(boundaryLines);
|
|
1967
3517
|
}
|
|
@@ -1980,7 +3530,7 @@ function createEngine({
|
|
|
1980
3530
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1981
3531
|
const phi = Math.atan2(y, x);
|
|
1982
3532
|
const theta = r_norm * (Math.PI / 2);
|
|
1983
|
-
return new
|
|
3533
|
+
return new THREE6__namespace.Vector3(
|
|
1984
3534
|
Math.sin(theta) * Math.cos(phi),
|
|
1985
3535
|
Math.cos(theta),
|
|
1986
3536
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1993,22 +3543,23 @@ function createEngine({
|
|
|
1993
3543
|
}
|
|
1994
3544
|
}
|
|
1995
3545
|
if (polyPoints.length > 0) {
|
|
1996
|
-
const polyGeo = new
|
|
1997
|
-
polyGeo.setAttribute("position", new
|
|
3546
|
+
const polyGeo = new THREE6__namespace.BufferGeometry();
|
|
3547
|
+
polyGeo.setAttribute("position", new THREE6__namespace.Float32BufferAttribute(polyPoints, 3));
|
|
1998
3548
|
const polyMat = createSmartMaterial({
|
|
1999
|
-
uniforms: { color: { value: new
|
|
3549
|
+
uniforms: { color: { value: new THREE6__namespace.Color(3718648) } },
|
|
2000
3550
|
// Cyan-ish
|
|
2001
3551
|
vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
|
|
2002
3552
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
2003
3553
|
transparent: true,
|
|
2004
3554
|
depthWrite: false,
|
|
2005
|
-
blending:
|
|
3555
|
+
blending: THREE6__namespace.AdditiveBlending
|
|
2006
3556
|
});
|
|
2007
|
-
const polyLines = new
|
|
3557
|
+
const polyLines = new THREE6__namespace.LineSegments(polyGeo, polyMat);
|
|
2008
3558
|
polyLines.frustumCulled = false;
|
|
2009
3559
|
root.add(polyLines);
|
|
2010
3560
|
}
|
|
2011
3561
|
}
|
|
3562
|
+
labelManager.setLabels(dynamicLabels);
|
|
2012
3563
|
resize();
|
|
2013
3564
|
}
|
|
2014
3565
|
let lastData = void 0;
|
|
@@ -2029,6 +3580,10 @@ function createEngine({
|
|
|
2029
3580
|
}
|
|
2030
3581
|
function setConfig(cfg) {
|
|
2031
3582
|
currentConfig = cfg;
|
|
3583
|
+
applyGroundTheme(cfg);
|
|
3584
|
+
const externalFocusId = cfg.focus?.nodeId;
|
|
3585
|
+
if (typeof externalFocusId === "string") focusedNodeId = externalFocusId;
|
|
3586
|
+
if (externalFocusId === null) focusedNodeId = null;
|
|
2032
3587
|
if (cfg.projection) setProjection(cfg.projection);
|
|
2033
3588
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
2034
3589
|
state.lon = cfg.camera.lon;
|
|
@@ -2069,6 +3624,25 @@ function createEngine({
|
|
|
2069
3624
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
2070
3625
|
}
|
|
2071
3626
|
if (cfg.constellations) {
|
|
3627
|
+
const getLayoutPosition = (id) => {
|
|
3628
|
+
const n = nodeById.get(id);
|
|
3629
|
+
if (!n) return null;
|
|
3630
|
+
const x = n.meta?.x ?? 0;
|
|
3631
|
+
const y = n.meta?.y ?? 0;
|
|
3632
|
+
const z = n.meta?.z ?? 0;
|
|
3633
|
+
if (z === 0) {
|
|
3634
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
3635
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
3636
|
+
const phi = Math.atan2(y, x);
|
|
3637
|
+
const theta = r_norm * (Math.PI / 2);
|
|
3638
|
+
return new THREE6__namespace.Vector3(
|
|
3639
|
+
Math.sin(theta) * Math.cos(phi),
|
|
3640
|
+
Math.cos(theta),
|
|
3641
|
+
Math.sin(theta) * Math.sin(phi)
|
|
3642
|
+
).multiplyScalar(radius);
|
|
3643
|
+
}
|
|
3644
|
+
return new THREE6__namespace.Vector3(x, y, z);
|
|
3645
|
+
};
|
|
2072
3646
|
constellationLayer.load(cfg.constellations, (id) => {
|
|
2073
3647
|
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
2074
3648
|
const arr = cfg.arrangement[id];
|
|
@@ -2079,17 +3653,16 @@ function createEngine({
|
|
|
2079
3653
|
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
2080
3654
|
const phi = Math.atan2(y, x);
|
|
2081
3655
|
const theta = r_norm * (Math.PI / 2);
|
|
2082
|
-
return new
|
|
3656
|
+
return new THREE6__namespace.Vector3(
|
|
2083
3657
|
Math.sin(theta) * Math.cos(phi),
|
|
2084
3658
|
Math.cos(theta),
|
|
2085
3659
|
Math.sin(theta) * Math.sin(phi)
|
|
2086
3660
|
).multiplyScalar(radius);
|
|
2087
3661
|
}
|
|
2088
|
-
return new
|
|
3662
|
+
return new THREE6__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
2089
3663
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
});
|
|
3664
|
+
return getLayoutPosition(id);
|
|
3665
|
+
}, getLayoutPosition);
|
|
2093
3666
|
}
|
|
2094
3667
|
}
|
|
2095
3668
|
function setHandlers(next) {
|
|
@@ -2114,7 +3687,7 @@ function createEngine({
|
|
|
2114
3687
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
2115
3688
|
}
|
|
2116
3689
|
for (const item of constellationLayer.getItems()) {
|
|
2117
|
-
arr[item.config.id] = { position: [item.
|
|
3690
|
+
arr[item.config.id] = { position: [item.center.x, item.center.y, item.center.z] };
|
|
2118
3691
|
}
|
|
2119
3692
|
Object.assign(arr, state.tempArrangement);
|
|
2120
3693
|
return arr;
|
|
@@ -2138,60 +3711,70 @@ function createEngine({
|
|
|
2138
3711
|
const uAspect = camera.aspect;
|
|
2139
3712
|
const w = rect.width;
|
|
2140
3713
|
const h = rect.height;
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3714
|
+
const isEditMode = currentConfig?.editable ?? false;
|
|
3715
|
+
function pickLabel(threshold) {
|
|
3716
|
+
let closest = null;
|
|
3717
|
+
let minDist = threshold;
|
|
3718
|
+
for (const item of dynamicLabels) {
|
|
3719
|
+
if (!item.obj.visible) continue;
|
|
3720
|
+
if (isNodeFiltered(item.node)) continue;
|
|
3721
|
+
const labelMat = item.obj.material;
|
|
3722
|
+
if ((labelMat?.uniforms?.uAlpha?.value ?? 0) < 0.1) continue;
|
|
3723
|
+
const pWorld = item.obj.position;
|
|
3724
|
+
const pProj = smartProjectJS(pWorld);
|
|
3725
|
+
if (currentProjection.isClipped(pProj.z)) continue;
|
|
3726
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
3727
|
+
const yNDC = pProj.y * uScale;
|
|
3728
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
3729
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
3730
|
+
const d = Math.sqrt((mX - sX) ** 2 + (mY - sY) ** 2);
|
|
3731
|
+
if (d < minDist) {
|
|
3732
|
+
minDist = d;
|
|
3733
|
+
closest = item;
|
|
3734
|
+
}
|
|
2160
3735
|
}
|
|
3736
|
+
return closest;
|
|
2161
3737
|
}
|
|
3738
|
+
if (isEditMode) {
|
|
3739
|
+
if (starPoints) {
|
|
3740
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3741
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3742
|
+
raycaster.ray.direction.copy(worldDir);
|
|
3743
|
+
raycaster.params.Points.threshold = 65 * (state.fov / 60);
|
|
3744
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
3745
|
+
const pointHit = hits[0];
|
|
3746
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
3747
|
+
const id = starIndexToId[pointHit.index];
|
|
3748
|
+
if (id) {
|
|
3749
|
+
const node = nodeById.get(id);
|
|
3750
|
+
if (node && !isNodeFiltered(node)) {
|
|
3751
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3752
|
+
const starPos = new THREE6__namespace.Vector3(attr.getX(pointHit.index), attr.getY(pointHit.index), attr.getZ(pointHit.index));
|
|
3753
|
+
return { type: "star", node, index: pointHit.index, point: starPos, object: void 0 };
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
const editLabel = pickLabel(isTouchDevice ? 48 : 32);
|
|
3759
|
+
if (editLabel) {
|
|
3760
|
+
return { type: "label", node: editLabel.node, object: editLabel.obj, point: editLabel.obj.position.clone(), index: void 0 };
|
|
3761
|
+
}
|
|
3762
|
+
return void 0;
|
|
3763
|
+
}
|
|
3764
|
+
const closestLabel = pickLabel(isTouchDevice ? 48 : 40);
|
|
2162
3765
|
if (closestLabel) {
|
|
2163
3766
|
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
2164
3767
|
}
|
|
2165
3768
|
let closestConst = null;
|
|
2166
3769
|
let minConstDist = Infinity;
|
|
3770
|
+
const artWorldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
3771
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
3772
|
+
raycaster.ray.direction.copy(artWorldDir);
|
|
2167
3773
|
for (const item of constellationLayer.getItems()) {
|
|
2168
3774
|
if (!item.mesh.visible) continue;
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const uniforms = item.material.uniforms;
|
|
2173
|
-
if (!uniforms || !uniforms.uSize) continue;
|
|
2174
|
-
const uSize = uniforms.uSize.value;
|
|
2175
|
-
const uImgAspect = uniforms.uImgAspect.value;
|
|
2176
|
-
const uImgRotation = uniforms.uImgRotation.value;
|
|
2177
|
-
const dist = pWorld.length();
|
|
2178
|
-
if (dist < 1e-3) continue;
|
|
2179
|
-
const scale = uSize / dist * uScale;
|
|
2180
|
-
const halfH_px = scale / 2 * (h / 2);
|
|
2181
|
-
const halfW_px = halfH_px * uImgAspect;
|
|
2182
|
-
const xNDC = pProj.x * uScale / uAspect;
|
|
2183
|
-
const yNDC = pProj.y * uScale;
|
|
2184
|
-
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
2185
|
-
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
2186
|
-
const dx = mX - sX;
|
|
2187
|
-
const dy = mY - sY;
|
|
2188
|
-
const dy_cart = -dy;
|
|
2189
|
-
const cr = Math.cos(-uImgRotation);
|
|
2190
|
-
const sr = Math.sin(-uImgRotation);
|
|
2191
|
-
const localX = dx * cr - dy_cart * sr;
|
|
2192
|
-
const localY = dx * sr + dy_cart * cr;
|
|
2193
|
-
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
2194
|
-
const d = Math.sqrt(dx * dx + dy * dy);
|
|
3775
|
+
const hits = raycaster.intersectObject(item.mesh, false);
|
|
3776
|
+
if (hits.length > 0) {
|
|
3777
|
+
const d = hits[0].distance;
|
|
2195
3778
|
if (!closestConst || d < minConstDist) {
|
|
2196
3779
|
minConstDist = d;
|
|
2197
3780
|
closestConst = item;
|
|
@@ -2204,7 +3787,7 @@ function createEngine({
|
|
|
2204
3787
|
label: closestConst.config.title,
|
|
2205
3788
|
level: -1
|
|
2206
3789
|
};
|
|
2207
|
-
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.
|
|
3790
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.center.clone(), index: void 0 };
|
|
2208
3791
|
}
|
|
2209
3792
|
if (starPoints) {
|
|
2210
3793
|
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
@@ -2235,11 +3818,20 @@ function createEngine({
|
|
|
2235
3818
|
if (hit) {
|
|
2236
3819
|
state.dragMode = "node";
|
|
2237
3820
|
state.draggedNodeId = hit.node.id;
|
|
2238
|
-
|
|
3821
|
+
if (hit.type === "star" && hit.index !== void 0 && starPoints) {
|
|
3822
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3823
|
+
const starWorldPos = new THREE6__namespace.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3824
|
+
state.draggedDist = starWorldPos.length();
|
|
3825
|
+
} else {
|
|
3826
|
+
state.draggedDist = hit.point.length();
|
|
3827
|
+
}
|
|
2239
3828
|
document.body.style.cursor = "crosshair";
|
|
3829
|
+
state.velocityX = 0;
|
|
3830
|
+
state.velocityY = 0;
|
|
2240
3831
|
if (hit.type === "star") {
|
|
2241
3832
|
state.draggedStarIndex = hit.index ?? -1;
|
|
2242
3833
|
state.draggedGroup = null;
|
|
3834
|
+
state.tempArrangement = {};
|
|
2243
3835
|
} else if (hit.type === "label") {
|
|
2244
3836
|
const bookId = hit.node.id;
|
|
2245
3837
|
const children = [];
|
|
@@ -2250,7 +3842,7 @@ function createEngine({
|
|
|
2250
3842
|
if (starId) {
|
|
2251
3843
|
const starNode = nodeById.get(starId);
|
|
2252
3844
|
if (starNode && starNode.parent === bookId) {
|
|
2253
|
-
children.push({ index: i, initialPos: new
|
|
3845
|
+
children.push({ index: i, initialPos: new THREE6__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
2254
3846
|
}
|
|
2255
3847
|
}
|
|
2256
3848
|
}
|
|
@@ -2258,7 +3850,7 @@ function createEngine({
|
|
|
2258
3850
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
2259
3851
|
state.draggedStarIndex = -1;
|
|
2260
3852
|
} else if (hit.type === "constellation") {
|
|
2261
|
-
state.draggedGroup =
|
|
3853
|
+
state.draggedGroup = { labelInitialPos: hit.point.clone(), children: [] };
|
|
2262
3854
|
state.draggedStarIndex = -1;
|
|
2263
3855
|
}
|
|
2264
3856
|
}
|
|
@@ -2287,6 +3879,9 @@ function createEngine({
|
|
|
2287
3879
|
const attr = starPoints.geometry.attributes.position;
|
|
2288
3880
|
attr.setXYZ(idx, newPos.x, newPos.y, newPos.z);
|
|
2289
3881
|
attr.needsUpdate = true;
|
|
3882
|
+
editHoverTargetPos = newPos.clone();
|
|
3883
|
+
const starId = starIndexToId[idx];
|
|
3884
|
+
if (starId) state.tempArrangement[starId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2290
3885
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
2291
3886
|
const group = state.draggedGroup;
|
|
2292
3887
|
const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
|
|
@@ -2296,16 +3891,19 @@ function createEngine({
|
|
|
2296
3891
|
} else if (state.draggedNodeId) {
|
|
2297
3892
|
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
2298
3893
|
if (cItem) {
|
|
2299
|
-
|
|
3894
|
+
const vS = group.labelInitialPos.clone().normalize();
|
|
3895
|
+
const vE = newPos.clone().normalize();
|
|
3896
|
+
cItem.mesh.quaternion.setFromUnitVectors(vS, vE);
|
|
3897
|
+
cItem.center.copy(newPos);
|
|
2300
3898
|
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
2301
3899
|
}
|
|
2302
3900
|
}
|
|
2303
3901
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
2304
3902
|
const vEnd = newPos.clone().normalize();
|
|
2305
|
-
const q = new
|
|
3903
|
+
const q = new THREE6__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
2306
3904
|
if (starPoints && group.children.length > 0) {
|
|
2307
3905
|
const attr = starPoints.geometry.attributes.position;
|
|
2308
|
-
const tempVec = new
|
|
3906
|
+
const tempVec = new THREE6__namespace.Vector3();
|
|
2309
3907
|
for (const child of group.children) {
|
|
2310
3908
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
2311
3909
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -2341,7 +3939,7 @@ function createEngine({
|
|
|
2341
3939
|
if (res) {
|
|
2342
3940
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
2343
3941
|
const baseScale = 0.03;
|
|
2344
|
-
const size = new
|
|
3942
|
+
const size = new THREE6__namespace.Vector2(baseScale * res.aspect, baseScale);
|
|
2345
3943
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
2346
3944
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
2347
3945
|
}
|
|
@@ -2349,10 +3947,19 @@ function createEngine({
|
|
|
2349
3947
|
hoverLabelMesh.position.copy(hit.point);
|
|
2350
3948
|
hoverLabelMat.uniforms.uAlpha.value = 1;
|
|
2351
3949
|
hoverLabelMesh.visible = true;
|
|
3950
|
+
if (currentConfig?.editable && hit.type === "star" && hit.index !== void 0 && starPoints) {
|
|
3951
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3952
|
+
editHoverTargetPos = new THREE6__namespace.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3953
|
+
} else if (currentConfig?.editable && hit.type === "star") {
|
|
3954
|
+
editHoverTargetPos = hit.point.clone();
|
|
3955
|
+
}
|
|
2352
3956
|
} else {
|
|
2353
3957
|
currentHoverNodeId = null;
|
|
2354
3958
|
hoverLabelMat.uniforms.uAlpha.value = 0;
|
|
2355
3959
|
hoverLabelMesh.visible = false;
|
|
3960
|
+
if (currentConfig?.editable && state.dragMode !== "node") {
|
|
3961
|
+
editHoverTargetPos = null;
|
|
3962
|
+
}
|
|
2356
3963
|
}
|
|
2357
3964
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
2358
3965
|
handlers._lastHoverId = hit?.node.id;
|
|
@@ -2369,6 +3976,7 @@ function createEngine({
|
|
|
2369
3976
|
if (state.dragMode === "node") {
|
|
2370
3977
|
const fullArr = getFullArrangement();
|
|
2371
3978
|
handlers.onArrangementChange?.(fullArr);
|
|
3979
|
+
editDropFlash = 1;
|
|
2372
3980
|
state.dragMode = "none";
|
|
2373
3981
|
state.draggedNodeId = null;
|
|
2374
3982
|
state.draggedStarIndex = -1;
|
|
@@ -2383,10 +3991,12 @@ function createEngine({
|
|
|
2383
3991
|
if (hit) {
|
|
2384
3992
|
handlers.onSelect?.(hit.node);
|
|
2385
3993
|
constellationLayer.setFocused(hit.node.id);
|
|
3994
|
+
focusedNodeId = hit.node.id;
|
|
2386
3995
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2387
3996
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2388
3997
|
} else {
|
|
2389
3998
|
setFocusedBook(null);
|
|
3999
|
+
focusedNodeId = null;
|
|
2390
4000
|
}
|
|
2391
4001
|
}
|
|
2392
4002
|
} else {
|
|
@@ -2394,10 +4004,12 @@ function createEngine({
|
|
|
2394
4004
|
if (hit) {
|
|
2395
4005
|
handlers.onSelect?.(hit.node);
|
|
2396
4006
|
constellationLayer.setFocused(hit.node.id);
|
|
4007
|
+
focusedNodeId = hit.node.id;
|
|
2397
4008
|
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
2398
4009
|
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
2399
4010
|
} else {
|
|
2400
4011
|
setFocusedBook(null);
|
|
4012
|
+
focusedNodeId = null;
|
|
2401
4013
|
}
|
|
2402
4014
|
}
|
|
2403
4015
|
}
|
|
@@ -2413,7 +4025,7 @@ function createEngine({
|
|
|
2413
4025
|
handlers.onFovChange?.(state.fov);
|
|
2414
4026
|
updateUniforms();
|
|
2415
4027
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
2416
|
-
const quaternion = new
|
|
4028
|
+
const quaternion = new THREE6__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
2417
4029
|
const dampStartFov = 40;
|
|
2418
4030
|
const dampEndFov = 120;
|
|
2419
4031
|
let spinAmount = 1;
|
|
@@ -2422,27 +4034,27 @@ function createEngine({
|
|
|
2422
4034
|
spinAmount = 1 - Math.pow(t, 1.5) * 0.8;
|
|
2423
4035
|
}
|
|
2424
4036
|
if (spinAmount < 0.999) {
|
|
2425
|
-
const identityQuat = new
|
|
4037
|
+
const identityQuat = new THREE6__namespace.Quaternion();
|
|
2426
4038
|
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
2427
4039
|
}
|
|
2428
4040
|
const y = Math.sin(state.lat);
|
|
2429
4041
|
const r = Math.cos(state.lat);
|
|
2430
4042
|
const x = r * Math.sin(state.lon);
|
|
2431
4043
|
const z = -r * Math.cos(state.lon);
|
|
2432
|
-
const currentLook = new
|
|
4044
|
+
const currentLook = new THREE6__namespace.Vector3(x, y, z);
|
|
2433
4045
|
const camForward = currentLook.clone().normalize();
|
|
2434
4046
|
const camUp = camera.up.clone();
|
|
2435
|
-
const camRight = new
|
|
2436
|
-
const camUpOrtho = new
|
|
2437
|
-
const mat = new
|
|
2438
|
-
const qOld = new
|
|
4047
|
+
const camRight = new THREE6__namespace.Vector3().crossVectors(camForward, camUp).normalize();
|
|
4048
|
+
const camUpOrtho = new THREE6__namespace.Vector3().crossVectors(camRight, camForward).normalize();
|
|
4049
|
+
const mat = new THREE6__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
4050
|
+
const qOld = new THREE6__namespace.Quaternion().setFromRotationMatrix(mat);
|
|
2439
4051
|
const qNew = qOld.clone().multiply(quaternion);
|
|
2440
|
-
const newForward = new
|
|
4052
|
+
const newForward = new THREE6__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
2441
4053
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
2442
4054
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
2443
|
-
const newUp = new
|
|
4055
|
+
const newUp = new THREE6__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
2444
4056
|
camera.up.copy(newUp);
|
|
2445
|
-
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
4057
|
+
if (!getSceneDebug()?.disableZenithBias && e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
2446
4058
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
2447
4059
|
let t = (state.fov - ENGINE_CONFIG.zenithStartFov) / range;
|
|
2448
4060
|
t = Math.max(0, Math.min(1, t));
|
|
@@ -2554,7 +4166,7 @@ function createEngine({
|
|
|
2554
4166
|
state.fov = state.pinchStartFov / scale;
|
|
2555
4167
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
2556
4168
|
handlers.onFovChange?.(state.fov);
|
|
2557
|
-
if (state.fov > prevFov && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
4169
|
+
if (!getSceneDebug()?.disableZenithBias && state.fov > prevFov && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
2558
4170
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
2559
4171
|
let t = (state.fov - ENGINE_CONFIG.zenithStartFov) / range;
|
|
2560
4172
|
t = Math.max(0, Math.min(1, t));
|
|
@@ -2790,14 +4402,24 @@ function createEngine({
|
|
|
2790
4402
|
const r = Math.cos(state.lat);
|
|
2791
4403
|
const x = r * Math.sin(state.lon);
|
|
2792
4404
|
const z = -r * Math.cos(state.lon);
|
|
2793
|
-
const target = new
|
|
2794
|
-
const idealUp = new
|
|
4405
|
+
const target = new THREE6__namespace.Vector3(x, y, z);
|
|
4406
|
+
const idealUp = new THREE6__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
2795
4407
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
2796
4408
|
camera.up.normalize();
|
|
2797
4409
|
camera.lookAt(target);
|
|
2798
4410
|
camera.updateMatrixWorld();
|
|
2799
4411
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
4412
|
+
if (groundMaterial?.uniforms?.uZenithFlatten) {
|
|
4413
|
+
const flatten = getSceneDebug()?.disableZenithFlatten ? 0 : THREE6__namespace.MathUtils.smoothstep(
|
|
4414
|
+
state.lat,
|
|
4415
|
+
THREE6__namespace.MathUtils.degToRad(68),
|
|
4416
|
+
THREE6__namespace.MathUtils.degToRad(88)
|
|
4417
|
+
);
|
|
4418
|
+
groundMaterial.uniforms.uZenithFlatten.value = flatten;
|
|
4419
|
+
}
|
|
2800
4420
|
updateUniforms();
|
|
4421
|
+
if (getSceneDebug()?.horizonDiagnostics) runHorizonDiagnostics(now);
|
|
4422
|
+
updateChapterLabelAnchors();
|
|
2801
4423
|
const nowSec = now / 1e3;
|
|
2802
4424
|
const dt = lastTickTime > 0 ? Math.min(nowSec - lastTickTime, 0.1) : 0.016;
|
|
2803
4425
|
lastTickTime = nowSec;
|
|
@@ -2805,21 +4427,60 @@ function createEngine({
|
|
|
2805
4427
|
linesFader.update(dt);
|
|
2806
4428
|
artFader.target = currentConfig?.showConstellationArt ?? false;
|
|
2807
4429
|
artFader.update(dt);
|
|
2808
|
-
constellationLayer.update(state.fov, artFader.eased > 0.01);
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
}
|
|
4430
|
+
constellationLayer.update(state.fov, artFader.eased > 0.01, camera, dt);
|
|
4431
|
+
const baseArtOpacity = THREE6__namespace.MathUtils.clamp(currentConfig?.constellationBaseOpacity ?? 1, 0, 300);
|
|
4432
|
+
constellationLayer.setGlobalOpacity?.(artFader.eased * baseArtOpacity);
|
|
2812
4433
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
4434
|
+
if (backdropStarsMaterial?.uniforms) {
|
|
4435
|
+
const minGain = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropWideFovGain ?? 0.42, 0, 1);
|
|
4436
|
+
const fovT = THREE6__namespace.MathUtils.smoothstep(state.fov, 24, 100);
|
|
4437
|
+
const gain = THREE6__namespace.MathUtils.lerp(1, minGain, fovT);
|
|
4438
|
+
backdropStarsMaterial.uniforms.uBackdropGain.value = gain;
|
|
4439
|
+
backdropStarsMaterial.uniforms.uBackdropEnergy.value = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropEnergy ?? 2.2, 0.2, 5);
|
|
4440
|
+
backdropStarsMaterial.uniforms.uBackdropSizeExp.value = THREE6__namespace.MathUtils.clamp(currentConfig?.backdropSizeExponent ?? 0.9, 0.4, 1.4);
|
|
4441
|
+
}
|
|
4442
|
+
if (skyBackgroundMesh) skyBackgroundMesh.visible = currentConfig?.background !== "transparent";
|
|
2813
4443
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2814
|
-
|
|
2815
|
-
|
|
4444
|
+
if (moonMesh) moonMesh.visible = currentConfig?.showMoon ?? true;
|
|
4445
|
+
if (moonGlowMesh) moonGlowMesh.visible = currentConfig?.showMoon ?? true;
|
|
4446
|
+
const showSun = currentConfig?.showSunrise ?? true;
|
|
4447
|
+
if (sunDiscMesh) sunDiscMesh.visible = showSun;
|
|
4448
|
+
if (sunHaloMesh) sunHaloMesh.visible = showSun;
|
|
4449
|
+
if (milkyWayMesh) milkyWayMesh.visible = currentConfig?.showMilkyWay ?? true;
|
|
4450
|
+
if (editHoverMesh) {
|
|
4451
|
+
const ringMat = editHoverMesh.material;
|
|
4452
|
+
const isEditing = currentConfig?.editable ?? false;
|
|
4453
|
+
const isDraggingStar = state.dragMode === "node" && state.draggedStarIndex !== -1;
|
|
4454
|
+
const hasTarget = isEditing && editHoverTargetPos !== null;
|
|
4455
|
+
if (hasTarget) {
|
|
4456
|
+
editHoverMesh.position.copy(editHoverTargetPos);
|
|
4457
|
+
const pulseBoost = editDropFlash * 1.8;
|
|
4458
|
+
const targetAlpha = 0.8 + pulseBoost;
|
|
4459
|
+
ringMat.uniforms.uRingAlpha.value = THREE6__namespace.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, targetAlpha, 0.15);
|
|
4460
|
+
const tGold = isDraggingStar ? 1 : editDropFlash;
|
|
4461
|
+
const targetColor = new THREE6__namespace.Color(
|
|
4462
|
+
THREE6__namespace.MathUtils.lerp(0.55, 1, tGold),
|
|
4463
|
+
THREE6__namespace.MathUtils.lerp(0.88, 0.82, tGold),
|
|
4464
|
+
THREE6__namespace.MathUtils.lerp(1, 0.18, tGold)
|
|
4465
|
+
);
|
|
4466
|
+
ringMat.uniforms.uRingColor.value.lerp(targetColor, 0.18);
|
|
4467
|
+
const baseSize = isDraggingStar ? 0.075 : 0.06;
|
|
4468
|
+
const targetSize = baseSize * (1 + editDropFlash * 0.7);
|
|
4469
|
+
ringMat.uniforms.uRingSize.value = THREE6__namespace.MathUtils.lerp(ringMat.uniforms.uRingSize.value, targetSize, 0.18);
|
|
4470
|
+
editDropFlash = Math.max(0, editDropFlash - dt * 3);
|
|
4471
|
+
} else {
|
|
4472
|
+
ringMat.uniforms.uRingAlpha.value = THREE6__namespace.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, 0, 0.15);
|
|
4473
|
+
ringMat.uniforms.uRingSize.value = THREE6__namespace.MathUtils.lerp(ringMat.uniforms.uRingSize.value, 0.06, 0.2);
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
2816
4476
|
if (constellationLines) {
|
|
2817
4477
|
constellationLines.visible = linesFader.eased > 0.01;
|
|
2818
4478
|
if (constellationLines.visible && constellationLines.material) {
|
|
2819
4479
|
const mat = constellationLines.material;
|
|
2820
4480
|
if (mat.uniforms?.color) {
|
|
2821
4481
|
mat.uniforms.color.value.setHex(11193599);
|
|
2822
|
-
mat.
|
|
4482
|
+
if (mat.uniforms.uReveal) mat.uniforms.uReveal.value = linesFader.eased;
|
|
4483
|
+
mat.opacity = 1;
|
|
2823
4484
|
}
|
|
2824
4485
|
}
|
|
2825
4486
|
}
|
|
@@ -2830,116 +4491,35 @@ function createEngine({
|
|
|
2830
4491
|
const screenW = rect.width;
|
|
2831
4492
|
const screenH = rect.height;
|
|
2832
4493
|
const aspect = screenW / screenH;
|
|
2833
|
-
const
|
|
2834
|
-
const occupied = [];
|
|
2835
|
-
function isOverlapping(x2, y2, w, h) {
|
|
2836
|
-
for (const r2 of occupied) {
|
|
2837
|
-
if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
|
|
2838
|
-
}
|
|
2839
|
-
return false;
|
|
2840
|
-
}
|
|
2841
|
-
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
2842
|
-
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2843
|
-
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2844
|
-
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2845
|
-
const showChapters = state.fov < 45;
|
|
2846
|
-
for (const item of dynamicLabels) {
|
|
2847
|
-
const uniforms = item.obj.material.uniforms;
|
|
2848
|
-
const level = item.node.level;
|
|
2849
|
-
let isEnabled = false;
|
|
2850
|
-
if (level === 2 && showBookLabels) isEnabled = true;
|
|
2851
|
-
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
2852
|
-
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2853
|
-
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
2854
|
-
if (!isEnabled) {
|
|
2855
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2856
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2857
|
-
continue;
|
|
2858
|
-
}
|
|
2859
|
-
const pWorld = item.obj.position;
|
|
2860
|
-
const pProj = smartProjectJS(pWorld);
|
|
2861
|
-
if (pProj.z > 0.2) {
|
|
2862
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2863
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2864
|
-
continue;
|
|
2865
|
-
}
|
|
2866
|
-
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2867
|
-
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2868
|
-
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2869
|
-
continue;
|
|
2870
|
-
}
|
|
2871
|
-
const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
|
|
2872
|
-
const ndcY = pProj.y * globalUniforms.uScale.value;
|
|
2873
|
-
const sX = (ndcX * 0.5 + 0.5) * screenW;
|
|
2874
|
-
const sY = (-ndcY * 0.5 + 0.5) * screenH;
|
|
2875
|
-
const size = uniforms.uSize.value;
|
|
2876
|
-
const pixelH = size.y * screenH * 0.8;
|
|
2877
|
-
const pixelW = size.x * screenH * 0.8;
|
|
2878
|
-
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
2879
|
-
}
|
|
2880
|
-
const hoverId = handlers._lastHoverId;
|
|
4494
|
+
const hoverId = handlers._lastHoverId ?? null;
|
|
2881
4495
|
const selectedId = state.draggedNodeId;
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
4496
|
+
labelManager.update({
|
|
4497
|
+
nowMs: now,
|
|
4498
|
+
dt,
|
|
4499
|
+
fov: state.fov,
|
|
4500
|
+
camera,
|
|
4501
|
+
projectionId: currentProjection.id,
|
|
4502
|
+
screenW,
|
|
4503
|
+
screenH,
|
|
4504
|
+
globalScale: globalUniforms.uScale.value,
|
|
4505
|
+
aspect,
|
|
4506
|
+
hoverId,
|
|
4507
|
+
selectedId,
|
|
4508
|
+
focusedId: focusedNodeId,
|
|
4509
|
+
shouldFilter: !!currentFilter && filterStrength > 0.01,
|
|
4510
|
+
isNodeFiltered: (node) => {
|
|
4511
|
+
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
4512
|
+
return !!nodeToCheck && isNodeFiltered(nodeToCheck);
|
|
4513
|
+
},
|
|
4514
|
+
toggles: {
|
|
4515
|
+
showBookLabels: currentConfig?.showBookLabels === true,
|
|
4516
|
+
showDivisionLabels: currentConfig?.showDivisionLabels === true,
|
|
4517
|
+
showChapterLabels: currentConfig?.showChapterLabels === true,
|
|
4518
|
+
showGroupLabels: currentConfig?.showGroupLabels === true
|
|
4519
|
+
},
|
|
4520
|
+
config: currentConfig?.labelBehavior,
|
|
4521
|
+
project: smartProjectJS
|
|
2892
4522
|
});
|
|
2893
|
-
for (const l of labelsToCheck) {
|
|
2894
|
-
let target2 = 0;
|
|
2895
|
-
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2896
|
-
if (l.level === 1) {
|
|
2897
|
-
let rot = 0;
|
|
2898
|
-
const isWideAngle = currentProjection.id !== "perspective";
|
|
2899
|
-
if (isWideAngle) {
|
|
2900
|
-
const dx = l.sX - screenW / 2;
|
|
2901
|
-
const dy = l.sY - screenH / 2;
|
|
2902
|
-
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
2903
|
-
}
|
|
2904
|
-
l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2905
|
-
}
|
|
2906
|
-
if (l.level === 2) {
|
|
2907
|
-
{
|
|
2908
|
-
target2 = 1;
|
|
2909
|
-
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2910
|
-
}
|
|
2911
|
-
} else if (l.level === 1) {
|
|
2912
|
-
if (showDivisions || isSpecial) {
|
|
2913
|
-
const pad = -5;
|
|
2914
|
-
if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
|
|
2915
|
-
target2 = 1;
|
|
2916
|
-
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
} else if (l.level === 2.5 || l.level === 3) {
|
|
2920
|
-
if (showChapters || isSpecial) {
|
|
2921
|
-
target2 = 1;
|
|
2922
|
-
if (!isSpecial) {
|
|
2923
|
-
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2924
|
-
const focusFade = 1 - THREE5__namespace.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2925
|
-
target2 *= focusFade;
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
if (target2 > 0 && currentFilter && filterStrength > 0.01) {
|
|
2930
|
-
const node = l.item.node;
|
|
2931
|
-
if (node.level === 3) {
|
|
2932
|
-
target2 = 0;
|
|
2933
|
-
} else if (node.level === 2 || node.level === 2.5) {
|
|
2934
|
-
const nodeToCheck = node.level === 2.5 && node.parent ? nodeById.get(node.parent) : node;
|
|
2935
|
-
if (nodeToCheck && isNodeFiltered(nodeToCheck)) {
|
|
2936
|
-
target2 = 0;
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2941
|
-
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
2942
|
-
}
|
|
2943
4523
|
renderer.render(scene, camera);
|
|
2944
4524
|
}
|
|
2945
4525
|
function stop() {
|
|
@@ -2964,6 +4544,48 @@ function createEngine({
|
|
|
2964
4544
|
function dispose() {
|
|
2965
4545
|
stop();
|
|
2966
4546
|
constellationLayer.dispose();
|
|
4547
|
+
if (moonMesh) {
|
|
4548
|
+
scene.remove(moonMesh);
|
|
4549
|
+
moonMesh.geometry.dispose();
|
|
4550
|
+
moonMesh.material.dispose();
|
|
4551
|
+
moonMesh = null;
|
|
4552
|
+
}
|
|
4553
|
+
if (moonGlowMesh) {
|
|
4554
|
+
scene.remove(moonGlowMesh);
|
|
4555
|
+
moonGlowMesh.geometry.dispose();
|
|
4556
|
+
moonGlowMesh.material.dispose();
|
|
4557
|
+
moonGlowMesh = null;
|
|
4558
|
+
}
|
|
4559
|
+
if (sunDiscMesh) {
|
|
4560
|
+
scene.remove(sunDiscMesh);
|
|
4561
|
+
sunDiscMesh.geometry.dispose();
|
|
4562
|
+
sunDiscMesh.material.dispose();
|
|
4563
|
+
sunDiscMesh = null;
|
|
4564
|
+
}
|
|
4565
|
+
if (sunHaloMesh) {
|
|
4566
|
+
scene.remove(sunHaloMesh);
|
|
4567
|
+
sunHaloMesh.geometry.dispose();
|
|
4568
|
+
sunHaloMesh.material.dispose();
|
|
4569
|
+
sunHaloMesh = null;
|
|
4570
|
+
}
|
|
4571
|
+
if (milkyWayMesh) {
|
|
4572
|
+
scene.remove(milkyWayMesh);
|
|
4573
|
+
milkyWayMesh.geometry.dispose();
|
|
4574
|
+
milkyWayMesh.material.dispose();
|
|
4575
|
+
milkyWayMesh = null;
|
|
4576
|
+
}
|
|
4577
|
+
if (skyBackgroundMesh) {
|
|
4578
|
+
scene.remove(skyBackgroundMesh);
|
|
4579
|
+
skyBackgroundMesh.geometry.dispose();
|
|
4580
|
+
skyBackgroundMesh.material.dispose();
|
|
4581
|
+
skyBackgroundMesh = null;
|
|
4582
|
+
}
|
|
4583
|
+
if (editHoverMesh) {
|
|
4584
|
+
scene.remove(editHoverMesh);
|
|
4585
|
+
editHoverMesh.geometry.dispose();
|
|
4586
|
+
editHoverMesh.material.dispose();
|
|
4587
|
+
editHoverMesh = null;
|
|
4588
|
+
}
|
|
2967
4589
|
renderer.dispose();
|
|
2968
4590
|
renderer.domElement.remove();
|
|
2969
4591
|
}
|
|
@@ -2983,6 +4605,7 @@ function createEngine({
|
|
|
2983
4605
|
function flyTo(nodeId, targetFov) {
|
|
2984
4606
|
const node = nodeById.get(nodeId);
|
|
2985
4607
|
if (!node) return;
|
|
4608
|
+
focusedNodeId = nodeId;
|
|
2986
4609
|
const pos = getPosition(node).normalize();
|
|
2987
4610
|
flyToTargetLat = Math.asin(Math.max(-0.999, Math.min(0.999, pos.y)));
|
|
2988
4611
|
flyToTargetLon = Math.atan2(pos.x, -pos.z);
|
|
@@ -3013,10 +4636,11 @@ var init_createEngine = __esm({
|
|
|
3013
4636
|
init_ConstellationArtworkLayer();
|
|
3014
4637
|
init_projections();
|
|
3015
4638
|
init_fader();
|
|
4639
|
+
init_LabelManager();
|
|
3016
4640
|
ENGINE_CONFIG = {
|
|
3017
4641
|
minFov: 1,
|
|
3018
4642
|
maxFov: 135,
|
|
3019
|
-
defaultFov:
|
|
4643
|
+
defaultFov: 35,
|
|
3020
4644
|
dragSpeed: 125e-5,
|
|
3021
4645
|
inertiaDamping: 0.92,
|
|
3022
4646
|
blendStart: 35,
|
|
@@ -3043,7 +4667,7 @@ var init_createEngine = __esm({
|
|
|
3043
4667
|
};
|
|
3044
4668
|
ORDER_REVEAL_CONFIG = {
|
|
3045
4669
|
globalDim: 0.85,
|
|
3046
|
-
pulseAmplitude: 0.
|
|
4670
|
+
pulseAmplitude: 0.12,
|
|
3047
4671
|
pulseDuration: 2,
|
|
3048
4672
|
delayPerChapter: 0.1
|
|
3049
4673
|
};
|
|
@@ -32233,7 +33857,7 @@ var RNG = class {
|
|
|
32233
33857
|
const r = Math.sqrt(1 - y * y);
|
|
32234
33858
|
const x = r * Math.cos(theta);
|
|
32235
33859
|
const z = r * Math.sin(theta);
|
|
32236
|
-
return new
|
|
33860
|
+
return new THREE6__namespace.Vector3(x, y, z);
|
|
32237
33861
|
}
|
|
32238
33862
|
};
|
|
32239
33863
|
function simpleNoise3D(v, scale) {
|
|
@@ -32271,11 +33895,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
32271
33895
|
});
|
|
32272
33896
|
});
|
|
32273
33897
|
const bookCount = books.length;
|
|
32274
|
-
const mwRad =
|
|
32275
|
-
const mwNormal = new
|
|
33898
|
+
const mwRad = THREE6__namespace.MathUtils.degToRad(opts.milkyWayAngle);
|
|
33899
|
+
const mwNormal = new THREE6__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
32276
33900
|
const anchors = [];
|
|
32277
33901
|
for (let i = 0; i < bookCount; i++) {
|
|
32278
|
-
let bestP = new
|
|
33902
|
+
let bestP = new THREE6__namespace.Vector3();
|
|
32279
33903
|
let valid = false;
|
|
32280
33904
|
let attempt = 0;
|
|
32281
33905
|
while (!valid && attempt < 100) {
|
|
@@ -32301,7 +33925,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32301
33925
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
32302
33926
|
for (let c = 0; c < book.chapters; c++) {
|
|
32303
33927
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
32304
|
-
const offset = new
|
|
33928
|
+
const offset = new THREE6__namespace.Vector3(
|
|
32305
33929
|
(rng.next() - 0.5) * 2,
|
|
32306
33930
|
(rng.next() - 0.5) * 2,
|
|
32307
33931
|
(rng.next() - 0.5) * 2
|
|
@@ -32322,7 +33946,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
32322
33946
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
32323
33947
|
const divId = `D:${book.testament}:${book.division}`;
|
|
32324
33948
|
if (!divisions.has(divId)) {
|
|
32325
|
-
divisions.set(divId, { sum: new
|
|
33949
|
+
divisions.set(divId, { sum: new THREE6__namespace.Vector3(), count: 0 });
|
|
32326
33950
|
}
|
|
32327
33951
|
const entry = divisions.get(divId);
|
|
32328
33952
|
entry.sum.add(anchorPos);
|