@project-skymap/library 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +373 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +373 -172
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1411,16 +1411,111 @@ function createEngine({
|
|
|
1411
1411
|
function getSceneDebug() {
|
|
1412
1412
|
return currentConfig?.debug?.sceneMechanics;
|
|
1413
1413
|
}
|
|
1414
|
+
function getFreezeBand() {
|
|
1415
|
+
const dbg = getSceneDebug();
|
|
1416
|
+
const startRaw = dbg?.freezeBandStartFov ?? ENGINE_CONFIG.freezeBandStartFov;
|
|
1417
|
+
const endRaw = dbg?.freezeBandEndFov ?? ENGINE_CONFIG.freezeBandEndFov;
|
|
1418
|
+
const start2 = Math.min(startRaw, endRaw);
|
|
1419
|
+
const end = Math.max(startRaw, endRaw);
|
|
1420
|
+
return { start: start2, end };
|
|
1421
|
+
}
|
|
1422
|
+
function isInTransitionFreezeBand(fov) {
|
|
1423
|
+
const band = getFreezeBand();
|
|
1424
|
+
return fov >= band.start && fov <= band.end;
|
|
1425
|
+
}
|
|
1426
|
+
function getZenithBiasStartFov() {
|
|
1427
|
+
return getSceneDebug()?.zenithBiasStartFov ?? ENGINE_CONFIG.zenithBiasStartFov;
|
|
1428
|
+
}
|
|
1429
|
+
function getVerticalPanDampConfig() {
|
|
1430
|
+
const dbg = getSceneDebug();
|
|
1431
|
+
const fovStartRaw = dbg?.verticalPanDampStartFov ?? ENGINE_CONFIG.verticalPanDampStartFov;
|
|
1432
|
+
const fovEndRaw = dbg?.verticalPanDampEndFov ?? ENGINE_CONFIG.verticalPanDampEndFov;
|
|
1433
|
+
const latStartRaw = dbg?.verticalPanDampLatStartDeg ?? ENGINE_CONFIG.verticalPanDampLatStartDeg;
|
|
1434
|
+
const latEndRaw = dbg?.verticalPanDampLatEndDeg ?? ENGINE_CONFIG.verticalPanDampLatEndDeg;
|
|
1435
|
+
return {
|
|
1436
|
+
fovStart: Math.min(fovStartRaw, fovEndRaw),
|
|
1437
|
+
fovEnd: Math.max(fovStartRaw, fovEndRaw),
|
|
1438
|
+
latStartDeg: Math.min(latStartRaw, latEndRaw),
|
|
1439
|
+
latEndDeg: Math.max(latStartRaw, latEndRaw)
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function getVerticalPanFactor(fov, lat) {
|
|
1443
|
+
if (zenithProjectionLockActive) return 0;
|
|
1444
|
+
const cfg = getVerticalPanDampConfig();
|
|
1445
|
+
const fovT = THREE6.MathUtils.smoothstep(fov, cfg.fovStart, cfg.fovEnd);
|
|
1446
|
+
const zenithT = THREE6.MathUtils.smoothstep(
|
|
1447
|
+
Math.max(lat, 0),
|
|
1448
|
+
THREE6.MathUtils.degToRad(cfg.latStartDeg),
|
|
1449
|
+
THREE6.MathUtils.degToRad(cfg.latEndDeg)
|
|
1450
|
+
);
|
|
1451
|
+
const lock = Math.max(fovT * 0.65, fovT * zenithT);
|
|
1452
|
+
return THREE6.MathUtils.clamp(1 - lock, 0, 1);
|
|
1453
|
+
}
|
|
1454
|
+
function getMovementMassFactor(fov, wideFovFactor = ENGINE_CONFIG.movementMassWideFov) {
|
|
1455
|
+
const t = THREE6.MathUtils.smoothstep(fov, 24, 96);
|
|
1456
|
+
return THREE6.MathUtils.lerp(1, wideFovFactor, t);
|
|
1457
|
+
}
|
|
1458
|
+
function compressInputDelta(delta) {
|
|
1459
|
+
const absDelta = Math.abs(delta);
|
|
1460
|
+
if (absDelta < 1e-4) return 0;
|
|
1461
|
+
return Math.sign(delta) * (absDelta / (1 + absDelta * ENGINE_CONFIG.inputCompression));
|
|
1462
|
+
}
|
|
1414
1463
|
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
1415
1464
|
function mix(a, b, t) {
|
|
1416
1465
|
return a * (1 - t) + b * t;
|
|
1417
1466
|
}
|
|
1418
1467
|
let currentProjection = new BlendedProjection(ENGINE_CONFIG.blendStart, ENGINE_CONFIG.blendEnd);
|
|
1468
|
+
let zenithProjectionLockActive = false;
|
|
1469
|
+
function getZenithLockBlendThresholds() {
|
|
1470
|
+
const enterRaw = ENGINE_CONFIG.zenithLockBlendEnter;
|
|
1471
|
+
const exitRaw = ENGINE_CONFIG.zenithLockBlendExit;
|
|
1472
|
+
return {
|
|
1473
|
+
enter: Math.max(0, Math.min(1, enterRaw)),
|
|
1474
|
+
exit: Math.max(0, Math.min(1, Math.min(exitRaw, enterRaw)))
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function getZenithLockLat() {
|
|
1478
|
+
return Math.PI / 2 - 1e-3;
|
|
1479
|
+
}
|
|
1480
|
+
function getBlendForZenithControl() {
|
|
1481
|
+
if (currentProjection instanceof BlendedProjection) return currentProjection.getBlend();
|
|
1482
|
+
return 0;
|
|
1483
|
+
}
|
|
1484
|
+
function applyZenithAutoCenter() {
|
|
1485
|
+
const zenithLat = getZenithLockLat();
|
|
1486
|
+
const blend = getBlendForZenithControl();
|
|
1487
|
+
let pullT = THREE6.MathUtils.smoothstep(
|
|
1488
|
+
blend,
|
|
1489
|
+
ENGINE_CONFIG.zenithAutoCenterBlendStart,
|
|
1490
|
+
ENGINE_CONFIG.zenithAutoCenterBlendEnd
|
|
1491
|
+
);
|
|
1492
|
+
if (zenithProjectionLockActive) pullT = 1;
|
|
1493
|
+
if (pullT <= 1e-4) return;
|
|
1494
|
+
const pullLerp = THREE6.MathUtils.lerp(
|
|
1495
|
+
ENGINE_CONFIG.zenithAutoCenterMinLerp,
|
|
1496
|
+
ENGINE_CONFIG.zenithAutoCenterMaxLerp,
|
|
1497
|
+
pullT
|
|
1498
|
+
);
|
|
1499
|
+
state.lat = THREE6.MathUtils.lerp(state.lat, zenithLat, pullLerp);
|
|
1500
|
+
state.targetLat = THREE6.MathUtils.lerp(state.targetLat, zenithLat, Math.min(1, pullLerp * 1.15));
|
|
1501
|
+
state.velocityY *= 1 - 0.85 * pullT;
|
|
1502
|
+
if (zenithProjectionLockActive && Math.abs(state.lat - zenithLat) < 25e-5) {
|
|
1503
|
+
state.lat = zenithLat;
|
|
1504
|
+
state.targetLat = zenithLat;
|
|
1505
|
+
state.velocityY = 0;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1419
1508
|
function syncProjectionState() {
|
|
1420
1509
|
if (currentProjection instanceof BlendedProjection) {
|
|
1421
1510
|
currentProjection.setFov(state.fov);
|
|
1422
1511
|
currentProjection.setBlendOverride(getSceneDebug()?.projectionBlendOverride ?? null);
|
|
1423
1512
|
globalUniforms.uBlend.value = currentProjection.getBlend();
|
|
1513
|
+
const blend = currentProjection.getBlend();
|
|
1514
|
+
const th = getZenithLockBlendThresholds();
|
|
1515
|
+
if (!zenithProjectionLockActive && blend >= th.enter) zenithProjectionLockActive = true;
|
|
1516
|
+
else if (zenithProjectionLockActive && blend <= th.exit) zenithProjectionLockActive = false;
|
|
1517
|
+
} else {
|
|
1518
|
+
zenithProjectionLockActive = false;
|
|
1424
1519
|
}
|
|
1425
1520
|
globalUniforms.uProjectionType.value = currentProjection.glslProjectionType;
|
|
1426
1521
|
}
|
|
@@ -1677,8 +1772,13 @@ function createEngine({
|
|
|
1677
1772
|
}
|
|
1678
1773
|
const flatten = groundMaterial?.uniforms?.uZenithFlatten?.value;
|
|
1679
1774
|
const blend = currentProjection instanceof BlendedProjection ? currentProjection.getBlend() : -1;
|
|
1775
|
+
const freeze = isInTransitionFreezeBand(state.fov) ? 1 : 0;
|
|
1776
|
+
const zenithBiasStart = getZenithBiasStartFov();
|
|
1777
|
+
const vPanCfg = getVerticalPanDampConfig();
|
|
1778
|
+
const vPan = getVerticalPanFactor(state.fov, state.lat);
|
|
1779
|
+
const moveMass = getMovementMassFactor(state.fov);
|
|
1680
1780
|
console.debug(
|
|
1681
|
-
`[HorizonDiag] fov=${state.fov.toFixed(1)} latDeg=${THREE6.MathUtils.radToDeg(state.lat).toFixed(1)} mode=${activeHorizonProfile.mode} blend=${blend.toFixed(3)} flatten=${Number(flatten ?? 0).toFixed(3)} drops=${dropCount} bins=${JSON.stringify(compact)}`
|
|
1781
|
+
`[HorizonDiag] fov=${state.fov.toFixed(1)} latDeg=${THREE6.MathUtils.radToDeg(state.lat).toFixed(1)} mode=${activeHorizonProfile.mode} blend=${blend.toFixed(3)} freeze=${freeze} zLock=${zenithProjectionLockActive ? 1 : 0} biasStart=${zenithBiasStart.toFixed(1)} vPan=${vPan.toFixed(3)} moveMass=${moveMass.toFixed(3)} vPanFov=${vPanCfg.fovStart.toFixed(1)}-${vPanCfg.fovEnd.toFixed(1)} vPanLat=${vPanCfg.latStartDeg.toFixed(1)}-${vPanCfg.latEndDeg.toFixed(1)} flatten=${Number(flatten ?? 0).toFixed(3)} drops=${dropCount} bins=${JSON.stringify(compact)}`
|
|
1682
1782
|
);
|
|
1683
1783
|
}
|
|
1684
1784
|
function createGround() {
|
|
@@ -1827,9 +1927,6 @@ function createEngine({
|
|
|
1827
1927
|
let sunDiscMesh = null;
|
|
1828
1928
|
let sunHaloMesh = null;
|
|
1829
1929
|
let milkyWayMesh = null;
|
|
1830
|
-
let editHoverMesh = null;
|
|
1831
|
-
let editHoverTargetPos = null;
|
|
1832
|
-
let editDropFlash = 0;
|
|
1833
1930
|
function createSkyBackground() {
|
|
1834
1931
|
const geo = new THREE6.SphereGeometry(2400, 32, 32);
|
|
1835
1932
|
const mat = createSmartMaterial({
|
|
@@ -2381,7 +2478,8 @@ function createEngine({
|
|
|
2381
2478
|
uTime: globalUniforms.uTime,
|
|
2382
2479
|
uBackdropGain: { value: 1 },
|
|
2383
2480
|
uBackdropEnergy: { value: 2.2 },
|
|
2384
|
-
uBackdropSizeExp: { value: 0.9 }
|
|
2481
|
+
uBackdropSizeExp: { value: 0.9 },
|
|
2482
|
+
uRevealZoom: { value: 0 }
|
|
2385
2483
|
},
|
|
2386
2484
|
vertexShaderBody: `
|
|
2387
2485
|
attribute float size;
|
|
@@ -2395,6 +2493,7 @@ function createEngine({
|
|
|
2395
2493
|
uniform float uBackdropGain;
|
|
2396
2494
|
uniform float uBackdropEnergy;
|
|
2397
2495
|
uniform float uBackdropSizeExp;
|
|
2496
|
+
uniform float uRevealZoom;
|
|
2398
2497
|
|
|
2399
2498
|
void main() {
|
|
2400
2499
|
vec3 nPos = normalize(position);
|
|
@@ -2410,7 +2509,12 @@ function createEngine({
|
|
|
2410
2509
|
float twinkle = sin(uTime * 3.0 + position.x * 0.05 + position.z * 0.03) * 0.5 + 0.5;
|
|
2411
2510
|
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.4 * turbulence);
|
|
2412
2511
|
|
|
2413
|
-
|
|
2512
|
+
// Backdrop appears latest \u2014 fully hidden at wide FOV, emerges when zoomed in.
|
|
2513
|
+
// Thresholds come from ZOOM_REVEAL_CONFIG (baked at startup).
|
|
2514
|
+
float mappedZoom = pow(uRevealZoom, ${ZOOM_REVEAL_CONFIG.zoomCurveExp});
|
|
2515
|
+
float backdropReveal = smoothstep(${ZOOM_REVEAL_CONFIG.backdropRevealStart}, ${ZOOM_REVEAL_CONFIG.backdropRevealEnd}, mappedZoom);
|
|
2516
|
+
|
|
2517
|
+
vColor = color * uBackdropEnergy * extinction * horizonFade * scintillation * uBackdropGain * backdropReveal;
|
|
2414
2518
|
|
|
2415
2519
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
2416
2520
|
gl_Position = smartProject(mvPosition);
|
|
@@ -2450,55 +2554,6 @@ function createEngine({
|
|
|
2450
2554
|
points.frustumCulled = false;
|
|
2451
2555
|
backdropGroup.add(points);
|
|
2452
2556
|
}
|
|
2453
|
-
function createEditHoverRing() {
|
|
2454
|
-
const geo = new THREE6.PlaneGeometry(1, 1);
|
|
2455
|
-
const mat = createSmartMaterial({
|
|
2456
|
-
uniforms: {
|
|
2457
|
-
uRingSize: { value: 0.06 },
|
|
2458
|
-
uRingAlpha: { value: 0 },
|
|
2459
|
-
uRingColor: { value: new THREE6.Color(0.55, 0.88, 1) }
|
|
2460
|
-
},
|
|
2461
|
-
vertexShaderBody: `
|
|
2462
|
-
uniform float uRingSize;
|
|
2463
|
-
varying vec2 vUv;
|
|
2464
|
-
void main() {
|
|
2465
|
-
vUv = uv;
|
|
2466
|
-
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
2467
|
-
vec4 proj = smartProject(mvPos);
|
|
2468
|
-
if (proj.z > 4.0) { gl_Position = vec4(10.0, 10.0, 10.0, 1.0); return; }
|
|
2469
|
-
vec2 offset = position.xy * uRingSize * uScale;
|
|
2470
|
-
proj.xy += offset / vec2(uAspect, 1.0);
|
|
2471
|
-
vScreenPos = proj.xy / proj.w;
|
|
2472
|
-
gl_Position = proj;
|
|
2473
|
-
}
|
|
2474
|
-
`,
|
|
2475
|
-
fragmentShader: `
|
|
2476
|
-
varying vec2 vUv;
|
|
2477
|
-
uniform float uRingAlpha;
|
|
2478
|
-
uniform vec3 uRingColor;
|
|
2479
|
-
void main() {
|
|
2480
|
-
float alphaMask = getMaskAlpha();
|
|
2481
|
-
if (alphaMask < 0.01) discard;
|
|
2482
|
-
vec2 p = vUv * 2.0 - 1.0;
|
|
2483
|
-
float d = length(p);
|
|
2484
|
-
float ring = smoothstep(0.52, 0.62, d) * (1.0 - smoothstep(0.80, 0.92, d));
|
|
2485
|
-
float glow = (1.0 - smoothstep(0.55, 0.98, d)) * 0.18;
|
|
2486
|
-
float a = (ring + glow) * uRingAlpha * alphaMask;
|
|
2487
|
-
if (a < 0.005) discard;
|
|
2488
|
-
gl_FragColor = vec4(uRingColor * (ring * 1.2 + glow), a);
|
|
2489
|
-
}
|
|
2490
|
-
`,
|
|
2491
|
-
transparent: true,
|
|
2492
|
-
depthWrite: false,
|
|
2493
|
-
depthTest: false,
|
|
2494
|
-
side: THREE6.DoubleSide,
|
|
2495
|
-
blending: THREE6.AdditiveBlending
|
|
2496
|
-
});
|
|
2497
|
-
editHoverMesh = new THREE6.Mesh(geo, mat);
|
|
2498
|
-
editHoverMesh.renderOrder = 500;
|
|
2499
|
-
editHoverMesh.frustumCulled = false;
|
|
2500
|
-
scene.add(editHoverMesh);
|
|
2501
|
-
}
|
|
2502
2557
|
createSkyBackground();
|
|
2503
2558
|
createGround();
|
|
2504
2559
|
createAtmosphere();
|
|
@@ -2506,7 +2561,6 @@ function createEngine({
|
|
|
2506
2561
|
createSun();
|
|
2507
2562
|
createMilkyWay();
|
|
2508
2563
|
createBackdropStars();
|
|
2509
|
-
createEditHoverRing();
|
|
2510
2564
|
const raycaster = new THREE6.Raycaster();
|
|
2511
2565
|
raycaster.params.Points.threshold = 5;
|
|
2512
2566
|
new THREE6.Vector2();
|
|
@@ -2613,7 +2667,7 @@ function createEngine({
|
|
|
2613
2667
|
function getPosition(n) {
|
|
2614
2668
|
if (currentConfig?.arrangement) {
|
|
2615
2669
|
const arr = currentConfig.arrangement[n.id];
|
|
2616
|
-
if (arr) {
|
|
2670
|
+
if (arr?.position) {
|
|
2617
2671
|
const [px, py, pz] = arr.position;
|
|
2618
2672
|
if (pz === 0) {
|
|
2619
2673
|
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
@@ -2762,6 +2816,7 @@ function createEngine({
|
|
|
2762
2816
|
const starChapterIndices = [];
|
|
2763
2817
|
const starTestamentIndices = [];
|
|
2764
2818
|
const starDivisionIndices = [];
|
|
2819
|
+
const starRevealThresholds = [];
|
|
2765
2820
|
const chapterLineCutById = /* @__PURE__ */ new Map();
|
|
2766
2821
|
const chapterStarSizeById = /* @__PURE__ */ new Map();
|
|
2767
2822
|
const chapterWeightNormById = /* @__PURE__ */ new Map();
|
|
@@ -2798,12 +2853,23 @@ function createEngine({
|
|
|
2798
2853
|
} else if (minWeight === maxWeight) {
|
|
2799
2854
|
maxWeight = minWeight + 1;
|
|
2800
2855
|
}
|
|
2856
|
+
{
|
|
2857
|
+
const pctCap = THREE6.MathUtils.clamp(cfg.starSizeWeightPercentile ?? 0.95, 0.5, 1);
|
|
2858
|
+
const allWeights = [];
|
|
2859
|
+
for (const n of laidOut.nodes) {
|
|
2860
|
+
if (n.level === 3 && typeof n.weight === "number") allWeights.push(n.weight);
|
|
2861
|
+
}
|
|
2862
|
+
allWeights.sort((a, b) => a - b);
|
|
2863
|
+
const capIdx = Math.min(Math.floor(pctCap * allWeights.length), allWeights.length - 1);
|
|
2864
|
+
const cappedMax = allWeights[capIdx];
|
|
2865
|
+
if (cappedMax !== void 0 && cappedMax > minWeight) maxWeight = cappedMax;
|
|
2866
|
+
}
|
|
2801
2867
|
for (const n of laidOut.nodes) {
|
|
2802
2868
|
if (n.level === 3) {
|
|
2803
2869
|
let baseSize = 3.5;
|
|
2804
2870
|
let weightNorm = 0;
|
|
2805
2871
|
if (typeof n.weight === "number") {
|
|
2806
|
-
weightNorm = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
2872
|
+
weightNorm = THREE6.MathUtils.clamp((n.weight - minWeight) / (maxWeight - minWeight), 0, 1);
|
|
2807
2873
|
const sizeExp = cfg.starSizeExponent ?? 4;
|
|
2808
2874
|
const sizeScale = cfg.starSizeScale ?? 6;
|
|
2809
2875
|
baseSize = Math.pow(weightNorm, sizeExp) * 22 * sizeScale;
|
|
@@ -2828,6 +2894,14 @@ function createEngine({
|
|
|
2828
2894
|
starIndexToId.push(n.id);
|
|
2829
2895
|
const baseSize = chapterStarSizeById.get(n.id) ?? 3.5;
|
|
2830
2896
|
starSizes.push(baseSize);
|
|
2897
|
+
{
|
|
2898
|
+
const wn = chapterWeightNormById.get(n.id) ?? 0;
|
|
2899
|
+
starRevealThresholds.push(THREE6.MathUtils.lerp(
|
|
2900
|
+
-ZOOM_REVEAL_CONFIG.chapterFeather,
|
|
2901
|
+
ZOOM_REVEAL_CONFIG.chapterRevealMax,
|
|
2902
|
+
1 - wn
|
|
2903
|
+
));
|
|
2904
|
+
}
|
|
2831
2905
|
chapterLineCutById.set(
|
|
2832
2906
|
n.id,
|
|
2833
2907
|
THREE6.MathUtils.clamp(2.5 + baseSize * 0.45, 3, 40)
|
|
@@ -2978,6 +3052,7 @@ function createEngine({
|
|
|
2978
3052
|
starGeo.setAttribute("chapterIndex", new THREE6.Float32BufferAttribute(starChapterIndices, 1));
|
|
2979
3053
|
starGeo.setAttribute("testamentIndex", new THREE6.Float32BufferAttribute(starTestamentIndices, 1));
|
|
2980
3054
|
starGeo.setAttribute("divisionIndex", new THREE6.Float32BufferAttribute(starDivisionIndices, 1));
|
|
3055
|
+
starGeo.setAttribute("revealThreshold", new THREE6.Float32BufferAttribute(starRevealThresholds, 1));
|
|
2981
3056
|
const starMat = createSmartMaterial({
|
|
2982
3057
|
uniforms: {
|
|
2983
3058
|
pixelRatio: { value: renderer.getPixelRatio() },
|
|
@@ -2995,19 +3070,22 @@ function createEngine({
|
|
|
2995
3070
|
uFilterDivisionIndex: { value: -1 },
|
|
2996
3071
|
uFilterBookIndex: { value: -1 },
|
|
2997
3072
|
uFilterStrength: { value: 0 },
|
|
2998
|
-
uFilterDimFactor: { value: 0.08 }
|
|
3073
|
+
uFilterDimFactor: { value: 0.08 },
|
|
3074
|
+
uRevealZoom: { value: 0 }
|
|
2999
3075
|
},
|
|
3000
3076
|
vertexShaderBody: `
|
|
3001
|
-
attribute float size;
|
|
3002
|
-
attribute vec3 color;
|
|
3077
|
+
attribute float size;
|
|
3078
|
+
attribute vec3 color;
|
|
3003
3079
|
attribute float phase;
|
|
3004
3080
|
attribute float bookIndex;
|
|
3005
3081
|
attribute float chapterIndex;
|
|
3006
3082
|
attribute float testamentIndex;
|
|
3007
3083
|
attribute float divisionIndex;
|
|
3084
|
+
attribute float revealThreshold;
|
|
3008
3085
|
|
|
3009
3086
|
varying vec3 vColor;
|
|
3010
3087
|
varying float vSize;
|
|
3088
|
+
varying float vReveal;
|
|
3011
3089
|
uniform float pixelRatio;
|
|
3012
3090
|
|
|
3013
3091
|
uniform float uTime;
|
|
@@ -3024,6 +3102,7 @@ function createEngine({
|
|
|
3024
3102
|
uniform float uFilterBookIndex;
|
|
3025
3103
|
uniform float uFilterStrength;
|
|
3026
3104
|
uniform float uFilterDimFactor;
|
|
3105
|
+
uniform float uRevealZoom;
|
|
3027
3106
|
|
|
3028
3107
|
void main() {
|
|
3029
3108
|
vec3 nPos = normalize(position);
|
|
@@ -3086,11 +3165,17 @@ function createEngine({
|
|
|
3086
3165
|
float perceptualSize = pow(size, 0.7);
|
|
3087
3166
|
gl_PointSize = clamp((perceptualSize * sizeBoost * 20.0) * uScale * pixelRatio * (2000.0 / length(mvPosition.xyz)) * horizonFade, 1.0, 600.0);
|
|
3088
3167
|
vSize = gl_PointSize;
|
|
3168
|
+
|
|
3169
|
+
// Zoom-based reveal: faint stars hide at wide FOV, fade in as user zooms.
|
|
3170
|
+
// Exponent and feather baked from ZOOM_REVEAL_CONFIG at startup.
|
|
3171
|
+
float mappedZoom = pow(uRevealZoom, ${ZOOM_REVEAL_CONFIG.zoomCurveExp});
|
|
3172
|
+
vReveal = smoothstep(revealThreshold, revealThreshold + ${ZOOM_REVEAL_CONFIG.chapterFeather}, mappedZoom);
|
|
3089
3173
|
}
|
|
3090
3174
|
`,
|
|
3091
3175
|
fragmentShader: `
|
|
3092
3176
|
varying vec3 vColor;
|
|
3093
3177
|
varying float vSize;
|
|
3178
|
+
varying float vReveal;
|
|
3094
3179
|
void main() {
|
|
3095
3180
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
3096
3181
|
float d = length(coord) * 2.0;
|
|
@@ -3119,7 +3204,8 @@ function createEngine({
|
|
|
3119
3204
|
float spikeV = exp(-coord.x * coord.x * 180.0) * exp(-abs(coord.y) * 6.0);
|
|
3120
3205
|
float spikes = (spikeH + spikeV) * 0.18 * spikeFactor;
|
|
3121
3206
|
|
|
3122
|
-
|
|
3207
|
+
// vReveal drives the additive contribution (AdditiveBlending uses SRC_ALPHA).
|
|
3208
|
+
gl_FragColor = vec4(finalColor * (k + spikes) * alphaMask, vReveal);
|
|
3123
3209
|
}
|
|
3124
3210
|
`,
|
|
3125
3211
|
transparent: true,
|
|
@@ -3624,20 +3710,23 @@ function createEngine({
|
|
|
3624
3710
|
constellationLayer.load(cfg.constellations, (id) => {
|
|
3625
3711
|
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
3626
3712
|
const arr = cfg.arrangement[id];
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3713
|
+
const coords = arr.center ?? arr.position;
|
|
3714
|
+
if (coords) {
|
|
3715
|
+
if (coords[2] === 0) {
|
|
3716
|
+
const x = coords[0];
|
|
3717
|
+
const y = coords[1];
|
|
3718
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
3719
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
3720
|
+
const phi = Math.atan2(y, x);
|
|
3721
|
+
const theta = r_norm * (Math.PI / 2);
|
|
3722
|
+
return new THREE6.Vector3(
|
|
3723
|
+
Math.sin(theta) * Math.cos(phi),
|
|
3724
|
+
Math.cos(theta),
|
|
3725
|
+
Math.sin(theta) * Math.sin(phi)
|
|
3726
|
+
).multiplyScalar(radius);
|
|
3727
|
+
}
|
|
3728
|
+
return new THREE6.Vector3(coords[0], coords[1], coords[2]);
|
|
3639
3729
|
}
|
|
3640
|
-
return new THREE6.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
3641
3730
|
}
|
|
3642
3731
|
return getLayoutPosition(id);
|
|
3643
3732
|
}, getLayoutPosition);
|
|
@@ -3665,7 +3754,7 @@ function createEngine({
|
|
|
3665
3754
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
3666
3755
|
}
|
|
3667
3756
|
for (const item of constellationLayer.getItems()) {
|
|
3668
|
-
arr[item.config.id] = {
|
|
3757
|
+
arr[item.config.id] = { center: [item.center.x, item.center.y, item.center.z] };
|
|
3669
3758
|
}
|
|
3670
3759
|
Object.assign(arr, state.tempArrangement);
|
|
3671
3760
|
return arr;
|
|
@@ -3788,29 +3877,65 @@ function createEngine({
|
|
|
3788
3877
|
isMouseInWindow = false;
|
|
3789
3878
|
edgeHoverStart = 0;
|
|
3790
3879
|
}
|
|
3880
|
+
function screenSpacePickStar(mx, my, maxPx = 50) {
|
|
3881
|
+
if (!starPoints) return null;
|
|
3882
|
+
const attr = starPoints.geometry.attributes.position;
|
|
3883
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
3884
|
+
const w = rect.width;
|
|
3885
|
+
const h = rect.height;
|
|
3886
|
+
const uScale = globalUniforms.uScale.value;
|
|
3887
|
+
const uAspect = globalUniforms.uAspect.value;
|
|
3888
|
+
let bestIdx = -1;
|
|
3889
|
+
let bestDist2 = maxPx * maxPx;
|
|
3890
|
+
const worldPos = new THREE6.Vector3();
|
|
3891
|
+
for (let i = 0; i < attr.count; i++) {
|
|
3892
|
+
worldPos.set(attr.getX(i), attr.getY(i), attr.getZ(i));
|
|
3893
|
+
const proj = smartProjectJS(worldPos);
|
|
3894
|
+
if (currentProjection.isClipped(proj.z)) continue;
|
|
3895
|
+
const sx = (proj.x * uScale / uAspect * 0.5 + 0.5) * w;
|
|
3896
|
+
const sy = (-(proj.y * uScale) * 0.5 + 0.5) * h;
|
|
3897
|
+
const dx = mx - sx;
|
|
3898
|
+
const dy = my - sy;
|
|
3899
|
+
const d2 = dx * dx + dy * dy;
|
|
3900
|
+
if (d2 < bestDist2) {
|
|
3901
|
+
bestDist2 = d2;
|
|
3902
|
+
bestIdx = i;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (bestIdx < 0) return null;
|
|
3906
|
+
return {
|
|
3907
|
+
index: bestIdx,
|
|
3908
|
+
worldPos: new THREE6.Vector3(attr.getX(bestIdx), attr.getY(bestIdx), attr.getZ(bestIdx))
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3791
3911
|
function onMouseDown(e) {
|
|
3792
3912
|
state.lastMouseX = e.clientX;
|
|
3793
3913
|
state.lastMouseY = e.clientY;
|
|
3794
3914
|
if (currentConfig?.editable) {
|
|
3915
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
3916
|
+
const mX = e.clientX - rect.left;
|
|
3917
|
+
const mY = e.clientY - rect.top;
|
|
3918
|
+
const starHit = screenSpacePickStar(mX, mY);
|
|
3919
|
+
if (starHit) {
|
|
3920
|
+
state.dragMode = "node";
|
|
3921
|
+
state.draggedStarIndex = starHit.index;
|
|
3922
|
+
state.draggedNodeId = starIndexToId[starHit.index] ?? null;
|
|
3923
|
+
state.draggedDist = starHit.worldPos.length();
|
|
3924
|
+
state.draggedGroup = null;
|
|
3925
|
+
state.tempArrangement = {};
|
|
3926
|
+
state.velocityX = 0;
|
|
3927
|
+
state.velocityY = 0;
|
|
3928
|
+
return;
|
|
3929
|
+
}
|
|
3795
3930
|
const hit = pick(e);
|
|
3796
|
-
if (hit) {
|
|
3931
|
+
if (hit && (hit.type === "label" || hit.type === "constellation")) {
|
|
3797
3932
|
state.dragMode = "node";
|
|
3798
3933
|
state.draggedNodeId = hit.node.id;
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
const starWorldPos = new THREE6.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3802
|
-
state.draggedDist = starWorldPos.length();
|
|
3803
|
-
} else {
|
|
3804
|
-
state.draggedDist = hit.point.length();
|
|
3805
|
-
}
|
|
3806
|
-
document.body.style.cursor = "crosshair";
|
|
3934
|
+
state.draggedDist = hit.point.length();
|
|
3935
|
+
state.draggedStarIndex = -1;
|
|
3807
3936
|
state.velocityX = 0;
|
|
3808
3937
|
state.velocityY = 0;
|
|
3809
|
-
if (hit.type === "
|
|
3810
|
-
state.draggedStarIndex = hit.index ?? -1;
|
|
3811
|
-
state.draggedGroup = null;
|
|
3812
|
-
state.tempArrangement = {};
|
|
3813
|
-
} else if (hit.type === "label") {
|
|
3938
|
+
if (hit.type === "label") {
|
|
3814
3939
|
const bookId = hit.node.id;
|
|
3815
3940
|
const children = [];
|
|
3816
3941
|
if (starPoints && starPoints.geometry.attributes.position) {
|
|
@@ -3825,12 +3950,21 @@ function createEngine({
|
|
|
3825
3950
|
}
|
|
3826
3951
|
}
|
|
3827
3952
|
}
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3953
|
+
const constellations = [];
|
|
3954
|
+
for (const cItem of constellationLayer.getItems()) {
|
|
3955
|
+
const anchored = cItem.config.anchors.some((anchorId) => {
|
|
3956
|
+
const n = nodeById.get(anchorId);
|
|
3957
|
+
return n?.parent === bookId;
|
|
3958
|
+
});
|
|
3959
|
+
if (anchored) {
|
|
3960
|
+
constellations.push({ id: cItem.config.id, initialCenter: cItem.center.clone() });
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children, constellations };
|
|
3964
|
+
} else {
|
|
3965
|
+
state.draggedGroup = { labelInitialPos: hit.point.clone(), children: [], constellations: [] };
|
|
3833
3966
|
}
|
|
3967
|
+
return;
|
|
3834
3968
|
}
|
|
3835
3969
|
return;
|
|
3836
3970
|
}
|
|
@@ -3857,7 +3991,6 @@ function createEngine({
|
|
|
3857
3991
|
const attr = starPoints.geometry.attributes.position;
|
|
3858
3992
|
attr.setXYZ(idx, newPos.x, newPos.y, newPos.z);
|
|
3859
3993
|
attr.needsUpdate = true;
|
|
3860
|
-
editHoverTargetPos = newPos.clone();
|
|
3861
3994
|
const starId = starIndexToId[idx];
|
|
3862
3995
|
if (starId) state.tempArrangement[starId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
3863
3996
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
@@ -3873,7 +4006,7 @@ function createEngine({
|
|
|
3873
4006
|
const vE = newPos.clone().normalize();
|
|
3874
4007
|
cItem.mesh.quaternion.setFromUnitVectors(vS, vE);
|
|
3875
4008
|
cItem.center.copy(newPos);
|
|
3876
|
-
state.tempArrangement[state.draggedNodeId] = {
|
|
4009
|
+
state.tempArrangement[state.draggedNodeId] = { center: [newPos.x, newPos.y, newPos.z] };
|
|
3877
4010
|
}
|
|
3878
4011
|
}
|
|
3879
4012
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
@@ -3892,6 +4025,20 @@ function createEngine({
|
|
|
3892
4025
|
}
|
|
3893
4026
|
attr.needsUpdate = true;
|
|
3894
4027
|
}
|
|
4028
|
+
if (group.constellations.length > 0) {
|
|
4029
|
+
for (const { id, initialCenter } of group.constellations) {
|
|
4030
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === id);
|
|
4031
|
+
if (cItem) {
|
|
4032
|
+
const newCenter = initialCenter.clone().applyQuaternion(q);
|
|
4033
|
+
cItem.center.copy(newCenter);
|
|
4034
|
+
cItem.mesh.quaternion.setFromUnitVectors(
|
|
4035
|
+
initialCenter.clone().normalize(),
|
|
4036
|
+
newCenter.clone().normalize()
|
|
4037
|
+
);
|
|
4038
|
+
state.tempArrangement[id] = { center: [newCenter.x, newCenter.y, newCenter.z] };
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3895
4042
|
}
|
|
3896
4043
|
} else if (state.dragMode === "camera") {
|
|
3897
4044
|
const deltaX = e.clientX - state.lastMouseX;
|
|
@@ -3899,13 +4046,15 @@ function createEngine({
|
|
|
3899
4046
|
state.lastMouseX = e.clientX;
|
|
3900
4047
|
state.lastMouseY = e.clientY;
|
|
3901
4048
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
3902
|
-
const
|
|
3903
|
-
const
|
|
3904
|
-
|
|
3905
|
-
|
|
4049
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4050
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4051
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4052
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4053
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4054
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
3906
4055
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
3907
|
-
state.velocityX =
|
|
3908
|
-
state.velocityY =
|
|
4056
|
+
state.velocityX = moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4057
|
+
state.velocityY = moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
3909
4058
|
state.lon = state.targetLon;
|
|
3910
4059
|
state.lat = state.targetLat;
|
|
3911
4060
|
} else {
|
|
@@ -3925,26 +4074,17 @@ function createEngine({
|
|
|
3925
4074
|
hoverLabelMesh.position.copy(hit.point);
|
|
3926
4075
|
hoverLabelMat.uniforms.uAlpha.value = 1;
|
|
3927
4076
|
hoverLabelMesh.visible = true;
|
|
3928
|
-
if (currentConfig?.editable && hit.type === "star" && hit.index !== void 0 && starPoints) {
|
|
3929
|
-
const attr = starPoints.geometry.attributes.position;
|
|
3930
|
-
editHoverTargetPos = new THREE6.Vector3(attr.getX(hit.index), attr.getY(hit.index), attr.getZ(hit.index));
|
|
3931
|
-
} else if (currentConfig?.editable && hit.type === "star") {
|
|
3932
|
-
editHoverTargetPos = hit.point.clone();
|
|
3933
|
-
}
|
|
3934
4077
|
} else {
|
|
3935
4078
|
currentHoverNodeId = null;
|
|
3936
4079
|
hoverLabelMat.uniforms.uAlpha.value = 0;
|
|
3937
4080
|
hoverLabelMesh.visible = false;
|
|
3938
|
-
if (currentConfig?.editable && state.dragMode !== "node") {
|
|
3939
|
-
editHoverTargetPos = null;
|
|
3940
|
-
}
|
|
3941
4081
|
}
|
|
3942
4082
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
3943
4083
|
handlers._lastHoverId = hit?.node.id;
|
|
3944
4084
|
handlers.onHover?.(hit?.node);
|
|
3945
4085
|
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
3946
4086
|
}
|
|
3947
|
-
document.body.style.cursor = hit ? currentConfig?.editable ? "
|
|
4087
|
+
document.body.style.cursor = hit ? currentConfig?.editable && hit.type === "star" ? "grab" : "pointer" : "default";
|
|
3948
4088
|
}
|
|
3949
4089
|
}
|
|
3950
4090
|
function onMouseUp(e) {
|
|
@@ -3954,7 +4094,6 @@ function createEngine({
|
|
|
3954
4094
|
if (state.dragMode === "node") {
|
|
3955
4095
|
const fullArr = getFullArrangement();
|
|
3956
4096
|
handlers.onArrangementChange?.(fullArr);
|
|
3957
|
-
editDropFlash = 1;
|
|
3958
4097
|
state.dragMode = "none";
|
|
3959
4098
|
state.draggedNodeId = null;
|
|
3960
4099
|
state.draggedStarIndex = -1;
|
|
@@ -3997,20 +4136,30 @@ function createEngine({
|
|
|
3997
4136
|
const aspect = container.clientWidth / container.clientHeight;
|
|
3998
4137
|
renderer.domElement.getBoundingClientRect();
|
|
3999
4138
|
const vBefore = getMouseViewVector(state.fov, aspect);
|
|
4000
|
-
const
|
|
4139
|
+
const zoomResistance = THREE6.MathUtils.lerp(
|
|
4140
|
+
1,
|
|
4141
|
+
ENGINE_CONFIG.zoomResistanceWideFov,
|
|
4142
|
+
THREE6.MathUtils.smoothstep(state.fov, 24, 100)
|
|
4143
|
+
);
|
|
4144
|
+
const zoomSpeed = 1e-3 * state.fov * zoomResistance;
|
|
4001
4145
|
state.fov += e.deltaY * zoomSpeed;
|
|
4002
4146
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
4003
4147
|
handlers.onFovChange?.(state.fov);
|
|
4004
4148
|
updateUniforms();
|
|
4005
4149
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
4006
4150
|
const quaternion = new THREE6.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
4007
|
-
const dampStartFov =
|
|
4008
|
-
const dampEndFov =
|
|
4151
|
+
const dampStartFov = 32;
|
|
4152
|
+
const dampEndFov = 110;
|
|
4009
4153
|
let spinAmount = 1;
|
|
4010
4154
|
if (state.fov > dampStartFov) {
|
|
4011
4155
|
const t = Math.max(0, Math.min(1, (state.fov - dampStartFov) / (dampEndFov - dampStartFov)));
|
|
4012
|
-
spinAmount = 1 - Math.pow(t, 1.
|
|
4156
|
+
spinAmount = 1 - Math.pow(t, 1.35) * 0.92;
|
|
4013
4157
|
}
|
|
4158
|
+
const blendForSpin = getBlendForZenithControl();
|
|
4159
|
+
const blendSpinDamp = THREE6.MathUtils.smoothstep(blendForSpin, 0.58, 0.9);
|
|
4160
|
+
spinAmount *= 1 - 0.88 * blendSpinDamp;
|
|
4161
|
+
if (zenithProjectionLockActive) spinAmount = Math.min(spinAmount, 0.02);
|
|
4162
|
+
spinAmount = Math.max(0.02, Math.min(1, spinAmount));
|
|
4014
4163
|
if (spinAmount < 0.999) {
|
|
4015
4164
|
const identityQuat = new THREE6.Quaternion();
|
|
4016
4165
|
quaternion.slerp(identityQuat, 1 - spinAmount);
|
|
@@ -4032,9 +4181,10 @@ function createEngine({
|
|
|
4032
4181
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
4033
4182
|
const newUp = new THREE6.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
4034
4183
|
camera.up.copy(newUp);
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4184
|
+
const zenithBiasStartFov = getZenithBiasStartFov();
|
|
4185
|
+
if (!zenithProjectionLockActive && !getSceneDebug()?.disableZenithBias && !isInTransitionFreezeBand(state.fov) && e.deltaY > 0 && state.fov > zenithBiasStartFov) {
|
|
4186
|
+
const range = ENGINE_CONFIG.maxFov - zenithBiasStartFov;
|
|
4187
|
+
let t = (state.fov - zenithBiasStartFov) / range;
|
|
4038
4188
|
t = Math.max(0, Math.min(1, t));
|
|
4039
4189
|
const bias = ENGINE_CONFIG.zenithStrength * t;
|
|
4040
4190
|
const zenithLat = Math.PI / 2 - 1e-3;
|
|
@@ -4126,13 +4276,15 @@ function createEngine({
|
|
|
4126
4276
|
}
|
|
4127
4277
|
}
|
|
4128
4278
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
4129
|
-
const
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4132
|
-
|
|
4279
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4280
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4281
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4282
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4283
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4284
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
4133
4285
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
4134
|
-
state.velocityX =
|
|
4135
|
-
state.velocityY =
|
|
4286
|
+
state.velocityX = moveX * ENGINE_CONFIG.dragSpeed * speedScale;
|
|
4287
|
+
state.velocityY = moveY * ENGINE_CONFIG.dragSpeed * speedScale * latFactor;
|
|
4136
4288
|
state.lon = state.targetLon;
|
|
4137
4289
|
state.lat = state.targetLat;
|
|
4138
4290
|
} else if (touches.length === 2) {
|
|
@@ -4144,9 +4296,10 @@ function createEngine({
|
|
|
4144
4296
|
state.fov = state.pinchStartFov / scale;
|
|
4145
4297
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
4146
4298
|
handlers.onFovChange?.(state.fov);
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4299
|
+
const zenithBiasStartFov = getZenithBiasStartFov();
|
|
4300
|
+
if (!zenithProjectionLockActive && !getSceneDebug()?.disableZenithBias && !isInTransitionFreezeBand(state.fov) && state.fov > prevFov && state.fov > zenithBiasStartFov) {
|
|
4301
|
+
const range = ENGINE_CONFIG.maxFov - zenithBiasStartFov;
|
|
4302
|
+
let t = (state.fov - zenithBiasStartFov) / range;
|
|
4150
4303
|
t = Math.max(0, Math.min(1, t));
|
|
4151
4304
|
const bias = ENGINE_CONFIG.zenithStrength * t;
|
|
4152
4305
|
const zenithLat = Math.PI / 2 - 1e-3;
|
|
@@ -4159,8 +4312,12 @@ function createEngine({
|
|
|
4159
4312
|
state.lastMouseX = center.x;
|
|
4160
4313
|
state.lastMouseY = center.y;
|
|
4161
4314
|
const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
|
|
4162
|
-
|
|
4163
|
-
|
|
4315
|
+
const latFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4316
|
+
const massFactor = getMovementMassFactor(state.fov);
|
|
4317
|
+
const moveX = compressInputDelta(deltaX) * massFactor;
|
|
4318
|
+
const moveY = compressInputDelta(deltaY) * massFactor;
|
|
4319
|
+
state.targetLon += moveX * ENGINE_CONFIG.dragSpeed * speedScale * 0.5;
|
|
4320
|
+
state.targetLat += moveY * ENGINE_CONFIG.dragSpeed * speedScale * 0.5 * latFactor;
|
|
4164
4321
|
state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
|
|
4165
4322
|
state.lon = state.targetLon;
|
|
4166
4323
|
state.lat = state.targetLat;
|
|
@@ -4323,7 +4480,9 @@ function createEngine({
|
|
|
4323
4480
|
if (inZoneX || inZoneY) {
|
|
4324
4481
|
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
4325
4482
|
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
4326
|
-
const
|
|
4483
|
+
const edgeMassFactor = getMovementMassFactor(state.fov, ENGINE_CONFIG.edgePanMassWideFov);
|
|
4484
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov) * edgeMassFactor;
|
|
4485
|
+
const verticalPanFactor = getVerticalPanFactor(state.fov, state.lat);
|
|
4327
4486
|
if (mouseNDC.x < -1 + t) {
|
|
4328
4487
|
const s = (-1 + t - mouseNDC.x) / t;
|
|
4329
4488
|
panX = -s * s * speedBase;
|
|
@@ -4333,10 +4492,10 @@ function createEngine({
|
|
|
4333
4492
|
}
|
|
4334
4493
|
if (mouseNDC.y < -1 + t) {
|
|
4335
4494
|
const s = (-1 + t - mouseNDC.y) / t;
|
|
4336
|
-
panY = -s * s * speedBase;
|
|
4495
|
+
panY = -s * s * speedBase * verticalPanFactor;
|
|
4337
4496
|
} else if (mouseNDC.y > 1 - t) {
|
|
4338
4497
|
const s = (mouseNDC.y - (1 - t)) / t;
|
|
4339
|
-
panY = s * s * speedBase;
|
|
4498
|
+
panY = s * s * speedBase * verticalPanFactor;
|
|
4340
4499
|
}
|
|
4341
4500
|
}
|
|
4342
4501
|
} else {
|
|
@@ -4368,14 +4527,32 @@ function createEngine({
|
|
|
4368
4527
|
state.targetLat = state.lat;
|
|
4369
4528
|
} else if (!state.isDragging && !flyToActive) {
|
|
4370
4529
|
state.lon += state.velocityX;
|
|
4530
|
+
state.velocityY *= getVerticalPanFactor(state.fov, state.lat);
|
|
4371
4531
|
state.lat += state.velocityY;
|
|
4372
|
-
const
|
|
4532
|
+
const baseDamping = isTouchDevice ? ENGINE_CONFIG.touchInertiaDamping : ENGINE_CONFIG.inertiaDamping;
|
|
4533
|
+
const speed = Math.hypot(state.velocityX, state.velocityY);
|
|
4534
|
+
const damping = speed < ENGINE_CONFIG.lowSpeedVelocityThreshold ? Math.min(baseDamping, ENGINE_CONFIG.lowSpeedInertiaDamping) : baseDamping;
|
|
4373
4535
|
state.velocityX *= damping;
|
|
4374
4536
|
state.velocityY *= damping;
|
|
4375
|
-
if (Math.abs(state.velocityX) <
|
|
4376
|
-
if (Math.abs(state.velocityY) <
|
|
4537
|
+
if (Math.abs(state.velocityX) < ENGINE_CONFIG.velocityStopThreshold) state.velocityX = 0;
|
|
4538
|
+
if (Math.abs(state.velocityY) < ENGINE_CONFIG.velocityStopThreshold) state.velocityY = 0;
|
|
4377
4539
|
}
|
|
4378
4540
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
4541
|
+
if (!flyToActive) {
|
|
4542
|
+
const latDeg = THREE6.MathUtils.radToDeg(state.lat);
|
|
4543
|
+
if (latDeg < HORIZON_ZOOM_CONFIG.latStartDeg) {
|
|
4544
|
+
const t = THREE6.MathUtils.clamp(latDeg / HORIZON_ZOOM_CONFIG.latStartDeg, 0, 1);
|
|
4545
|
+
const maxFov = THREE6.MathUtils.lerp(
|
|
4546
|
+
HORIZON_ZOOM_CONFIG.safeFovAtHorizon,
|
|
4547
|
+
ENGINE_CONFIG.maxFov,
|
|
4548
|
+
t
|
|
4549
|
+
);
|
|
4550
|
+
if (state.fov > maxFov) {
|
|
4551
|
+
state.fov = THREE6.MathUtils.lerp(state.fov, maxFov, HORIZON_ZOOM_CONFIG.lerpRate);
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
applyZenithAutoCenter();
|
|
4379
4556
|
const y = Math.sin(state.lat);
|
|
4380
4557
|
const r = Math.cos(state.lat);
|
|
4381
4558
|
const x = r * Math.sin(state.lon);
|
|
@@ -4388,11 +4565,13 @@ function createEngine({
|
|
|
4388
4565
|
camera.updateMatrixWorld();
|
|
4389
4566
|
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
4390
4567
|
if (groundMaterial?.uniforms?.uZenithFlatten) {
|
|
4391
|
-
const
|
|
4568
|
+
const targetFlatten = getSceneDebug()?.disableZenithFlatten ? 0 : THREE6.MathUtils.smoothstep(
|
|
4392
4569
|
state.lat,
|
|
4393
4570
|
THREE6.MathUtils.degToRad(68),
|
|
4394
4571
|
THREE6.MathUtils.degToRad(88)
|
|
4395
4572
|
);
|
|
4573
|
+
const prevFlatten = Number(groundMaterial.uniforms.uZenithFlatten.value ?? 0);
|
|
4574
|
+
const flatten = isInTransitionFreezeBand(state.fov) ? THREE6.MathUtils.clamp(targetFlatten, prevFlatten - 0.01, prevFlatten + 0.01) : targetFlatten;
|
|
4396
4575
|
groundMaterial.uniforms.uZenithFlatten.value = flatten;
|
|
4397
4576
|
}
|
|
4398
4577
|
updateUniforms();
|
|
@@ -4409,6 +4588,11 @@ function createEngine({
|
|
|
4409
4588
|
const baseArtOpacity = THREE6.MathUtils.clamp(currentConfig?.constellationBaseOpacity ?? 1, 0, 300);
|
|
4410
4589
|
constellationLayer.setGlobalOpacity?.(artFader.eased * baseArtOpacity);
|
|
4411
4590
|
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
4591
|
+
const revealZoom = currentConfig?.starZoomReveal ?? true ? THREE6.MathUtils.clamp(
|
|
4592
|
+
(ZOOM_REVEAL_CONFIG.wideFov - state.fov) / (ZOOM_REVEAL_CONFIG.wideFov - ZOOM_REVEAL_CONFIG.narrowFov),
|
|
4593
|
+
0,
|
|
4594
|
+
1
|
|
4595
|
+
) : 1;
|
|
4412
4596
|
if (backdropStarsMaterial?.uniforms) {
|
|
4413
4597
|
const minGain = THREE6.MathUtils.clamp(currentConfig?.backdropWideFovGain ?? 0.42, 0, 1);
|
|
4414
4598
|
const fovT = THREE6.MathUtils.smoothstep(state.fov, 24, 100);
|
|
@@ -4416,6 +4600,11 @@ function createEngine({
|
|
|
4416
4600
|
backdropStarsMaterial.uniforms.uBackdropGain.value = gain;
|
|
4417
4601
|
backdropStarsMaterial.uniforms.uBackdropEnergy.value = THREE6.MathUtils.clamp(currentConfig?.backdropEnergy ?? 2.2, 0.2, 5);
|
|
4418
4602
|
backdropStarsMaterial.uniforms.uBackdropSizeExp.value = THREE6.MathUtils.clamp(currentConfig?.backdropSizeExponent ?? 0.9, 0.4, 1.4);
|
|
4603
|
+
backdropStarsMaterial.uniforms.uRevealZoom.value = revealZoom;
|
|
4604
|
+
}
|
|
4605
|
+
if (starPoints?.material) {
|
|
4606
|
+
const sm = starPoints.material;
|
|
4607
|
+
if (sm.uniforms.uRevealZoom) sm.uniforms.uRevealZoom.value = revealZoom;
|
|
4419
4608
|
}
|
|
4420
4609
|
if (skyBackgroundMesh) skyBackgroundMesh.visible = currentConfig?.background !== "transparent";
|
|
4421
4610
|
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
@@ -4425,32 +4614,6 @@ function createEngine({
|
|
|
4425
4614
|
if (sunDiscMesh) sunDiscMesh.visible = showSun;
|
|
4426
4615
|
if (sunHaloMesh) sunHaloMesh.visible = showSun;
|
|
4427
4616
|
if (milkyWayMesh) milkyWayMesh.visible = currentConfig?.showMilkyWay ?? true;
|
|
4428
|
-
if (editHoverMesh) {
|
|
4429
|
-
const ringMat = editHoverMesh.material;
|
|
4430
|
-
const isEditing = currentConfig?.editable ?? false;
|
|
4431
|
-
const isDraggingStar = state.dragMode === "node" && state.draggedStarIndex !== -1;
|
|
4432
|
-
const hasTarget = isEditing && editHoverTargetPos !== null;
|
|
4433
|
-
if (hasTarget) {
|
|
4434
|
-
editHoverMesh.position.copy(editHoverTargetPos);
|
|
4435
|
-
const pulseBoost = editDropFlash * 1.8;
|
|
4436
|
-
const targetAlpha = 0.8 + pulseBoost;
|
|
4437
|
-
ringMat.uniforms.uRingAlpha.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, targetAlpha, 0.15);
|
|
4438
|
-
const tGold = isDraggingStar ? 1 : editDropFlash;
|
|
4439
|
-
const targetColor = new THREE6.Color(
|
|
4440
|
-
THREE6.MathUtils.lerp(0.55, 1, tGold),
|
|
4441
|
-
THREE6.MathUtils.lerp(0.88, 0.82, tGold),
|
|
4442
|
-
THREE6.MathUtils.lerp(1, 0.18, tGold)
|
|
4443
|
-
);
|
|
4444
|
-
ringMat.uniforms.uRingColor.value.lerp(targetColor, 0.18);
|
|
4445
|
-
const baseSize = isDraggingStar ? 0.075 : 0.06;
|
|
4446
|
-
const targetSize = baseSize * (1 + editDropFlash * 0.7);
|
|
4447
|
-
ringMat.uniforms.uRingSize.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingSize.value, targetSize, 0.18);
|
|
4448
|
-
editDropFlash = Math.max(0, editDropFlash - dt * 3);
|
|
4449
|
-
} else {
|
|
4450
|
-
ringMat.uniforms.uRingAlpha.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingAlpha.value, 0, 0.15);
|
|
4451
|
-
ringMat.uniforms.uRingSize.value = THREE6.MathUtils.lerp(ringMat.uniforms.uRingSize.value, 0.06, 0.2);
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
4617
|
if (constellationLines) {
|
|
4455
4618
|
constellationLines.visible = linesFader.eased > 0.01;
|
|
4456
4619
|
if (constellationLines.visible && constellationLines.material) {
|
|
@@ -4558,12 +4721,6 @@ function createEngine({
|
|
|
4558
4721
|
skyBackgroundMesh.material.dispose();
|
|
4559
4722
|
skyBackgroundMesh = null;
|
|
4560
4723
|
}
|
|
4561
|
-
if (editHoverMesh) {
|
|
4562
|
-
scene.remove(editHoverMesh);
|
|
4563
|
-
editHoverMesh.geometry.dispose();
|
|
4564
|
-
editHoverMesh.material.dispose();
|
|
4565
|
-
editHoverMesh = null;
|
|
4566
|
-
}
|
|
4567
4724
|
renderer.dispose();
|
|
4568
4725
|
renderer.domElement.remove();
|
|
4569
4726
|
}
|
|
@@ -4606,7 +4763,7 @@ function createEngine({
|
|
|
4606
4763
|
}
|
|
4607
4764
|
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled, setHierarchyFilter, flyTo, setProjection };
|
|
4608
4765
|
}
|
|
4609
|
-
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
4766
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG, HORIZON_ZOOM_CONFIG, ZOOM_REVEAL_CONFIG;
|
|
4610
4767
|
var init_createEngine = __esm({
|
|
4611
4768
|
"src/engine/createEngine.ts"() {
|
|
4612
4769
|
init_layout();
|
|
@@ -4621,8 +4778,28 @@ var init_createEngine = __esm({
|
|
|
4621
4778
|
defaultFov: 35,
|
|
4622
4779
|
dragSpeed: 125e-5,
|
|
4623
4780
|
inertiaDamping: 0.92,
|
|
4781
|
+
lowSpeedInertiaDamping: 0.78,
|
|
4782
|
+
lowSpeedVelocityThreshold: 25e-4,
|
|
4783
|
+
velocityStopThreshold: 4e-5,
|
|
4784
|
+
zoomResistanceWideFov: 0.82,
|
|
4785
|
+
movementMassWideFov: 0.74,
|
|
4786
|
+
edgePanMassWideFov: 0.68,
|
|
4787
|
+
inputCompression: 0.018,
|
|
4624
4788
|
blendStart: 35,
|
|
4625
4789
|
blendEnd: 83,
|
|
4790
|
+
freezeBandStartFov: 76,
|
|
4791
|
+
freezeBandEndFov: 84,
|
|
4792
|
+
zenithBiasStartFov: 85,
|
|
4793
|
+
zenithLockBlendEnter: 0.9,
|
|
4794
|
+
zenithLockBlendExit: 0.8,
|
|
4795
|
+
zenithAutoCenterBlendStart: 0.62,
|
|
4796
|
+
zenithAutoCenterBlendEnd: 0.9,
|
|
4797
|
+
zenithAutoCenterMinLerp: 0.012,
|
|
4798
|
+
zenithAutoCenterMaxLerp: 0.16,
|
|
4799
|
+
verticalPanDampStartFov: 72,
|
|
4800
|
+
verticalPanDampEndFov: 96,
|
|
4801
|
+
verticalPanDampLatStartDeg: 45,
|
|
4802
|
+
verticalPanDampLatEndDeg: 82,
|
|
4626
4803
|
zenithStartFov: 75,
|
|
4627
4804
|
zenithStrength: 0.15,
|
|
4628
4805
|
horizonLockStrength: 0.05,
|
|
@@ -4649,6 +4826,30 @@ var init_createEngine = __esm({
|
|
|
4649
4826
|
pulseDuration: 2,
|
|
4650
4827
|
delayPerChapter: 0.1
|
|
4651
4828
|
};
|
|
4829
|
+
HORIZON_ZOOM_CONFIG = {
|
|
4830
|
+
latStartDeg: 20,
|
|
4831
|
+
// coupling is fully off above this elevation
|
|
4832
|
+
safeFovAtHorizon: 60,
|
|
4833
|
+
// max FOV at the horizon (below freeze-band threshold)
|
|
4834
|
+
lerpRate: 0.03
|
|
4835
|
+
// gentle — should feel like a natural breathing-in
|
|
4836
|
+
};
|
|
4837
|
+
ZOOM_REVEAL_CONFIG = {
|
|
4838
|
+
wideFov: 120,
|
|
4839
|
+
// above this FOV, revealZoom = 0 (nothing new revealed)
|
|
4840
|
+
narrowFov: 8,
|
|
4841
|
+
// below this FOV, revealZoom = 1 (everything visible)
|
|
4842
|
+
zoomCurveExp: 1.8,
|
|
4843
|
+
// non-linear curve exponent (try 1.5 – 2.5)
|
|
4844
|
+
chapterRevealMax: 0.5,
|
|
4845
|
+
// faintest chapter star threshold — visible by ~fov 35
|
|
4846
|
+
chapterFeather: 0.1,
|
|
4847
|
+
// smoothstep width for chapter star fade-in
|
|
4848
|
+
backdropRevealStart: 0.4,
|
|
4849
|
+
// backdrop starts appearing at this mappedZoom
|
|
4850
|
+
backdropRevealEnd: 0.65
|
|
4851
|
+
// backdrop fully visible at this mappedZoom
|
|
4852
|
+
};
|
|
4652
4853
|
}
|
|
4653
4854
|
});
|
|
4654
4855
|
var StarMap = forwardRef(
|