Compare commits

...

3 Commits

Author SHA1 Message Date
rayd1o
f9c1334365 release: bump version to 0.40.5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 05:03:30 +08:00
rayd1o
5f47ec1659 release: bump version to 0.40.4 2026-04-26 01:41:29 +08:00
rayd1o
229be0bced release: bump version to 0.40.3 2026-04-25 23:02:22 +08:00
11 changed files with 677 additions and 203 deletions

View File

@@ -1 +1 @@
0.40.2
0.40.5

View File

@@ -8,8 +8,40 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`bugfix + 小功能混合)
- `bugfix` -> `+0.0.1`
## [0.40.5] — 2026-04-26
### 🔧 Improvements
- 卫星拖尾改用 Instanced screen-space ribbon单 draw call 渲染所有轨迹段,支持像素级宽度控制
- Iridium 地面覆盖重写为球面投影径向网格,修复填充光晕不可见问题;新增外圈 LineLoop
- 搜索面板打开时改用双 rAF 延迟聚焦输入框,确保 CSS 过渡完成后焦点可靠触发
- 代码清理:提取 `IRIDIUM_OVERLAY_COLOR``IRIDIUM_REFERENCE_ALTITUDE_KM` 常量,消除重复三角函数调用
---
## [0.39.0] — 2026-04-24
## [0.40.4] — 2026-04-26
### 🔧 Improvements
- 新增页面可见性恢复处理,页面从后台切回前台时主动刷新卫星位置,避免累积后台时间在下一帧一次性回放
- 抽出卫星轨迹状态与轨迹几何清理 helper统一后台恢复与清空数据时的轨迹重置路径
### 🐛 Fixes
- 修复页面在后台停留较久后恢复前台时,卫星轨迹因超大 `deltaTime` 突然跳变、拖尾异常拉长的问题
- 修复后台恢复后首帧仍沿用旧轨迹缓存,导致轨迹与当前卫星位置短时错位的问题
---
## [0.40.3] — 2026-04-25
### 🔧 Improvements
- 卫星点云升级为自定义 ShaderMaterial支持 per-point alpha 控制,锁定/悬停卫星从点云中精确隐藏
- 修复锁定环与自发光选中标记的 depthTest 错误false → true消除远端渲染穿透 artifact
- 新增锁定环悬停态缩放与线宽LOCKED_RING_HOVER_SCALE / LOCKED_RING_HOVER_LINE_WIDTH
- 修复 updateLockedDotWorldTransform / updateLockedHaloWorldTransform 未强制刷新 matrixWorld 导致的位置漂移
---
## [0.40.2] — 2026-04-24
### 🔧 Improvements

View File

@@ -16,12 +16,15 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
- `dev` 当前开发分支历史推导到:`0.40.2`
- `dev` 当前开发分支历史推导到:`0.40.5`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
| `0.40.5` | improvement | `dev` | `pending` | 卫星 ribbon 拖尾、Iridium 覆盖球面投影填充+外圈、搜索自动聚焦修复 |
| `0.40.4` | bugfix | `dev` | `pending` | 修复页面后台恢复后卫星轨迹跳变与位置错位,统一轨迹重置路径 |
| `0.40.3` | improvement | `dev` | `pending` | 卫星点云升级 ShaderMaterial修复锁定环 depthTest 与位置漂移,新增悬停态缩放 |
| `0.40.2` | improvement | `dev` | `pending` | 卫星点大小随镜头缩放动态调整,调小默认基础尺寸 |
| `0.40.1` | improvement | `dev` | `pending` | 卫星选中标记配色跟随图例,修复 footprint 遮蔽卫星渲染问题,修复选中海缆误触发卫星高亮 |
| `0.40.0` | feature | `dev` | `pending` | Earth 卫星 footprint 按星座能力分层Iridium 独立 coverage ring 落地,卫星详情卡补齐覆盖能力与当前显示说明 |

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.40.2",
"version": "0.40.5",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -277,13 +277,16 @@ export const CABLE_STATE = {
export const SATELLITE_CONFIG = {
maxCount: -1,
initialLoadCount: 2400,
hydrateFullAfterInitialLoad: true,
initialLoadCount: null,
hydrateFullAfterInitialLoad: false,
trailLength: 10,
trailLineWidth: 3,
displayAltitudeOffset: 8,
frontFacingDotThreshold: 0.015,
overlayRenderOrder: 12,
dotSize: 2.8,
dotBaseSize: 2.8,
dotBackdropScale: 1.28,
dotZoomScalePower: 1,
ringSize: 0.07,
apiPath: '/api/v1/visualization/geo/satellites',
breathingSpeed: 0.08,

View File

@@ -5,7 +5,11 @@ const SURFACE_SCALE = 1.003;
const SURFACE_OFFSET = 0.72;
const CLUSTER_DIAMETER_KM_APPROX = 4500;
const CLUSTER_RADIUS_KM_BASE = CLUSTER_DIAMETER_KM_APPROX / 2;
const SURFACE_AXIS = new THREE.Vector3(0, 0, 1);
const IRIDIUM_OVERLAY_COLOR = 0x5faeff;
const IRIDIUM_REFERENCE_ALTITUDE_KM = 780;
const FILL_RINGS = 12;
const FILL_SEGMENTS = 48;
const RING_SEGMENTS = 72;
function disposeMaterial(material) {
if (!material) return;
@@ -19,34 +23,27 @@ function disposeMaterial(material) {
function disposeObjectTree(object) {
if (!object) return;
object.traverse((child) => {
if (child.geometry) {
child.geometry.dispose();
}
if (child.material) {
disposeMaterial(child.material);
}
if (child.geometry) child.geometry.dispose();
if (child.material) disposeMaterial(child.material);
});
}
function createIridiumClusterMaterial() {
function createIridiumFillMaterial() {
return new THREE.ShaderMaterial({
transparent: true,
side: THREE.DoubleSide,
depthTest: true,
depthWrite: false,
polygonOffset: true,
polygonOffsetFactor: -3,
polygonOffsetUnits: -3,
blending: THREE.AdditiveBlending,
uniforms: {
uColor: { value: new THREE.Color(0x5faeff) },
uOpacity: { value: 0.24 },
uColor: { value: new THREE.Color(IRIDIUM_OVERLAY_COLOR) },
uOpacity: { value: 0.55 },
},
vertexShader: `
attribute vec2 aUv;
varying vec2 vUv;
void main() {
vUv = uv;
vUv = aUv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
@@ -54,12 +51,10 @@ function createIridiumClusterMaterial() {
uniform vec3 uColor;
uniform float uOpacity;
varying vec2 vUv;
void main() {
vec2 p = vUv * 2.0 - 1.0;
float ellipseMetric = p.x * p.x * 0.82 + p.y * p.y * 1.06;
float alpha = exp(-ellipseMetric * 1.05) * (1.0 - smoothstep(0.86, 1.24, ellipseMetric));
alpha *= uOpacity;
float r2 = dot(vUv, vUv);
float glow = exp(-r2 * 1.4) * (1.0 - smoothstep(0.72, 1.0, r2));
float alpha = glow * uOpacity;
if (alpha <= 0.001) discard;
gl_FragColor = vec4(uColor, alpha);
}
@@ -67,6 +62,17 @@ function createIridiumClusterMaterial() {
});
}
function createIridiumRingMaterial() {
return new THREE.LineBasicMaterial({
color: new THREE.Color(IRIDIUM_OVERLAY_COLOR),
transparent: true,
opacity: 0.75,
blending: THREE.AdditiveBlending,
depthTest: true,
depthWrite: false,
});
}
function projectOffsetToSurface(
centerNormal,
alongTrack,
@@ -88,13 +94,55 @@ function projectOffsetToSurface(
function computeClusterRadiusKm(altitudeKm) {
const altitudeScale = THREE.MathUtils.clamp(
(Number(altitudeKm) || 780) / 780,
(Number(altitudeKm) || IRIDIUM_REFERENCE_ALTITUDE_KM) / IRIDIUM_REFERENCE_ALTITUDE_KM,
0.88,
1.18,
);
return CLUSTER_RADIUS_KM_BASE * altitudeScale;
}
function buildFillGeometry() {
// Radial grid: center + FILL_RINGS rings × FILL_SEGMENTS points each.
// Positions are updated in world space each frame; indices are static.
const vertexCount = 1 + FILL_RINGS * FILL_SEGMENTS;
const positions = new Float32Array(vertexCount * 3);
const uvs = new Float32Array(vertexCount * 2);
// Center vertex: uv = (0,0)
// Edge vertices: uv on unit circle, r = ring/FILL_RINGS
const indices = [];
// Center to first ring: triangle fan
for (let s = 0; s < FILL_SEGMENTS; s++) {
const a = 1 + s;
const b = 1 + (s + 1) % FILL_SEGMENTS;
indices.push(0, a, b);
}
// Ring to ring
for (let r = 0; r < FILL_RINGS - 1; r++) {
const ringBase = 1 + r * FILL_SEGMENTS;
const nextBase = ringBase + FILL_SEGMENTS;
for (let s = 0; s < FILL_SEGMENTS; s++) {
const s1 = (s + 1) % FILL_SEGMENTS;
indices.push(ringBase + s, nextBase + s, ringBase + s1);
indices.push(nextBase + s, nextBase + s1, ringBase + s1);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("aUv", new THREE.BufferAttribute(uvs, 2));
geometry.setIndex(indices);
return geometry;
}
function buildRingGeometry() {
const positions = new Float32Array(RING_SEGMENTS * 3);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
return geometry;
}
export function createIridiumFootprintAdapter({
earthObj,
earthRadiusWorld,
@@ -105,19 +153,21 @@ export function createIridiumFootprintAdapter({
const group = new THREE.Group();
group.name = "iridium-footprint-overlay";
group.renderOrder = renderOrder;
group.userData = {
earthRadiusWorld,
clusterGlow: null,
};
group.userData = { earthRadiusWorld, fill: null, outerRing: null };
const clusterGlow = new THREE.Mesh(
new THREE.CircleGeometry(1, 72),
createIridiumClusterMaterial(),
);
clusterGlow.name = "iridium-cluster-glow";
clusterGlow.renderOrder = renderOrder - 1;
group.add(clusterGlow);
group.userData.clusterGlow = clusterGlow;
const fill = new THREE.Mesh(buildFillGeometry(), createIridiumFillMaterial());
fill.name = "iridium-cluster-fill";
fill.renderOrder = renderOrder;
fill.frustumCulled = false;
group.add(fill);
group.userData.fill = fill;
const outerRing = new THREE.LineLoop(buildRingGeometry(), createIridiumRingMaterial());
outerRing.name = "iridium-outer-ring";
outerRing.renderOrder = renderOrder;
outerRing.frustumCulled = false;
group.add(outerRing);
group.userData.outerRing = outerRing;
earthObj.add(group);
return group;
@@ -129,30 +179,65 @@ export function updateIridiumFootprintAdapter(
) {
if (!group || !position || !alongTrack || !crossTrack) return;
const earthRadiusWorld =
group.userData?.earthRadiusWorld || EARTH_RADIUS_KM;
const earthRadiusWorld = group.userData?.earthRadiusWorld || EARTH_RADIUS_KM;
const centerNormal = position.clone().normalize();
const clusterRadiusKm = computeClusterRadiusKm(altitudeKm);
const clusterGlow = group.userData?.clusterGlow || null;
const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM;
const alongRadiusKm = clusterRadiusKm * 1.18;
const crossRadiusKm = clusterRadiusKm * 0.96;
if (clusterGlow) {
const clusterCenter = projectOffsetToSurface(
centerNormal,
alongTrack,
crossTrack,
0,
0,
earthRadiusWorld,
);
const clusterNormal = clusterCenter.clone().normalize();
clusterGlow.position.copy(clusterCenter);
clusterGlow.quaternion.setFromUnitVectors(SURFACE_AXIS, clusterNormal);
clusterGlow.scale.set(
clusterRadiusKm * worldUnitsPerKm * 1.18,
clusterRadiusKm * worldUnitsPerKm * 0.96,
1,
const fill = group.userData?.fill;
if (fill) {
const posAttr = fill.geometry.attributes.position;
const uvAttr = fill.geometry.attributes.aUv;
// Center vertex
const center = projectOffsetToSurface(
centerNormal, alongTrack, crossTrack, 0, 0, earthRadiusWorld,
);
posAttr.setXYZ(0, center.x, center.y, center.z);
uvAttr.setXY(0, 0, 0);
// Ring vertices
for (let r = 1; r <= FILL_RINGS; r++) {
const t = r / FILL_RINGS;
const aKm = alongRadiusKm * t;
const cKm = crossRadiusKm * t;
for (let s = 0; s < FILL_SEGMENTS; s++) {
const angle = (s / FILL_SEGMENTS) * Math.PI * 2;
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
const pt = projectOffsetToSurface(
centerNormal, alongTrack, crossTrack,
aKm * cosA,
cKm * sinA,
earthRadiusWorld,
);
const vi = 1 + (r - 1) * FILL_SEGMENTS + s;
posAttr.setXYZ(vi, pt.x, pt.y, pt.z);
uvAttr.setXY(vi, t * cosA, t * sinA);
}
}
posAttr.needsUpdate = true;
uvAttr.needsUpdate = true;
fill.geometry.computeBoundingSphere();
}
const outerRing = group.userData?.outerRing;
if (outerRing) {
const posAttr = outerRing.geometry.attributes.position;
for (let k = 0; k < RING_SEGMENTS; k++) {
const angle = (k / RING_SEGMENTS) * Math.PI * 2;
const pt = projectOffsetToSurface(
centerNormal, alongTrack, crossTrack,
alongRadiusKm * Math.cos(angle),
crossRadiusKm * Math.sin(angle),
earthRadiusWorld,
);
posAttr.setXYZ(k, pt.x, pt.y, pt.z);
}
posAttr.needsUpdate = true;
outerRing.geometry.computeBoundingSphere();
}
}

View File

@@ -242,6 +242,7 @@ let activeTouchPoints = new Map();
let pinchGesture = null;
let pointerDragDistance = 0;
let suppressNextClick = false;
let shouldRefreshSatellitesAfterVisibilityResume = false;
const clock = new THREE.Clock();
const interactionRaycaster = new THREE.Raycaster();
@@ -2392,6 +2393,7 @@ export async function setSatellitesEnabled(
function setupEventListeners() {
const handleResize = () => onWindowResize();
const handleVisibilityChange = () => onVisibilityChange();
const handlePointerMove = (event) => onPointerMove(event);
const handlePointerDown = (event) => onPointerDown(event);
const handlePointerUp = (event) => onPointerUp(event);
@@ -2403,6 +2405,7 @@ function setupEventListeners() {
const handleInfoCardDrag = () => repositionCruiseConnector();
bindListener(window, "resize", handleResize);
bindListener(document, "visibilitychange", handleVisibilityChange);
bindListener(window, "pagehide", handlePageHide);
bindListener(window, "beforeunload", handlePageHide);
bindListener(window, "earth:rotation-mode-change", handleRotationMode);
@@ -2425,6 +2428,20 @@ function setupEventListeners() {
}
}
function onVisibilityChange() {
if (document.hidden) {
shouldRefreshSatellitesAfterVisibilityResume = true;
return;
}
if (!shouldRefreshSatellitesAfterVisibilityResume) return;
shouldRefreshSatellitesAfterVisibilityResume = false;
// Drop the elapsed background time so the next RAF does not replay it.
clock.getDelta();
updateSatellitePositions(0, true, { resetTrails: true });
}
function updateHudScale() {
const widthScale = window.innerWidth / HUD_CONFIG.scaleReferenceWidth;
const heightScale = window.innerHeight / HUD_CONFIG.scaleReferenceHeight;

View File

@@ -66,8 +66,64 @@ const SATELLITE_CONSTELLATION_LABELS = Object.freeze({
});
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
const TRAIL_RIBBON_VERTEX_SHADER = /* glsl */ `
attribute vec3 instanceStart;
attribute vec3 instanceEnd;
attribute vec3 instanceColorStart;
attribute vec3 instanceColorEnd;
uniform vec2 resolution;
uniform float lineWidth;
varying vec3 vColor;
void main() {
float t = position.x;
float side = position.y;
vec4 clipStart = projectionMatrix * modelViewMatrix * vec4(instanceStart, 1.0);
vec4 clipEnd = projectionMatrix * modelViewMatrix * vec4(instanceEnd, 1.0);
vec2 screenStart = (clipStart.xy / clipStart.w * 0.5 + 0.5) * resolution;
vec2 screenEnd = (clipEnd.xy / clipEnd.w * 0.5 + 0.5) * resolution;
vec2 dir = screenEnd - screenStart;
float segLen = length(dir);
vec4 clipPos = mix(clipStart, clipEnd, t);
if (segLen > 0.001) {
dir /= segLen;
vec2 normal = vec2(-dir.y, dir.x);
clipPos.xy += normal * side * lineWidth * 0.5 / resolution * 2.0 * clipPos.w;
vColor = mix(instanceColorStart, instanceColorEnd, t);
} else {
vColor = vec3(0.0);
}
gl_Position = clipPos;
}
`;
const TRAIL_RIBBON_FRAGMENT_SHADER = /* glsl */ `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`;
const TRAIL_INSTANCE_ATTRIBUTE_NAMES = [
"instanceStart",
"instanceEnd",
"instanceColorStart",
"instanceColorEnd",
];
const FALLBACK_ORBIT_DAY_MS = 24 * 60 * 60 * 1000;
const FALLBACK_MIN_MEAN_MOTION = 12;
const FALLBACK_MEAN_MOTION_SPREAD = 4;
const FALLBACK_TRAIL_TIP_LENGTH = 0.004;
const FALLBACK_TRAIL_ALPHA_START = 0.2;
const FALLBACK_TRAIL_ALPHA_END = 0.8;
const DOT_TEXTURE_SIZE = 32;
const POSITION_UPDATE_INTERVAL_MS = 250;
const BACKGROUND_TRAIL_RESET_DELTA_MS = 2000;
const DIMMED_SATELLITE_BRIGHTNESS = 0.42;
const DIMMED_SATELLITE_TRAIL_BRIGHTNESS = 0.24;
const DIMMED_SATELLITE_POINT_OPACITY = 0.62;
@@ -81,8 +137,10 @@ const LOCKED_HALO_CORE_PIXEL_RADIUS = 8;
const LOCKED_HALO_PIXEL_RADIUS = 24;
const FILLED_TEXTURE_RADIUS_RATIO = 0.28;
const LOCKED_RING_IDLE_SCALE = 0.68;
const LOCKED_RING_HOVER_SCALE = 1.32;
const LOCKED_RING_HOVER_LINE_WIDTH = 5;
const HOVER_RING_LINE_WIDTH = 3;
const LOCKED_RING_IDLE_OPACITY = 0.92;
const DOT_ZOOM_SCALE_POWER = 1;
const EARTH_RADIUS_KM = 6378.137;
const GROUND_FOOTPRINT_MIN_ELEVATION_DEG = 25;
const GROUND_FOOTPRINT_RADIUS_OFFSET = 0.72;
@@ -112,6 +170,71 @@ const satelliteSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
export let breathingPhase = 0;
function getPointPixelRatio() {
return typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
}
function getSatelliteDotBaseSize() {
return SATELLITE_CONFIG.dotBaseSize;
}
function getSatelliteDotBackdropSize() {
return getSatelliteDotBaseSize() * SATELLITE_CONFIG.dotBackdropScale;
}
function getSatelliteDotZoomScale(cameraDistance) {
return Math.pow(CONFIG.defaultCameraZ / Math.max(cameraDistance, 1), SATELLITE_CONFIG.dotZoomScalePower);
}
function createSatellitePointMaterial({
texture,
size,
opacity = 1,
useVertexColor = true,
baseColor = 0xffffff,
alphaTest = 0.04,
}) {
return new THREE.ShaderMaterial({
uniforms: {
pointTexture: { value: texture },
size: { value: size * getPointPixelRatio() },
opacity: { value: opacity },
baseColor: { value: new THREE.Color(baseColor) },
},
transparent: true,
depthTest: true,
depthWrite: false,
vertexShader: `
uniform float size;
uniform vec3 baseColor;
attribute float alpha;
${useVertexColor ? "attribute vec3 color;" : ""}
varying float vAlpha;
varying vec3 vColor;
void main() {
vAlpha = alpha;
vColor = ${useVertexColor ? "color" : "baseColor"};
gl_PointSize = size;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D pointTexture;
uniform float opacity;
varying float vAlpha;
varying vec3 vColor;
void main() {
vec4 texel = texture2D(pointTexture, gl_PointCoord);
float alphaValue = texel.a * opacity * vAlpha;
if (alphaValue < ${alphaTest.toFixed(3)}) discard;
gl_FragColor = vec4(vColor * texel.rgb, alphaValue);
}
`,
});
}
const SATELLITE_LEGEND_RULES = [
{
key: "equatorial",
@@ -197,9 +320,22 @@ export function updateBreathingPhase(deltaTime = 16) {
export function updateSatellitePointSize() {
if (!satellitePoints || !cameraRef) return;
const camDist = cameraRef.position.length();
const zoomScale = Math.pow(CONFIG.defaultCameraZ / Math.max(camDist, 1), DOT_ZOOM_SCALE_POWER);
satellitePoints.material.size = SATELLITE_CONFIG.dotSize * zoomScale;
satelliteBackdropPoints.material.size = SATELLITE_CONFIG.dotSize * 1.28 * zoomScale;
const zoomScale = getSatelliteDotZoomScale(camDist);
const pointPixelRatio = getPointPixelRatio();
const dotSize = getSatelliteDotBaseSize() * zoomScale;
const backdropSize = getSatelliteDotBackdropSize() * zoomScale;
const shaderDotSize = dotSize * pointPixelRatio;
const shaderBackdropSize = backdropSize * pointPixelRatio;
if (satellitePoints.material.uniforms?.size) {
satellitePoints.material.uniforms.size.value = shaderDotSize;
} else {
satellitePoints.material.size = dotSize;
}
if (satelliteBackdropPoints.material.uniforms?.size) {
satelliteBackdropPoints.material.uniforms.size.value = shaderBackdropSize;
} else {
satelliteBackdropPoints.material.size = backdropSize;
}
}
function getBreathingPulse(phase) {
@@ -338,7 +474,7 @@ function createBackdropDotTexture() {
return texture;
}
function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
function createRingTexture(innerRadius, outerRadius, color = "#ffffff", lineWidth = 3) {
const size = DOT_TEXTURE_SIZE * 2;
const canvas = document.createElement("canvas");
canvas.width = size;
@@ -347,7 +483,7 @@ function createRingTexture(innerRadius, outerRadius, color = "#ffffff") {
const center = size / 2;
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.arc(center, center, (innerRadius + outerRadius) / 2, 0, Math.PI * 2);
ctx.stroke();
@@ -384,26 +520,21 @@ export function createSatellites(scene, earthObj) {
const pointsGeometry = new THREE.BufferGeometry();
const backdropGeometry = new THREE.BufferGeometry();
const backdropMaterial = new THREE.PointsMaterial({
size: SATELLITE_CONFIG.dotSize * 1.28,
map: backdropTexture,
color: 0x0b1626,
transparent: true,
const backdropMaterial = createSatellitePointMaterial({
size: getSatelliteDotBackdropSize(),
texture: backdropTexture,
useVertexColor: false,
baseColor: 0x0b1626,
opacity: 0.42,
sizeAttenuation: false,
alphaTest: 0.04,
depthWrite: false,
});
const pointsMaterial = new THREE.PointsMaterial({
size: SATELLITE_CONFIG.dotSize,
map: dotTexture,
vertexColors: true,
transparent: true,
const pointsMaterial = createSatellitePointMaterial({
size: getSatelliteDotBaseSize(),
texture: dotTexture,
useVertexColor: true,
opacity: 0.9,
sizeAttenuation: false,
alphaTest: 0.1,
depthWrite: false,
});
satelliteBackdropPoints = new THREE.Points(backdropGeometry, backdropMaterial);
@@ -444,16 +575,32 @@ export function createSatellites(scene, earthObj) {
earthObj.add(satelliteBackdropPoints);
earthObj.add(satellitePoints);
const trailGeometry = new THREE.BufferGeometry();
// Instanced screen-space ribbon: one quad instance per trail segment.
// Single mesh / single draw call for all satellite trails.
const ribbonGeometry = new THREE.InstancedBufferGeometry();
// Base quad: position.x = t (0=seg-start, 1=seg-end), position.y = side (-1/+1)
ribbonGeometry.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array([0, -1, 0, 0, 1, 0, 1, -1, 0, 1, 1, 0]), 3),
);
ribbonGeometry.setIndex(new THREE.BufferAttribute(new Uint16Array([0, 2, 1, 2, 3, 1]), 1));
const trailMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
const trailResolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
const trailMaterial = new THREE.ShaderMaterial({
uniforms: {
lineWidth: { value: SATELLITE_CONFIG.trailLineWidth },
resolution: { value: trailResolution },
},
vertexShader: TRAIL_RIBBON_VERTEX_SHADER,
fragmentShader: TRAIL_RIBBON_FRAGMENT_SHADER,
transparent: true,
opacity: 0.3,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
satelliteTrails = new THREE.LineSegments(trailGeometry, trailMaterial);
satelliteTrails = new THREE.Mesh(ribbonGeometry, trailMaterial);
satelliteTrails.onBeforeRender = (renderer) => renderer.getSize(trailResolution);
satelliteTrails.frustumCulled = false;
satelliteTrails.visible = false;
satelliteTrails.userData = { type: "satelliteTrails" };
earthObj.add(satelliteTrails);
@@ -481,6 +628,31 @@ function createSatellitePositionState() {
};
}
function resetSatelliteTrailState() {
satellitePositions.forEach((position) => {
position.trail = [];
position.trailIndex = 0;
position.trailCount = 0;
});
}
function clearSatelliteTrailGeometry() {
if (!satelliteTrails) return;
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
const attr = satelliteTrails.geometry.attributes[name];
if (attr?.array) {
attr.array.fill(0);
attr.needsUpdate = true;
}
}
}
function clearSatelliteTrails() {
resetSatelliteTrailState();
clearSatelliteTrailGeometry();
}
function ensureSatelliteCapacity(count) {
if (!satellitePoints || !satelliteBackdropPoints || !satelliteTrails) return;
@@ -492,16 +664,27 @@ function ensureSatelliteCapacity(count) {
const previousBackdropPositions =
satelliteBackdropPoints.geometry.attributes.position?.array || null;
const previousColors = satellitePoints.geometry.attributes.color?.array || null;
const previousTrailPositions =
satelliteTrails.geometry.attributes.position?.array || null;
const previousTrailColors =
satelliteTrails.geometry.attributes.color?.array || null;
const previousPointAlphas = satellitePoints.geometry.attributes.alpha?.array || null;
const previousBackdropAlphas =
satelliteBackdropPoints.geometry.attributes.alpha?.array || null;
const previousInstanceStarts =
satelliteTrails.geometry.attributes.instanceStart?.array || null;
const previousInstanceEnds =
satelliteTrails.geometry.attributes.instanceEnd?.array || null;
const previousInstanceColorStarts =
satelliteTrails.geometry.attributes.instanceColorStart?.array || null;
const previousInstanceColorEnds =
satelliteTrails.geometry.attributes.instanceColorEnd?.array || null;
const previousSatellitePositions = satellitePositions;
const previousCapacity = satelliteCapacity;
const positions = new Float32Array(nextCapacity * 3);
const backdropPositions = new Float32Array(nextCapacity * 3);
const colors = new Float32Array(nextCapacity * 3);
const pointAlphas = new Float32Array(nextCapacity);
const backdropAlphas = new Float32Array(nextCapacity);
pointAlphas.fill(1);
backdropAlphas.fill(1);
if (previousPointPositions) {
positions.set(
previousPointPositions.subarray(0, Math.min(previousPointPositions.length, positions.length)),
@@ -518,10 +701,27 @@ function ensureSatelliteCapacity(count) {
if (previousColors) {
colors.set(previousColors.subarray(0, Math.min(previousColors.length, colors.length)));
}
if (previousPointAlphas) {
pointAlphas.set(
previousPointAlphas.subarray(0, Math.min(previousPointAlphas.length, pointAlphas.length)),
);
}
if (previousBackdropAlphas) {
backdropAlphas.set(
previousBackdropAlphas.subarray(
0,
Math.min(previousBackdropAlphas.length, backdropAlphas.length),
),
);
}
satelliteBackdropPoints.geometry.setAttribute(
"position",
new THREE.BufferAttribute(backdropPositions, 3),
);
satelliteBackdropPoints.geometry.setAttribute(
"alpha",
new THREE.BufferAttribute(backdropAlphas, 1),
);
satelliteBackdropPoints.geometry.setDrawRange(
0,
Math.min(previousCapacity, nextCapacity),
@@ -534,34 +734,54 @@ function ensureSatelliteCapacity(count) {
"color",
new THREE.BufferAttribute(colors, 3),
);
satellitePoints.geometry.setAttribute(
"alpha",
new THREE.BufferAttribute(pointAlphas, 1),
);
satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity));
const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
const trailColors = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
if (previousTrailPositions) {
trailPositions.set(
previousTrailPositions.subarray(
0,
Math.min(previousTrailPositions.length, trailPositions.length),
),
const segCount = nextCapacity * (TRAIL_LENGTH - 1);
const instanceStarts = new Float32Array(segCount * 3);
const instanceEnds = new Float32Array(segCount * 3);
const instanceColorStarts = new Float32Array(segCount * 3);
const instanceColorEnds = new Float32Array(segCount * 3);
if (previousInstanceStarts) {
instanceStarts.set(
previousInstanceStarts.subarray(0, Math.min(previousInstanceStarts.length, instanceStarts.length)),
);
}
if (previousTrailColors) {
trailColors.set(
previousTrailColors.subarray(
0,
Math.min(previousTrailColors.length, trailColors.length),
),
if (previousInstanceEnds) {
instanceEnds.set(
previousInstanceEnds.subarray(0, Math.min(previousInstanceEnds.length, instanceEnds.length)),
);
}
if (previousInstanceColorStarts) {
instanceColorStarts.set(
previousInstanceColorStarts.subarray(0, Math.min(previousInstanceColorStarts.length, instanceColorStarts.length)),
);
}
if (previousInstanceColorEnds) {
instanceColorEnds.set(
previousInstanceColorEnds.subarray(0, Math.min(previousInstanceColorEnds.length, instanceColorEnds.length)),
);
}
satelliteTrails.geometry.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3),
"instanceStart",
new THREE.InstancedBufferAttribute(instanceStarts, 3),
);
satelliteTrails.geometry.setAttribute(
"color",
new THREE.BufferAttribute(trailColors, 3),
"instanceEnd",
new THREE.InstancedBufferAttribute(instanceEnds, 3),
);
satelliteTrails.geometry.setAttribute(
"instanceColorStart",
new THREE.InstancedBufferAttribute(instanceColorStarts, 3),
);
satelliteTrails.geometry.setAttribute(
"instanceColorEnd",
new THREE.InstancedBufferAttribute(instanceColorEnds, 3),
);
satelliteTrails.geometry.instanceCount = segCount;
satellitePositions = Array.from({ length: nextCapacity }, (_, index) => {
const previousState = previousSatellitePositions[index];
@@ -579,6 +799,25 @@ function ensureSatelliteCapacity(count) {
satelliteCapacity = nextCapacity;
}
function shouldHideSatellitePoint(index) {
return index === hoveredSatelliteIndex || index === lockedSatelliteIndex;
}
function updateSatellitePointVisibilityAttributes(count = satelliteData.length) {
const pointAlphaAttr = satellitePoints?.geometry?.attributes?.alpha;
const backdropAlphaAttr = satelliteBackdropPoints?.geometry?.attributes?.alpha;
if (!pointAlphaAttr?.array || !backdropAlphaAttr?.array) return;
const visibleCount = Math.min(count, pointAlphaAttr.array.length, backdropAlphaAttr.array.length);
for (let i = 0; i < visibleCount; i++) {
const alpha = shouldHideSatellitePoint(i) ? 0 : 1;
pointAlphaAttr.array[i] = alpha;
backdropAlphaAttr.array[i] = alpha;
}
pointAlphaAttr.needsUpdate = true;
backdropAlphaAttr.needsUpdate = true;
}
function computeSatellitePosition(satellite, time) {
try {
const props = satellite.properties;
@@ -752,7 +991,7 @@ function buildTleLinesFromElements(props, fallbackTime) {
};
}
function generateFallbackPosition(satellite, index, total) {
function generateFallbackPosition(satellite, index, total, time = new Date()) {
const radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
const noradId = satellite.properties?.norad_cat_id || index;
@@ -764,9 +1003,21 @@ function generateFallbackPosition(satellite, index, total) {
.split("")
.reduce((a, b) => a + b.charCodeAt(0), 0);
const randomOffset = (hash % 1000) / 1000;
const rawMeanMotion = Number(satellite.properties?.mean_motion);
const meanMotion =
Number.isFinite(rawMeanMotion) && rawMeanMotion > 0
? rawMeanMotion
: FALLBACK_MIN_MEAN_MOTION + randomOffset * FALLBACK_MEAN_MOTION_SPREAD;
const normalizedIndex = index / total;
const theta = normalizedIndex * Math.PI * 2 * 10 + (raan * Math.PI) / 180;
const elapsedDays = Number.isFinite(time?.getTime?.())
? time.getTime() / FALLBACK_ORBIT_DAY_MS
: Date.now() / FALLBACK_ORBIT_DAY_MS;
const fallbackPhase = elapsedDays * meanMotion * Math.PI * 2;
const theta =
normalizedIndex * Math.PI * 2 * 10 +
(raan * Math.PI) / 180 +
fallbackPhase;
const phi =
(inclination * Math.PI) / 180 + ((meanAnomaly * Math.PI) / 180) * 0.1;
@@ -795,6 +1046,7 @@ export async function loadSatellites(options = {}) {
const data = await response.json();
satelliteData = data.features || [];
satelliteSatrecCache = new Map();
resetSatelliteTrailState();
ensureSatelliteCapacity(satelliteData.length);
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
return {
@@ -803,31 +1055,49 @@ export async function loadSatellites(options = {}) {
};
}
export function updateSatellitePositions(deltaTime = 0, force = false) {
export function updateSatellitePositions(deltaTime = 0, force = false, options = {}) {
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
const shouldUpdateTrails =
showSatellites || showTrails || lockedSatelliteIndex !== null;
positionUpdateAccumulator += deltaTime;
showSatellites ||
showTrails ||
lockedSatelliteIndex !== null;
const shouldResetTrails =
options.resetTrails ||
(!force && deltaTime >= BACKGROUND_TRAIL_RESET_DELTA_MS);
if (shouldResetTrails) {
clearSatelliteTrails();
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
} else {
positionUpdateAccumulator += deltaTime;
}
if (!force && positionUpdateAccumulator < POSITION_UPDATE_INTERVAL_MS) {
return;
}
const elapsedMs = Math.max(
positionUpdateAccumulator,
POSITION_UPDATE_INTERVAL_MS,
);
const elapsedMs = shouldResetTrails
? 0
: Math.max(
positionUpdateAccumulator,
POSITION_UPDATE_INTERVAL_MS,
);
positionUpdateAccumulator = 0;
const positions = satellitePoints.geometry.attributes.position.array;
const backdropPositions =
satelliteBackdropPoints.geometry.attributes.position.array;
const colors = satellitePoints.geometry.attributes.color.array;
const trailPositions = satelliteTrails.geometry.attributes.position.array;
const trailColors = satelliteTrails.geometry.attributes.color.array;
const pointAlphas = satellitePoints.geometry.attributes.alpha.array;
const backdropAlphas = satelliteBackdropPoints.geometry.attributes.alpha.array;
const instanceStarts = satelliteTrails.geometry.attributes.instanceStart.array;
const instanceEnds = satelliteTrails.geometry.attributes.instanceEnd.array;
const instanceColorStarts = satelliteTrails.geometry.attributes.instanceColorStart.array;
const instanceColorEnds = satelliteTrails.geometry.attributes.instanceColorEnd.array;
const baseTime = new Date(Date.now() + elapsedMs);
const count = Math.min(satelliteData.length, satelliteCapacity);
let trailSegmentCount = 0;
for (let i = 0; i < count; i++) {
const satellite = satelliteData[i];
@@ -839,16 +1109,30 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
let pos = computeSatellitePosition(satellite, adjustedTime);
if (!pos) {
pos = generateFallbackPosition(satellite, i, count);
pos = generateFallbackPosition(satellite, i, count, adjustedTime);
}
satellitePositions[i].current.copy(pos);
if (shouldUpdateTrails && i !== lockedSatelliteIndex) {
if (shouldUpdateTrails) {
const satPos = satellitePositions[i];
satPos.trail[satPos.trailIndex] = pos.clone();
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
if (satPos.trailCount === 0 && TRAIL_LENGTH > 1) {
for (let k = 0; k < TRAIL_LENGTH; k++) {
const offsetMs = (TRAIL_LENGTH - 1 - k) * POSITION_UPDATE_INTERVAL_MS;
const pastTime = new Date(adjustedTime.getTime() - offsetMs);
let pastPos = computeSatellitePosition(satellite, pastTime);
if (!pastPos) {
pastPos = generateFallbackPosition(satellite, i, count, pastTime);
}
satPos.trail[satPos.trailIndex] = pastPos;
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
}
satPos.trailCount = TRAIL_LENGTH;
} else {
satPos.trail[satPos.trailIndex] = pos.clone();
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
}
}
positions[i * 3] = pos.x;
@@ -871,34 +1155,64 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
colors[i * 3] = r * pointBrightness;
colors[i * 3 + 1] = g * pointBrightness;
colors[i * 3 + 2] = b * pointBrightness;
const pointAlpha = shouldHideSatellitePoint(i) ? 0 : 1;
pointAlphas[i] = pointAlpha;
backdropAlphas[i] = pointAlpha;
const satPosition = satellitePositions[i];
for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
if (j < satPosition.trailCount) {
const idx =
(satPosition.trailIndex - satPosition.trailCount + j + TRAIL_LENGTH) %
TRAIL_LENGTH;
const trailPoint = satPosition.trail[idx];
if (trailPoint) {
trailPositions[trailIdx] = trailPoint.x;
trailPositions[trailIdx + 1] = trailPoint.y;
trailPositions[trailIdx + 2] = trailPoint.z;
const alpha = (j + 1) / satPosition.trailCount;
trailColors[trailIdx] = r * alpha * trailBrightness;
trailColors[trailIdx + 1] = g * alpha * trailBrightness;
trailColors[trailIdx + 2] = b * alpha * trailBrightness;
continue;
const tc = satPosition.trailCount;
let hasVisibleTrail = false;
for (let j = 0; j < TRAIL_LENGTH - 1; j++) {
if (j + 1 < tc) {
const idxA =
(satPosition.trailIndex - tc + j + TRAIL_LENGTH) % TRAIL_LENGTH;
const idxB =
(satPosition.trailIndex - tc + j + 1 + TRAIL_LENGTH) % TRAIL_LENGTH;
const ptA = satPosition.trail[idxA];
const ptB = satPosition.trail[idxB];
if (ptA && ptB && ptA.distanceToSquared(ptB) > 1e-8) {
const base = trailSegmentCount * 3;
instanceStarts[base] = ptA.x;
instanceStarts[base + 1] = ptA.y;
instanceStarts[base + 2] = ptA.z;
instanceEnds[base] = ptB.x;
instanceEnds[base + 1] = ptB.y;
instanceEnds[base + 2] = ptB.z;
const alphaA = (j + 1) / tc;
const alphaB = (j + 2) / tc;
instanceColorStarts[base] = r * alphaA * trailBrightness;
instanceColorStarts[base + 1] = g * alphaA * trailBrightness;
instanceColorStarts[base + 2] = b * alphaA * trailBrightness;
instanceColorEnds[base] = r * alphaB * trailBrightness;
instanceColorEnds[base + 1] = g * alphaB * trailBrightness;
instanceColorEnds[base + 2] = b * alphaB * trailBrightness;
hasVisibleTrail = true;
trailSegmentCount++;
}
}
trailPositions[trailIdx] = pos.x;
trailPositions[trailIdx + 1] = pos.y;
trailPositions[trailIdx + 2] = pos.z;
trailColors[trailIdx] = 0;
trailColors[trailIdx + 1] = 0;
trailColors[trailIdx + 2] = 0;
}
if (!hasVisibleTrail) {
const base = trailSegmentCount * 3;
const dist = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
const nx = pos.x / dist;
const ny = pos.y / dist;
const nz = pos.z / dist;
const tip = FALLBACK_TRAIL_TIP_LENGTH;
instanceStarts[base] = pos.x + nx * tip;
instanceStarts[base + 1] = pos.y + ny * tip;
instanceStarts[base + 2] = pos.z + nz * tip;
instanceEnds[base] = pos.x;
instanceEnds[base + 1] = pos.y;
instanceEnds[base + 2] = pos.z;
const fallbackAlphaStart = FALLBACK_TRAIL_ALPHA_START * trailBrightness;
const fallbackAlphaEnd = FALLBACK_TRAIL_ALPHA_END * trailBrightness;
instanceColorStarts[base] = r * fallbackAlphaStart;
instanceColorStarts[base + 1] = g * fallbackAlphaStart;
instanceColorStarts[base + 2] = b * fallbackAlphaStart;
instanceColorEnds[base] = r * fallbackAlphaEnd;
instanceColorEnds[base + 1] = g * fallbackAlphaEnd;
instanceColorEnds[base + 2] = b * fallbackAlphaEnd;
trailSegmentCount++;
}
}
@@ -909,23 +1223,40 @@ export function updateSatellitePositions(deltaTime = 0, force = false) {
backdropPositions[i * 3] = 0;
backdropPositions[i * 3 + 1] = 0;
backdropPositions[i * 3 + 2] = 0;
pointAlphas[i] = 0;
backdropAlphas[i] = 0;
for (let j = 0; j < TRAIL_LENGTH; j++) {
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
trailPositions[trailIdx] = 0;
trailPositions[trailIdx + 1] = 0;
trailPositions[trailIdx + 2] = 0;
}
}
const trailArrayLength = instanceStarts.length / 3;
for (let i = trailSegmentCount; i < trailArrayLength; i++) {
const base = i * 3;
instanceStarts[base] = 0;
instanceStarts[base + 1] = 0;
instanceStarts[base + 2] = 0;
instanceEnds[base] = 0;
instanceEnds[base + 1] = 0;
instanceEnds[base + 2] = 0;
instanceColorStarts[base] = 0;
instanceColorStarts[base + 1] = 0;
instanceColorStarts[base + 2] = 0;
instanceColorEnds[base] = 0;
instanceColorEnds[base + 1] = 0;
instanceColorEnds[base + 2] = 0;
}
satellitePoints.geometry.attributes.position.needsUpdate = true;
satellitePoints.geometry.attributes.color.needsUpdate = true;
satellitePoints.geometry.attributes.alpha.needsUpdate = true;
satellitePoints.geometry.setDrawRange(0, count);
satelliteBackdropPoints.geometry.attributes.position.needsUpdate = true;
satelliteBackdropPoints.geometry.attributes.alpha.needsUpdate = true;
satelliteBackdropPoints.geometry.setDrawRange(0, count);
satelliteTrails.geometry.attributes.position.needsUpdate = true;
satelliteTrails.geometry.attributes.color.needsUpdate = true;
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
satelliteTrails.geometry.attributes[name].needsUpdate = true;
}
satelliteTrails.geometry.instanceCount = trailSegmentCount;
// Keep the hover ring synced with the propagated satellite position even
// when the pointer stays still and no new hover event is emitted.
@@ -1014,10 +1345,12 @@ export function setSatelliteSunDirection(direction) {
export function setLockedSatelliteIndex(index) {
lockedSatelliteIndex = index;
updateSatellitePointVisibilityAttributes();
}
export function setHoveredSatelliteIndex(index) {
hoveredSatelliteIndex = index;
updateSatellitePointVisibilityAttributes();
}
function normalizeSatelliteDisplayStyle(nextStyle) {
@@ -1190,7 +1523,7 @@ export function isSatelliteFrontFacing(index, camera = cameraRef) {
function createLockedHaloMaterial(color = "#ffbf47") {
return new THREE.ShaderMaterial({
transparent: true,
depthTest: false,
depthTest: true,
depthWrite: false,
side: THREE.DoubleSide,
uniforms: {
@@ -1397,6 +1730,7 @@ function clearLockedSatelliteStyleVisuals() {
function updateLockedDotWorldTransform(position) {
if (!lockedDotSprite || !position || !earthObjRef) return;
earthObjRef.updateMatrixWorld(true);
const worldPosition = position.clone().applyMatrix4(earthObjRef.matrixWorld);
lockedDotSprite.position.copy(worldPosition);
if (cameraRef) {
@@ -1417,6 +1751,7 @@ function updateLockedDotWorldTransform(position) {
function updateLockedHaloWorldTransform(position) {
if (!position || !earthObjRef || !lockedHaloMesh) return;
earthObjRef.updateMatrixWorld(true);
const worldPosition = position.clone().applyMatrix4(earthObjRef.matrixWorld);
const viewDirection = cameraRef
? scratchToCamera.subVectors(cameraRef.position, worldPosition).normalize()
@@ -1764,7 +2099,7 @@ function showSelfGlowStyle(position, color = "#ffd25a") {
color: new THREE.Color(color),
transparent: true,
opacity: 0.96,
depthTest: false,
depthTest: true,
depthWrite: false,
side: THREE.DoubleSide,
});
@@ -1824,6 +2159,7 @@ function createRingSprite(position, isLocked = false, color = "#ffcc00") {
8,
12,
isLocked ? color : "#ffffff",
isLocked ? LOCKED_RING_HOVER_LINE_WIDTH : HOVER_RING_LINE_WIDTH,
);
const filledTexture = isLocked
? createFilledCircleTexture(color)
@@ -1832,7 +2168,9 @@ function createRingSprite(position, isLocked = false, color = "#ffcc00") {
map: ringTexture,
transparent: true,
opacity: 0.8,
depthTest: false,
depthTest: true,
depthWrite: false,
alphaTest: 0.01,
sizeAttenuation: false,
});
@@ -1866,24 +2204,8 @@ function updateLockedMarkerVisual(isHovered) {
}
}
function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
if (!earthObjRef) return null;
const ringTexture = createRingTexture(7, 11, color);
const spriteMaterial = new THREE.SpriteMaterial({
map: ringTexture,
transparent: true,
opacity: 0.55,
depthTest: false,
sizeAttenuation: false,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.copy(position);
sprite.scale.set(SATELLITE_CONFIG.ringSize * 0.8, SATELLITE_CONFIG.ringSize * 0.8, 1);
sprite.renderOrder = SATELLITE_CONFIG.overlayRenderOrder;
earthObjRef.add(sprite);
return sprite;
function createRelatedSatelliteSprite(position) {
return createRingSprite(position, false);
}
export function showHoverRing(position, isLocked = false) {
@@ -1962,7 +2284,7 @@ export function updateLockedRingPosition(position) {
1 +
(ringPulse * 2 - 1) * SATELLITE_CONFIG.breathingScaleAmplitude;
const markerSize = isHovered
? SATELLITE_CONFIG.ringSize
? SATELLITE_CONFIG.ringSize * LOCKED_RING_HOVER_SCALE
: SATELLITE_CONFIG.ringSize * LOCKED_RING_IDLE_SCALE;
lockedRingSprite.scale.set(
markerSize * breathScale,
@@ -2026,26 +2348,38 @@ export function setSatelliteRingState(index, state, position) {
hoveredSatelliteIndex = index;
hideHoverRings();
showHoverRing(position, false);
updateSatellitePointVisibilityAttributes();
break;
case "locked":
hoveredSatelliteIndex = null;
hideHoverRings();
showHoverRing(position, true);
updateSatellitePointVisibilityAttributes();
break;
case "none":
hoveredSatelliteIndex = null;
hideHoverRings();
hideLockedRing();
updateSatellitePointVisibilityAttributes();
break;
}
}
function applyDimMaterialState(isDimmed) {
if (satellitePoints) {
satellitePoints.material.opacity = isDimmed ? DIMMED_SATELLITE_POINT_OPACITY : 0.9;
const opacity = isDimmed ? DIMMED_SATELLITE_POINT_OPACITY : 0.9;
if (satellitePoints.material.uniforms?.opacity) {
satellitePoints.material.uniforms.opacity.value = opacity;
} else {
satellitePoints.material.opacity = opacity;
}
}
if (satelliteBackdropPoints) {
satelliteBackdropPoints.material.opacity = isDimmed ? DIMMED_SATELLITE_BACKDROP_OPACITY : 0.42;
const opacity = isDimmed ? DIMMED_SATELLITE_BACKDROP_OPACITY : 0.42;
if (satelliteBackdropPoints.material.uniforms?.opacity) {
satelliteBackdropPoints.material.uniforms.opacity.value = opacity;
} else {
satelliteBackdropPoints.material.opacity = opacity;
}
}
}
@@ -2069,7 +2403,7 @@ export function highlightRelatedSatellites(indices, color = "#7dd3fc") {
indices.forEach((index) => {
const pos = satellitePositions?.[index]?.current;
if (!pos) return;
const sprite = createRelatedSatelliteSprite(pos, color);
const sprite = createRelatedSatelliteSprite(pos);
if (!sprite) return;
relatedSatelliteSprites.push({ index, sprite, color });
});
@@ -2234,14 +2568,13 @@ export function clearSatelliteData() {
satellitePositions.forEach((position) => {
position.current.set(0, 0, 0);
position.trail = [];
position.trailIndex = 0;
position.trailCount = 0;
});
resetSatelliteTrailState();
if (satellitePoints) {
const positionAttr = satellitePoints.geometry.attributes.position;
const colorAttr = satellitePoints.geometry.attributes.color;
const alphaAttr = satellitePoints.geometry.attributes.alpha;
if (positionAttr?.array) {
positionAttr.array.fill(0);
positionAttr.needsUpdate = true;
@@ -2250,31 +2583,30 @@ export function clearSatelliteData() {
colorAttr.array.fill(0);
colorAttr.needsUpdate = true;
}
if (alphaAttr?.array) {
alphaAttr.array.fill(0);
alphaAttr.needsUpdate = true;
}
satellitePoints.geometry.setDrawRange(0, 0);
}
if (satelliteBackdropPoints) {
const backdropPositionAttr =
satelliteBackdropPoints.geometry.attributes.position;
const backdropAlphaAttr =
satelliteBackdropPoints.geometry.attributes.alpha;
if (backdropPositionAttr?.array) {
backdropPositionAttr.array.fill(0);
backdropPositionAttr.needsUpdate = true;
}
if (backdropAlphaAttr?.array) {
backdropAlphaAttr.array.fill(0);
backdropAlphaAttr.needsUpdate = true;
}
satelliteBackdropPoints.geometry.setDrawRange(0, 0);
}
if (satelliteTrails) {
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
const trailColorAttr = satelliteTrails.geometry.attributes.color;
if (trailPositionAttr?.array) {
trailPositionAttr.array.fill(0);
trailPositionAttr.needsUpdate = true;
}
if (trailColorAttr?.array) {
trailColorAttr.array.fill(0);
trailColorAttr.needsUpdate = true;
}
}
clearSatelliteTrailGeometry();
hideHoverRings();
hideLockedRing();

View File

@@ -242,13 +242,15 @@ export function openSearchPanel() {
window.dispatchEvent(
new CustomEvent("earth:search-open-change", { detail: { open: true } }),
);
window.setTimeout(() => {
input?.focus();
input?.select();
runSearch().catch((error) => {
console.warn("Running search failed:", error);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
input?.focus();
input?.select();
runSearch().catch((error) => {
console.warn("Running search failed:", error);
});
});
}, 16);
});
}
export function focusSearchInput({ select = false } = {}) {

View File

@@ -1,6 +1,6 @@
[project]
name = "planet"
version = "0.40.2"
version = "0.40.5"
description = "智能星球计划 - 态势感知系统"
requires-python = ">=3.14"
dependencies = [

2
uv.lock generated
View File

@@ -475,7 +475,7 @@ wheels = [
[[package]]
name = "planet"
version = "0.40.2"
version = "0.40.5"
source = { virtual = "." }
dependencies = [
{ name = "aiofiles" },