release: bump version to 0.29.0
This commit is contained in:
@@ -8,6 +8,23 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.29.0] — 2026-04-20
|
||||
|
||||
### ✨ Highlights
|
||||
- Earth 新增天球层第一版:引入真实全天星图、亮星层与太阳/月亮位置计算,地球场景首次具备可校准的天文背景
|
||||
- 地球昼夜分隔升级为更明显的日夜增强效果,夜面、晨昏带和太阳方向联动更容易直接读出来
|
||||
|
||||
### 🔧 Improvements
|
||||
- 新增 `celestial.js` 模块和 `assets/celestial/` 资源目录,统一管理星图、亮星数据以及太阳/月亮与光照同步
|
||||
- 卫星图例改为按倾角分组,严格固定为“赤道轨道 → 低倾角轨道 → 中倾角轨道 → 高倾角轨道 → 逆行轨道”顺序,并全部中文化
|
||||
- 图层面板补齐关闭按钮,拖拽脱离左列后不再被流布局 margin 影响,能够真正贴到品牌面板下沿
|
||||
|
||||
### 🐛 Fixes
|
||||
- 修复天球球壳放大后被相机 far plane 裁剪导致的外层黑环问题
|
||||
- 修复图层面板在左侧上移时始终与 brand panel 保持额外间距的问题
|
||||
|
||||
---
|
||||
|
||||
## [0.28.2] — 2026-04-20
|
||||
|
||||
### ✨ Highlights
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.28.2`
|
||||
- `dev` 当前开发分支历史推导到:`0.29.0`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.29.0` | feature | `dev` | `pending` | Earth 新增天球背景与太阳/月亮位置层,强化昼夜分隔并收口卫星图例与图层面板交互 |
|
||||
| `0.28.2` | bugfix | `dev` | `pending` | 修正媒体情报 tab 尺寸记忆与切换锚点逻辑,并清理 docs 根目录遗留旧路径文档 |
|
||||
| `0.28.1` | bugfix | `dev` | `pending` | 收口 Earth 媒体情报面板命名与 tab 文案,整理 docs 分组并归档已完成/废弃计划文档 |
|
||||
| `0.28.0` | feature | `dev` | `pending` | 合并 Earth 媒体情报面板,整合新闻直播与态势聚合 tab,并稳定 TV/news 的 reform、resize 与共享 HUD 行为 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.28.2",
|
||||
"version": "0.29.0",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
30
frontend/public/earth/assets/celestial/bright-stars.json
Normal file
30
frontend/public/earth/assets/celestial/bright-stars.json
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{ "id": 32349, "name": "Sirius", "raDeg": 101.2875, "decDeg": -16.7161, "mag": -1.46, "colorIndex": 0.00 },
|
||||
{ "id": 30438, "name": "Canopus", "raDeg": 95.9879, "decDeg": -52.6957, "mag": -0.74, "colorIndex": 0.15 },
|
||||
{ "id": 69673, "name": "Arcturus", "raDeg": 213.9153, "decDeg": 19.1824, "mag": -0.05, "colorIndex": 1.23 },
|
||||
{ "id": 71683, "name": "Alpha Centauri", "raDeg": 219.9021, "decDeg": -60.8339, "mag": -0.01, "colorIndex": 0.71 },
|
||||
{ "id": 91262, "name": "Vega", "raDeg": 279.2347, "decDeg": 38.7837, "mag": 0.03, "colorIndex": 0.00 },
|
||||
{ "id": 24608, "name": "Capella", "raDeg": 79.1723, "decDeg": 45.9979, "mag": 0.08, "colorIndex": 0.80 },
|
||||
{ "id": 24436, "name": "Rigel", "raDeg": 78.6345, "decDeg": -8.2016, "mag": 0.12, "colorIndex": -0.03 },
|
||||
{ "id": 37279, "name": "Procyon", "raDeg": 114.8255, "decDeg": 5.2250, "mag": 0.34, "colorIndex": 0.42 },
|
||||
{ "id": 7588, "name": "Achernar", "raDeg": 24.4286, "decDeg": -57.2368, "mag": 0.46, "colorIndex": -0.16 },
|
||||
{ "id": 27989, "name": "Betelgeuse", "raDeg": 88.7929, "decDeg": 7.4071, "mag": 0.50, "colorIndex": 1.85 },
|
||||
{ "id": 68702, "name": "Hadar", "raDeg": 210.9559, "decDeg": -60.3731, "mag": 0.61, "colorIndex": -0.23 },
|
||||
{ "id": 97649, "name": "Altair", "raDeg": 297.6958, "decDeg": 8.8683, "mag": 0.76, "colorIndex": 0.22 },
|
||||
{ "id": 60718, "name": "Acrux", "raDeg": 186.6490, "decDeg": -63.0991, "mag": 0.77, "colorIndex": -0.24 },
|
||||
{ "id": 21421, "name": "Aldebaran", "raDeg": 68.9800, "decDeg": 16.5093, "mag": 0.85, "colorIndex": 1.54 },
|
||||
{ "id": 65474, "name": "Spica", "raDeg": 201.2983, "decDeg": -11.1614, "mag": 0.98, "colorIndex": -0.23 },
|
||||
{ "id": 80763, "name": "Antares", "raDeg": 247.3519, "decDeg": -26.4320, "mag": 1.06, "colorIndex": 1.83 },
|
||||
{ "id": 37826, "name": "Pollux", "raDeg": 116.3289, "decDeg": 28.0262, "mag": 1.14, "colorIndex": 1.00 },
|
||||
{ "id": 113368, "name": "Fomalhaut", "raDeg": 344.4128, "decDeg": -29.6222, "mag": 1.16, "colorIndex": 0.09 },
|
||||
{ "id": 102098, "name": "Deneb", "raDeg": 310.3579, "decDeg": 45.2803, "mag": 1.25, "colorIndex": 0.09 },
|
||||
{ "id": 49669, "name": "Regulus", "raDeg": 152.0929, "decDeg": 11.9672, "mag": 1.35, "colorIndex": -0.11 },
|
||||
{ "id": 65477, "name": "Mimosa", "raDeg": 191.9303, "decDeg": -59.6888, "mag": 1.25, "colorIndex": -0.23 },
|
||||
{ "id": 33579, "name": "Alphard", "raDeg": 141.8969, "decDeg": -8.6586, "mag": 1.98, "colorIndex": 1.44 },
|
||||
{ "id": 21444, "name": "Bellatrix", "raDeg": 81.2828, "decDeg": 6.3497, "mag": 1.64, "colorIndex": -0.22 },
|
||||
{ "id": 25336, "name": "Elnath", "raDeg": 81.5729, "decDeg": 28.6075, "mag": 1.65, "colorIndex": -0.13 },
|
||||
{ "id": 26311, "name": "Alnilam", "raDeg": 84.0534, "decDeg": -1.2019, "mag": 1.69, "colorIndex": -0.19 },
|
||||
{ "id": 26727, "name": "Alnitak", "raDeg": 85.1897, "decDeg": -1.9426, "mag": 1.77, "colorIndex": -0.19 },
|
||||
{ "id": 25930, "name": "Saiph", "raDeg": 86.9391, "decDeg": -9.6696, "mag": 2.06, "colorIndex": -0.20 },
|
||||
{ "id": 58001, "name": "Alioth", "raDeg": 193.5073, "decDeg": 55.9598, "mag": 1.76, "colorIndex": -0.02 }
|
||||
]
|
||||
BIN
frontend/public/earth/assets/celestial/starmap_deep_8k.jpg
Normal file
BIN
frontend/public/earth/assets/celestial/starmap_deep_8k.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
BIN
frontend/public/earth/assets/celestial/starmap_equatorial_4k.jpg
Normal file
BIN
frontend/public/earth/assets/celestial/starmap_equatorial_4k.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
@@ -10,7 +10,8 @@
|
||||
"three": "https://esm.sh/three@0.128.0",
|
||||
"simplex-noise": "https://esm.sh/simplex-noise@4.0.1",
|
||||
"satellite.js": "https://esm.sh/satellite.js@5.0.0",
|
||||
"hls.js": "https://esm.sh/hls.js@1.6.15"
|
||||
"hls.js": "https://esm.sh/hls.js@1.6.15",
|
||||
"astronomy-engine": "https://esm.sh/astronomy-engine@2.1.19"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -64,6 +65,9 @@
|
||||
<button id="layer-panel-collapse" class="layer-panel-btn" type="button" aria-label="折叠图层列表" title="折叠">
|
||||
<span class="material-symbols-rounded">expand_less</span>
|
||||
</button>
|
||||
<button class="layer-panel-btn hud-panel-close" type="button" data-close-panel="layer-toggles" aria-label="关闭图层面板" title="关闭">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible body -->
|
||||
|
||||
487
frontend/public/earth/js/celestial.js
Normal file
487
frontend/public/earth/js/celestial.js
Normal file
@@ -0,0 +1,487 @@
|
||||
import * as THREE from "three";
|
||||
import * as Astronomy from "astronomy-engine";
|
||||
|
||||
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const defaultSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
|
||||
const defaultMoonDirection = new THREE.Vector3(-0.6, 0.45, -0.2).normalize();
|
||||
|
||||
let celestialRoot = null;
|
||||
let skySphere = null;
|
||||
let brightStarsGroup = null;
|
||||
let sunSprite = null;
|
||||
let moonSprite = null;
|
||||
let sunHaloSprite = null;
|
||||
let moonHaloSprite = null;
|
||||
let brightStarTexture = null;
|
||||
let sunDirection = defaultSunDirection.clone();
|
||||
let moonDirection = defaultMoonDirection.clone();
|
||||
let lastUpdatedAt = 0;
|
||||
let linkedSunLight = null;
|
||||
let linkedBackLight = null;
|
||||
let linkedEarth = null;
|
||||
let brightStarSprites = [];
|
||||
let celestialRotationQuaternion = new THREE.Quaternion();
|
||||
let celestialViewQuaternion = new THREE.Quaternion();
|
||||
let runtimeOrientationEuler = {
|
||||
...CELESTIAL_CONFIG.orientationEulerRad,
|
||||
};
|
||||
let runtimeFollowConfig = {
|
||||
...CELESTIAL_CONFIG.followEarthRotation,
|
||||
};
|
||||
const scratchEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
||||
|
||||
function getCelestialEuler() {
|
||||
const { x, y, z } = runtimeOrientationEuler;
|
||||
return new THREE.Euler(x, y, z, "YXZ");
|
||||
}
|
||||
|
||||
function refreshCelestialOrientation() {
|
||||
celestialRotationQuaternion.setFromEuler(getCelestialEuler());
|
||||
refreshCelestialView();
|
||||
}
|
||||
|
||||
function refreshCelestialView() {
|
||||
if (linkedEarth && runtimeFollowConfig.enabled) {
|
||||
const followX = runtimeFollowConfig.x
|
||||
? (linkedEarth.rotation.x - EARTH_CONFIG.tiltRad) * (runtimeFollowConfig.invertX ? -1 : 1)
|
||||
: 0;
|
||||
const followY = runtimeFollowConfig.y
|
||||
? linkedEarth.rotation.y * (runtimeFollowConfig.invertY ? -1 : 1)
|
||||
: 0;
|
||||
const followZ = runtimeFollowConfig.z
|
||||
? linkedEarth.rotation.z * (runtimeFollowConfig.invertZ ? -1 : 1)
|
||||
: 0;
|
||||
|
||||
scratchEuler.set(
|
||||
followX,
|
||||
followY,
|
||||
followZ,
|
||||
"YXZ",
|
||||
);
|
||||
celestialViewQuaternion.setFromEuler(scratchEuler);
|
||||
} else {
|
||||
celestialViewQuaternion.identity();
|
||||
}
|
||||
|
||||
if (celestialRoot) {
|
||||
celestialRoot.quaternion
|
||||
.copy(celestialRotationQuaternion)
|
||||
.multiply(celestialViewQuaternion);
|
||||
}
|
||||
}
|
||||
|
||||
function applyCelestialOrientation(direction) {
|
||||
return direction
|
||||
.clone()
|
||||
.applyQuaternion(celestialRotationQuaternion)
|
||||
.applyQuaternion(celestialViewQuaternion)
|
||||
.normalize();
|
||||
}
|
||||
|
||||
function configureTextureEncoding(texture) {
|
||||
if (!texture) return;
|
||||
if ("colorSpace" in texture && THREE.SRGBColorSpace) {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
} else if ("encoding" in texture && THREE.sRGBEncoding) {
|
||||
texture.encoding = THREE.sRGBEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
function createDiscTexture(stops, size = 256) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const gradient = ctx.createRadialGradient(
|
||||
size / 2,
|
||||
size / 2,
|
||||
0,
|
||||
size / 2,
|
||||
size / 2,
|
||||
size / 2,
|
||||
);
|
||||
|
||||
stops.forEach(([offset, color]) => gradient.addColorStop(offset, color));
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
configureTextureEncoding(texture);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createSprite({ texture, scale, opacity = 1, name, color = 0xffffff }) {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
toneMapped: false,
|
||||
color,
|
||||
});
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.name = name;
|
||||
sprite.scale.setScalar(scale);
|
||||
sprite.renderOrder = 100;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
function createSkySphere() {
|
||||
const geometry = new THREE.SphereGeometry(
|
||||
CELESTIAL_CONFIG.skyRadius,
|
||||
64,
|
||||
64,
|
||||
);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
side: THREE.BackSide,
|
||||
transparent: CELESTIAL_CONFIG.skyOpacity < 1,
|
||||
opacity: CELESTIAL_CONFIG.skyOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
fog: false,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.name = "celestial-sky-sphere";
|
||||
mesh.renderOrder = -1000;
|
||||
mesh.raycast = () => {};
|
||||
|
||||
textureLoader.load(
|
||||
CELESTIAL_CONFIG.starMapUrl,
|
||||
(texture) => {
|
||||
configureTextureEncoding(texture);
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
console.warn("Failed to load celestial star map texture");
|
||||
},
|
||||
);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function geoVectorToWorldDirection(vector) {
|
||||
return new THREE.Vector3(vector.x, vector.z, vector.y).normalize();
|
||||
}
|
||||
|
||||
function raDecToWorldDirection(raDeg, decDeg) {
|
||||
const raRad = THREE.MathUtils.degToRad(raDeg);
|
||||
const decRad = THREE.MathUtils.degToRad(decDeg);
|
||||
|
||||
const x = Math.cos(decRad) * Math.cos(raRad);
|
||||
const y = Math.sin(decRad);
|
||||
const z = Math.cos(decRad) * Math.sin(raRad);
|
||||
|
||||
return new THREE.Vector3(x, z, y).normalize();
|
||||
}
|
||||
|
||||
function colorFromBvIndex(colorIndex) {
|
||||
if (colorIndex <= -0.2) return 0xa8c9ff;
|
||||
if (colorIndex <= 0.0) return 0xc8dcff;
|
||||
if (colorIndex <= 0.3) return 0xf4f7ff;
|
||||
if (colorIndex <= 0.7) return 0xfff4dc;
|
||||
if (colorIndex <= 1.1) return 0xffdfb0;
|
||||
if (colorIndex <= 1.5) return 0xffc47f;
|
||||
return 0xffa35e;
|
||||
}
|
||||
|
||||
function scaleFromMagnitude(mag) {
|
||||
const brightness = THREE.MathUtils.clamp(
|
||||
1 - (mag - (-1.5)) / (CELESTIAL_CONFIG.brightStarMinMag - (-1.5)),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
return THREE.MathUtils.lerp(
|
||||
CELESTIAL_CONFIG.brightStarMinScale,
|
||||
CELESTIAL_CONFIG.brightStarMaxScale,
|
||||
Math.pow(brightness, 0.72),
|
||||
);
|
||||
}
|
||||
|
||||
function createBrightStarTexture() {
|
||||
return createDiscTexture([
|
||||
[0, "rgba(255,255,255,1)"],
|
||||
[0.18, "rgba(255,255,255,0.98)"],
|
||||
[0.46, "rgba(215,230,255,0.42)"],
|
||||
[1, "rgba(128,168,255,0)"],
|
||||
], 128);
|
||||
}
|
||||
|
||||
function createBrightStarsGroup() {
|
||||
const group = new THREE.Group();
|
||||
group.name = "bright-stars-group";
|
||||
group.visible = true;
|
||||
return group;
|
||||
}
|
||||
|
||||
async function loadBrightStars() {
|
||||
try {
|
||||
const response = await fetch(CELESTIAL_CONFIG.brightStarsUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const stars = await response.json();
|
||||
const filteredStars = stars
|
||||
.filter((star) => Number.isFinite(star?.raDeg) && Number.isFinite(star?.decDeg))
|
||||
.filter((star) => star.mag <= CELESTIAL_CONFIG.brightStarMinMag)
|
||||
.sort((a, b) => a.mag - b.mag)
|
||||
.slice(0, CELESTIAL_CONFIG.brightStarMaxCount);
|
||||
|
||||
brightStarTexture = createBrightStarTexture();
|
||||
brightStarSprites = filteredStars.map((star) => {
|
||||
const sprite = createSprite({
|
||||
texture: brightStarTexture,
|
||||
scale: scaleFromMagnitude(star.mag),
|
||||
opacity: CELESTIAL_CONFIG.brightStarOpacity,
|
||||
name: `bright-star-${star.name}`,
|
||||
color: colorFromBvIndex(star.colorIndex ?? 0.4),
|
||||
});
|
||||
sprite.position
|
||||
.copy(raDecToWorldDirection(star.raDeg, star.decDeg))
|
||||
.multiplyScalar(CELESTIAL_CONFIG.brightStarDistance);
|
||||
sprite.userData.star = star;
|
||||
return sprite;
|
||||
});
|
||||
|
||||
brightStarSprites.forEach((sprite) => brightStarsGroup?.add(sprite));
|
||||
} catch (error) {
|
||||
console.warn("Failed to load bright star layer", error);
|
||||
}
|
||||
}
|
||||
|
||||
function computeBodyDirection(body, date) {
|
||||
const vector = Astronomy.GeoVector(body, date, false);
|
||||
return geoVectorToWorldDirection(vector);
|
||||
}
|
||||
|
||||
function updateSpritePositions() {
|
||||
if (sunSprite) {
|
||||
sunSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
|
||||
}
|
||||
if (sunHaloSprite) {
|
||||
sunHaloSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
|
||||
}
|
||||
|
||||
if (moonSprite) {
|
||||
moonSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
|
||||
}
|
||||
if (moonHaloSprite) {
|
||||
moonHaloSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLighting() {
|
||||
const calibratedSunDirection = applyCelestialOrientation(sunDirection);
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
|
||||
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
|
||||
linkedSunLight.position
|
||||
.copy(calibratedSunDirection)
|
||||
.multiplyScalar(CELESTIAL_CONFIG.sunLightDistance);
|
||||
}
|
||||
|
||||
if (linkedBackLight) {
|
||||
linkedBackLight.color.setHex(CELESTIAL_CONFIG.backLightColor);
|
||||
linkedBackLight.intensity = CELESTIAL_CONFIG.backLightIntensity;
|
||||
linkedBackLight.position
|
||||
.copy(calibratedSunDirection)
|
||||
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
function computeCelestialState(date = new Date()) {
|
||||
sunDirection = computeBodyDirection(Astronomy.Body.Sun, date);
|
||||
moonDirection = computeBodyDirection(Astronomy.Body.Moon, date);
|
||||
updateSpritePositions();
|
||||
updateLighting();
|
||||
lastUpdatedAt = date.getTime();
|
||||
}
|
||||
|
||||
export function initCelestialLayer(
|
||||
scene,
|
||||
{ camera = null, sunLight = null, backLight = null, earth = null } = {},
|
||||
) {
|
||||
if (!scene || !CELESTIAL_CONFIG.enabled) return null;
|
||||
|
||||
disposeCelestialLayer();
|
||||
|
||||
linkedSunLight = sunLight;
|
||||
linkedBackLight = backLight;
|
||||
linkedEarth = earth;
|
||||
|
||||
celestialRoot = new THREE.Group();
|
||||
celestialRoot.name = "celestial-root";
|
||||
celestialRoot.position.set(0, 0, 0);
|
||||
|
||||
refreshCelestialOrientation();
|
||||
|
||||
skySphere = createSkySphere();
|
||||
brightStarsGroup = createBrightStarsGroup();
|
||||
|
||||
const sunTexture = createDiscTexture([
|
||||
[0, "rgba(255,255,255,1)"],
|
||||
[0.08, "rgba(255,251,242,1)"],
|
||||
[0.22, "rgba(255,242,205,0.98)"],
|
||||
[0.52, "rgba(255,211,117,0.9)"],
|
||||
[0.82, "rgba(255,162,54,0.18)"],
|
||||
[1, "rgba(255,120,32,0)"],
|
||||
]);
|
||||
const sunHaloTexture = createDiscTexture([
|
||||
[0, "rgba(255,245,214,0.9)"],
|
||||
[0.2, "rgba(255,220,154,0.54)"],
|
||||
[0.52, "rgba(255,160,72,0.16)"],
|
||||
[1, "rgba(255,120,32,0)"],
|
||||
], 512);
|
||||
const moonTexture = createDiscTexture([
|
||||
[0, "rgba(255,255,255,0.98)"],
|
||||
[0.42, "rgba(229,236,248,0.92)"],
|
||||
[0.76, "rgba(164,178,202,0.34)"],
|
||||
[1, "rgba(80,92,118,0)"],
|
||||
]);
|
||||
const moonHaloTexture = createDiscTexture([
|
||||
[0, "rgba(226,235,250,0.42)"],
|
||||
[0.38, "rgba(188,203,230,0.16)"],
|
||||
[1, "rgba(120,136,170,0)"],
|
||||
], 384);
|
||||
|
||||
sunSprite = createSprite({
|
||||
texture: sunTexture,
|
||||
scale: CELESTIAL_CONFIG.sunScale,
|
||||
name: "sun-sprite",
|
||||
});
|
||||
sunHaloSprite = createSprite({
|
||||
texture: sunHaloTexture,
|
||||
scale: CELESTIAL_CONFIG.sunHaloScale,
|
||||
opacity: 0.78,
|
||||
name: "sun-halo-sprite",
|
||||
});
|
||||
moonSprite = createSprite({
|
||||
texture: moonTexture,
|
||||
scale: CELESTIAL_CONFIG.moonScale,
|
||||
opacity: 0.98,
|
||||
name: "moon-sprite",
|
||||
});
|
||||
moonHaloSprite = createSprite({
|
||||
texture: moonHaloTexture,
|
||||
scale: CELESTIAL_CONFIG.moonHaloScale,
|
||||
opacity: 0.52,
|
||||
name: "moon-halo-sprite",
|
||||
});
|
||||
|
||||
sunHaloSprite.renderOrder = 98;
|
||||
moonHaloSprite.renderOrder = 98;
|
||||
|
||||
celestialRoot.add(skySphere);
|
||||
celestialRoot.add(brightStarsGroup);
|
||||
celestialRoot.add(sunHaloSprite);
|
||||
celestialRoot.add(moonHaloSprite);
|
||||
celestialRoot.add(sunSprite);
|
||||
celestialRoot.add(moonSprite);
|
||||
scene.add(celestialRoot);
|
||||
|
||||
computeCelestialState(new Date());
|
||||
loadBrightStars();
|
||||
|
||||
return {
|
||||
root: celestialRoot,
|
||||
getSunDirection: () => sunDirection.clone(),
|
||||
getMoonDirection: () => moonDirection.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCelestialLayer(date = new Date(), camera = null) {
|
||||
if (!celestialRoot) return;
|
||||
|
||||
refreshCelestialView();
|
||||
|
||||
const now = date.getTime();
|
||||
if (!lastUpdatedAt || now - lastUpdatedAt >= CELESTIAL_CONFIG.updateIntervalMs) {
|
||||
computeCelestialState(date);
|
||||
} else {
|
||||
updateLighting();
|
||||
updateSpritePositions();
|
||||
}
|
||||
}
|
||||
|
||||
export function getSunDirection() {
|
||||
return applyCelestialOrientation(sunDirection);
|
||||
}
|
||||
|
||||
export function getMoonDirection() {
|
||||
return applyCelestialOrientation(moonDirection);
|
||||
}
|
||||
|
||||
export function getCelestialDebugState() {
|
||||
return {
|
||||
orientationEulerRad: { ...runtimeOrientationEuler },
|
||||
followEarthRotation: { ...runtimeFollowConfig },
|
||||
};
|
||||
}
|
||||
|
||||
export function setCelestialOrientation(nextEuler = {}) {
|
||||
runtimeOrientationEuler = {
|
||||
...runtimeOrientationEuler,
|
||||
...nextEuler,
|
||||
};
|
||||
refreshCelestialOrientation();
|
||||
updateLighting();
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function setCelestialFollow(nextFollow = {}) {
|
||||
runtimeFollowConfig = {
|
||||
...runtimeFollowConfig,
|
||||
...nextFollow,
|
||||
};
|
||||
refreshCelestialView();
|
||||
updateLighting();
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function disposeCelestialLayer() {
|
||||
if (celestialRoot?.parent) {
|
||||
celestialRoot.parent.remove(celestialRoot);
|
||||
}
|
||||
|
||||
[skySphere, sunSprite, moonSprite, sunHaloSprite, moonHaloSprite, ...brightStarSprites].forEach((object) => {
|
||||
if (!object) return;
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (object.material.map) object.material.map.dispose?.();
|
||||
object.material.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
skySphere = null;
|
||||
brightStarsGroup = null;
|
||||
sunSprite = null;
|
||||
moonSprite = null;
|
||||
sunHaloSprite = null;
|
||||
moonHaloSprite = null;
|
||||
celestialRoot = null;
|
||||
brightStarSprites = [];
|
||||
brightStarTexture?.dispose?.();
|
||||
brightStarTexture = null;
|
||||
|
||||
lastUpdatedAt = 0;
|
||||
linkedSunLight = null;
|
||||
linkedBackLight = null;
|
||||
linkedEarth = null;
|
||||
sunDirection.copy(defaultSunDirection);
|
||||
moonDirection.copy(defaultMoonDirection);
|
||||
runtimeOrientationEuler = {
|
||||
...CELESTIAL_CONFIG.orientationEulerRad,
|
||||
};
|
||||
runtimeFollowConfig = {
|
||||
...CELESTIAL_CONFIG.followEarthRotation,
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,46 @@ export const EARTH_CONFIG = {
|
||||
latCoefficient: 0.5
|
||||
};
|
||||
|
||||
export const CELESTIAL_CONFIG = {
|
||||
enabled: true,
|
||||
updateIntervalMs: 10_000,
|
||||
skyRadius: 2600,
|
||||
skyOpacity: 1,
|
||||
starMapUrl: "./assets/celestial/starmap_equatorial_4k.jpg",
|
||||
brightStarsUrl: "./assets/celestial/bright-stars.json",
|
||||
orientationEulerRad: {
|
||||
x: 0.46,
|
||||
y: 1.18,
|
||||
z: -0.08,
|
||||
},
|
||||
followEarthRotation: {
|
||||
enabled: true,
|
||||
x: true,
|
||||
y: true,
|
||||
z: false,
|
||||
invertX: true,
|
||||
invertY: false,
|
||||
invertZ: false,
|
||||
},
|
||||
sunDistance: 2150,
|
||||
moonDistance: 2050,
|
||||
sunScale: 78,
|
||||
moonScale: 38,
|
||||
sunHaloScale: 136,
|
||||
moonHaloScale: 62,
|
||||
brightStarDistance: 2350,
|
||||
brightStarMinMag: 2.1,
|
||||
brightStarMaxCount: 36,
|
||||
brightStarMinScale: 2.4,
|
||||
brightStarMaxScale: 6.8,
|
||||
brightStarOpacity: 0.88,
|
||||
sunLightDistance: 460,
|
||||
sunLightIntensity: 1.28,
|
||||
sunLightColor: 0xfff4df,
|
||||
backLightIntensity: 0.18,
|
||||
backLightColor: 0x2b4c78,
|
||||
};
|
||||
|
||||
export const PATHS = {
|
||||
cablesApi: '/api/v1/visualization/geo/cables',
|
||||
landingPointsApi: '/api/v1/visualization/geo/landing-points',
|
||||
@@ -237,18 +277,18 @@ export const EARTH_MATERIAL_CONFIG = {
|
||||
occluderSegments: 48,
|
||||
|
||||
// Fresnel atmosphere glow — inner rim
|
||||
atmosInnerRadiusFactor: 1.018,
|
||||
atmosInnerRadiusFactor: 1.01,
|
||||
atmosInnerSegments: 64,
|
||||
atmosInnerColor: [0.25, 0.62, 1.0],
|
||||
atmosInnerRimPower: 3.2,
|
||||
atmosInnerIntensity: 0.72,
|
||||
atmosInnerIntensity: 0.18,
|
||||
|
||||
// Fresnel atmosphere glow — outer corona
|
||||
atmosOuterRadiusFactor: 1.07,
|
||||
atmosOuterRadiusFactor: 1.016,
|
||||
atmosOuterSegments: 48,
|
||||
atmosOuterColor: [0.18, 0.45, 0.9],
|
||||
atmosOuterRimPower: 5.0,
|
||||
atmosOuterIntensity: 0.28,
|
||||
atmosOuterIntensity: 0.02,
|
||||
|
||||
// Texture candidates — tried in order, first success wins
|
||||
textureUrls: [
|
||||
@@ -256,4 +296,16 @@ export const EARTH_MATERIAL_CONFIG = {
|
||||
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg',
|
||||
'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg',
|
||||
],
|
||||
|
||||
dayNight: {
|
||||
enabled: true,
|
||||
sunDirection: { x: 1, y: 0.2, z: 0.4 },
|
||||
nightFloor: 0.18,
|
||||
dayBoost: 1.08,
|
||||
twilightWidth: 0.18,
|
||||
twilightIntensity: 0.22,
|
||||
twilightColor: 0x4ea0ff,
|
||||
nightTintColor: 0x0b1830,
|
||||
nightTintIntensity: 0.1,
|
||||
},
|
||||
};
|
||||
|
||||
2
frontend/public/earth/js/controls.js
vendored
2
frontend/public/earth/js/controls.js
vendored
@@ -251,6 +251,7 @@ function setupDraggableHudPanels() {
|
||||
const capturedWidth = panelRect.width;
|
||||
panel.style.position = "absolute";
|
||||
panel.style.width = `${capturedWidth}px`;
|
||||
panel.style.margin = "0";
|
||||
app.appendChild(panel);
|
||||
}
|
||||
|
||||
@@ -972,6 +973,7 @@ function resetPanelInlineLayout(panel) {
|
||||
panel.style.transform = "";
|
||||
panel.style.position = "";
|
||||
panel.style.width = "";
|
||||
panel.style.margin = "";
|
||||
delete panel.dataset.dragged;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,73 @@ export let terrain = null;
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
let _earthMaterial = null;
|
||||
let _earthShader = null;
|
||||
const _earthSunDirection = new THREE.Vector3(
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
|
||||
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.z,
|
||||
).normalize();
|
||||
|
||||
function applyEarthDayNightShader(material) {
|
||||
if (!material || !EARTH_MATERIAL_CONFIG.dayNight.enabled) return;
|
||||
|
||||
const twilightColor = new THREE.Color(EARTH_MATERIAL_CONFIG.dayNight.twilightColor);
|
||||
const nightTintColor = new THREE.Color(EARTH_MATERIAL_CONFIG.dayNight.nightTintColor);
|
||||
|
||||
material.onBeforeCompile = (shader) => {
|
||||
_earthShader = shader;
|
||||
shader.uniforms.uSunDirectionWorld = { value: _earthSunDirection.clone() };
|
||||
shader.uniforms.uNightFloor = { value: EARTH_MATERIAL_CONFIG.dayNight.nightFloor };
|
||||
shader.uniforms.uDayBoost = { value: EARTH_MATERIAL_CONFIG.dayNight.dayBoost };
|
||||
shader.uniforms.uTwilightWidth = { value: EARTH_MATERIAL_CONFIG.dayNight.twilightWidth };
|
||||
shader.uniforms.uTwilightIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.twilightIntensity };
|
||||
shader.uniforms.uTwilightColor = { value: twilightColor };
|
||||
shader.uniforms.uNightTintColor = { value: nightTintColor };
|
||||
shader.uniforms.uNightTintIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.nightTintIntensity };
|
||||
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
"#include <common>",
|
||||
`#include <common>
|
||||
varying vec3 vWorldNormal;`,
|
||||
).replace(
|
||||
"#include <begin_vertex>",
|
||||
`#include <begin_vertex>
|
||||
vWorldNormal = normalize(mat3(modelMatrix) * normal);`,
|
||||
);
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
"#include <common>",
|
||||
`#include <common>
|
||||
varying vec3 vWorldNormal;
|
||||
uniform vec3 uSunDirectionWorld;
|
||||
uniform float uNightFloor;
|
||||
uniform float uDayBoost;
|
||||
uniform float uTwilightWidth;
|
||||
uniform float uTwilightIntensity;
|
||||
uniform vec3 uTwilightColor;
|
||||
uniform vec3 uNightTintColor;
|
||||
uniform float uNightTintIntensity;`,
|
||||
).replace(
|
||||
"#include <output_fragment>",
|
||||
`
|
||||
vec3 worldNormal = normalize(vWorldNormal);
|
||||
vec3 sunDir = normalize(uSunDirectionWorld);
|
||||
float sunFacing = dot(worldNormal, sunDir);
|
||||
float daylight = smoothstep(-uTwilightWidth, uTwilightWidth, sunFacing);
|
||||
float twilight = 1.0 - smoothstep(0.0, uTwilightWidth, abs(sunFacing));
|
||||
|
||||
outgoingLight *= mix(uNightFloor, uDayBoost, daylight);
|
||||
outgoingLight += uTwilightColor * twilight * uTwilightIntensity;
|
||||
outgoingLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
||||
|
||||
#include <output_fragment>
|
||||
`,
|
||||
);
|
||||
};
|
||||
|
||||
material.customProgramCacheKey = () => "earth-day-night-v1";
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function createEarth(scene) {
|
||||
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius, 128, 128);
|
||||
@@ -27,6 +94,7 @@ export function createEarth(scene) {
|
||||
depthWrite: true,
|
||||
depthTest: true,
|
||||
});
|
||||
applyEarthDayNightShader(material);
|
||||
_earthMaterial = material;
|
||||
|
||||
earth = new THREE.Mesh(geometry, material);
|
||||
@@ -271,6 +339,14 @@ export function clearEarthTexture() {
|
||||
_earthMaterial.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function setEarthSunDirection(direction) {
|
||||
if (!direction) return;
|
||||
_earthSunDirection.copy(direction).normalize();
|
||||
if (_earthShader?.uniforms?.uSunDirectionWorld) {
|
||||
_earthShader.uniforms.uSunDirectionWorld.value.copy(_earthSunDirection);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadEarthTexture() {
|
||||
return new Promise((resolve) => {
|
||||
if (!_earthMaterial) { resolve(); return; }
|
||||
|
||||
@@ -20,12 +20,21 @@ import {
|
||||
createEarth,
|
||||
createClouds,
|
||||
createTerrain,
|
||||
createStars,
|
||||
createGridLines,
|
||||
getEarth,
|
||||
loadEarthTexture,
|
||||
clearEarthTexture,
|
||||
setEarthSunDirection,
|
||||
} from "./earth.js";
|
||||
import {
|
||||
initCelestialLayer,
|
||||
updateCelestialLayer,
|
||||
disposeCelestialLayer,
|
||||
getCelestialDebugState,
|
||||
getSunDirection,
|
||||
setCelestialOrientation,
|
||||
setCelestialFollow,
|
||||
} from "./celestial.js";
|
||||
import {
|
||||
loadGeoJSONFromPath,
|
||||
loadLandingPoints,
|
||||
@@ -165,6 +174,7 @@ let cablesEnabled = true;
|
||||
let satellitesEnabled = true;
|
||||
let cableToggleToken = 0;
|
||||
let satelliteToggleToken = 0;
|
||||
let sceneLights = null;
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const interactionRaycaster = new THREE.Raycaster();
|
||||
@@ -912,7 +922,7 @@ export function init() {
|
||||
75,
|
||||
getViewportAspect(),
|
||||
0.1,
|
||||
1000,
|
||||
5000,
|
||||
);
|
||||
camera.position.z = CONFIG.defaultCameraZ;
|
||||
setSatelliteCamera(camera);
|
||||
@@ -923,7 +933,7 @@ export function init() {
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
syncRendererViewport();
|
||||
renderer.setClearColor(0x0a0a1a, 1);
|
||||
renderer.setClearColor(0x02040a, 1);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
const container = document.getElementById("container");
|
||||
@@ -932,7 +942,7 @@ export function init() {
|
||||
container.appendChild(renderer.domElement);
|
||||
}
|
||||
|
||||
addLights();
|
||||
sceneLights = addLights();
|
||||
initInfoCard();
|
||||
initLegend();
|
||||
setLegendItems("cables", getCableLegendItems());
|
||||
@@ -946,7 +956,12 @@ export function init() {
|
||||
inertialVelocity = { x: 0, y: 0 };
|
||||
createClouds(scene, earthObj);
|
||||
createTerrain(scene, earthObj, simplex);
|
||||
createStars(scene);
|
||||
initCelestialLayer(scene, {
|
||||
camera,
|
||||
sunLight: sceneLights?.sunLight ?? null,
|
||||
backLight: sceneLights?.backLight ?? null,
|
||||
earth: earthObj,
|
||||
});
|
||||
createGridLines(scene, earthObj);
|
||||
createSatellites(scene, earthObj);
|
||||
|
||||
@@ -967,17 +982,25 @@ function registerGlobalApi() {
|
||||
hideInfoCard();
|
||||
clearLockedObject();
|
||||
},
|
||||
celestial: {
|
||||
getState: () => getCelestialDebugState(),
|
||||
setOrientation: (nextEuler) => setCelestialOrientation(nextEuler),
|
||||
setFollow: (nextFollow) => setCelestialFollow(nextFollow),
|
||||
},
|
||||
destroy,
|
||||
init,
|
||||
};
|
||||
}
|
||||
|
||||
function addLights() {
|
||||
scene.add(new THREE.AmbientLight(0x404060));
|
||||
const ambientLight = new THREE.AmbientLight(0x404060);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
directionalLight.position.set(5, 3, 5);
|
||||
scene.add(directionalLight);
|
||||
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
sunLight.position.set(5, 3, 5);
|
||||
sunLight.target.position.set(0, 0, 0);
|
||||
scene.add(sunLight);
|
||||
scene.add(sunLight.target);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0x446688, 0.3);
|
||||
backLight.position.set(-5, 0, -5);
|
||||
@@ -986,6 +1009,13 @@ function addLights() {
|
||||
const pointLight = new THREE.PointLight(0xffffff, 0.4);
|
||||
pointLight.position.set(10, 10, 10);
|
||||
scene.add(pointLight);
|
||||
|
||||
return {
|
||||
ambientLight,
|
||||
sunLight,
|
||||
backLight,
|
||||
pointLight,
|
||||
};
|
||||
}
|
||||
|
||||
// Yield control to the browser so the renderer can paint a frame before the next step
|
||||
@@ -1710,6 +1740,8 @@ function animate() {
|
||||
updateSatellitePositions(deltaTime);
|
||||
updateBreathingPhase(deltaTime);
|
||||
updateRelatedSatelliteHighlights();
|
||||
updateCelestialLayer(new Date(), camera);
|
||||
setEarthSunDirection(getSunDirection());
|
||||
updateNewsViewFocus(getCurrentViewCenterCoords());
|
||||
|
||||
const satPositions = getSatellitePositions();
|
||||
@@ -1751,6 +1783,7 @@ export function destroy() {
|
||||
clearBGPData(getEarth());
|
||||
resetSatelliteState();
|
||||
clearUiState();
|
||||
disposeCelestialLayer();
|
||||
|
||||
if (scene) {
|
||||
disposeSceneObject(scene);
|
||||
@@ -1767,6 +1800,7 @@ export function destroy() {
|
||||
scene = null;
|
||||
camera = null;
|
||||
renderer = null;
|
||||
sceneLights = null;
|
||||
initialized = false;
|
||||
|
||||
delete window.__planetEarth;
|
||||
|
||||
@@ -24,7 +24,6 @@ let lockedSatelliteIndex = null;
|
||||
let hoveredSatelliteIndex = null;
|
||||
let positionUpdateAccumulator = 0;
|
||||
let satelliteCapacity = 0;
|
||||
let selectedSatelliteLegendKey = null;
|
||||
|
||||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
||||
const DOT_TEXTURE_SIZE = 32;
|
||||
@@ -38,43 +37,82 @@ export let breathingPhase = 0;
|
||||
|
||||
const SATELLITE_LEGEND_RULES = [
|
||||
{
|
||||
key: "starlink",
|
||||
label: "Starlink",
|
||||
color: "#00e6ff",
|
||||
match: (props) => (props?.name || "").includes("STARLINK"),
|
||||
},
|
||||
{
|
||||
key: "geo",
|
||||
label: "GEO / 倾角 20-30",
|
||||
color: "#ffcc00",
|
||||
key: "equatorial",
|
||||
label: "赤道轨道(0-30°)",
|
||||
color: "#ff3333",
|
||||
match: (props) => {
|
||||
const inclination = props?.inclination || 53;
|
||||
return inclination > 20 && inclination < 30;
|
||||
const inclination = Number(props?.inclination ?? 0);
|
||||
return inclination >= 0 && inclination < 30;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "iridium",
|
||||
label: "Iridium",
|
||||
color: "#ff8000",
|
||||
match: (props) => (props?.name || "").includes("IRIDIUM"),
|
||||
key: "low",
|
||||
label: "低倾角轨道(30-60°)",
|
||||
color: "#ff9933",
|
||||
match: (props) => {
|
||||
const inclination = Number(props?.inclination ?? 0);
|
||||
return inclination >= 30 && inclination < 60;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "mid-inclination",
|
||||
label: "倾角 50-70",
|
||||
color: "#00ff4d",
|
||||
key: "medium",
|
||||
label: "中倾角轨道(60-90°)",
|
||||
color: "#ffff33",
|
||||
match: (props) => {
|
||||
const inclination = props?.inclination || 53;
|
||||
return inclination > 50 && inclination < 70;
|
||||
const inclination = Number(props?.inclination ?? 0);
|
||||
return inclination >= 60 && inclination < 90;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "high",
|
||||
label: "高倾角轨道(90-120°)",
|
||||
color: "#33ff33",
|
||||
match: (props) => {
|
||||
const inclination = Number(props?.inclination ?? 0);
|
||||
return inclination >= 90 && inclination < 120;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "retrograde",
|
||||
label: "逆行轨道(120-180°)",
|
||||
color: "#3333ff",
|
||||
match: (props) => {
|
||||
const inclination = Number(props?.inclination ?? 0);
|
||||
return inclination >= 120 && inclination <= 180;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "other",
|
||||
label: "其他卫星",
|
||||
color: "#ffffff",
|
||||
label: "其他",
|
||||
color: "#d7e2f4",
|
||||
match: () => true,
|
||||
},
|
||||
];
|
||||
|
||||
const SATELLITE_RULE_COLOR_CACHE = new Map();
|
||||
|
||||
function getSatelliteLegendRule(props = {}) {
|
||||
return (
|
||||
SATELLITE_LEGEND_RULES.find((rule) => rule.match(props)) ||
|
||||
SATELLITE_LEGEND_RULES[SATELLITE_LEGEND_RULES.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
function getSatelliteRuleColor(rule) {
|
||||
if (!rule) {
|
||||
return { r: 1, g: 1, b: 1 };
|
||||
}
|
||||
|
||||
if (SATELLITE_RULE_COLOR_CACHE.has(rule.key)) {
|
||||
return SATELLITE_RULE_COLOR_CACHE.get(rule.key);
|
||||
}
|
||||
|
||||
const color = new THREE.Color(rule.color);
|
||||
const rgb = { r: color.r, g: color.g, b: color.b };
|
||||
SATELLITE_RULE_COLOR_CACHE.set(rule.key, rgb);
|
||||
return rgb;
|
||||
}
|
||||
|
||||
export function updateBreathingPhase(deltaTime = 16) {
|
||||
breathingPhase += SATELLITE_CONFIG.breathingSpeed * (deltaTime / 16);
|
||||
}
|
||||
@@ -98,31 +136,15 @@ export function getSatelliteLegendItems() {
|
||||
.filter((item) => presentKeys.has(item.key))
|
||||
.map(({ key, label, color }) => ({ key, label, color }));
|
||||
|
||||
if (!selectedSatelliteLegendKey) {
|
||||
return items.map(({ label, color }) => ({ label, color }));
|
||||
}
|
||||
|
||||
const selectedIndex = items.findIndex(
|
||||
(item) => item.key === selectedSatelliteLegendKey,
|
||||
);
|
||||
|
||||
if (selectedIndex > 0) {
|
||||
const [selectedItem] = items.splice(selectedIndex, 1);
|
||||
items.unshift(selectedItem);
|
||||
}
|
||||
|
||||
return items.map(({ label, color }) => ({ label, color }));
|
||||
}
|
||||
|
||||
export function setSelectedSatelliteLegend(props) {
|
||||
const rule = SATELLITE_LEGEND_RULES.find((item) =>
|
||||
item.match(props || {}),
|
||||
);
|
||||
selectedSatelliteLegendKey = rule?.key || null;
|
||||
return getSatelliteLegendRule(props || {});
|
||||
}
|
||||
|
||||
export function clearSelectedSatelliteLegend() {
|
||||
selectedSatelliteLegendKey = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function disposeMaterial(material) {
|
||||
@@ -538,36 +560,8 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
|
||||
positions[i * 3 + 1] = pos.y;
|
||||
positions[i * 3 + 2] = pos.z;
|
||||
|
||||
const inclination = props?.inclination || 53;
|
||||
const name = props?.name || "";
|
||||
const isStarlink = name.includes("STARLINK");
|
||||
const isGeo = inclination > 20 && inclination < 30;
|
||||
const isIridium = name.includes("IRIDIUM");
|
||||
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
if (isStarlink) {
|
||||
r = 0.0;
|
||||
g = 0.9;
|
||||
b = 1.0;
|
||||
} else if (isGeo) {
|
||||
r = 1.0;
|
||||
g = 0.8;
|
||||
b = 0.0;
|
||||
} else if (isIridium) {
|
||||
r = 1.0;
|
||||
g = 0.5;
|
||||
b = 0.0;
|
||||
} else if (inclination > 50 && inclination < 70) {
|
||||
r = 0.0;
|
||||
g = 1.0;
|
||||
b = 0.3;
|
||||
} else {
|
||||
r = 1.0;
|
||||
g = 1.0;
|
||||
b = 1.0;
|
||||
}
|
||||
const rule = getSatelliteLegendRule(props);
|
||||
const { r, g, b } = getSatelliteRuleColor(rule);
|
||||
|
||||
colors[i * 3] = r;
|
||||
colors[i * 3 + 1] = g;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.28.2"
|
||||
version = "0.29.0"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user