release: bump version to 0.40.5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,16 @@ This project follows the repository versioning rule:
|
|||||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||||
- `bugfix` -> `+0.0.1`
|
- `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.39.0] — 2026-04-24
|
||||||
|
|
||||||
## [0.40.4] — 2026-04-26
|
## [0.40.4] — 2026-04-26
|
||||||
|
|||||||
@@ -16,12 +16,13 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.40.4`
|
- `dev` 当前开发分支历史推导到:`0.40.5`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
| Version | Type | Branch | Commit | Summary |
|
| Version | Type | Branch | Commit | Summary |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `0.40.5` | improvement | `dev` | `pending` | 卫星 ribbon 拖尾、Iridium 覆盖球面投影填充+外圈、搜索自动聚焦修复 |
|
||||||
| `0.40.4` | bugfix | `dev` | `pending` | 修复页面后台恢复后卫星轨迹跳变与位置错位,统一轨迹重置路径 |
|
| `0.40.4` | bugfix | `dev` | `pending` | 修复页面后台恢复后卫星轨迹跳变与位置错位,统一轨迹重置路径 |
|
||||||
| `0.40.3` | improvement | `dev` | `pending` | 卫星点云升级 ShaderMaterial,修复锁定环 depthTest 与位置漂移,新增悬停态缩放 |
|
| `0.40.3` | improvement | `dev` | `pending` | 卫星点云升级 ShaderMaterial,修复锁定环 depthTest 与位置漂移,新增悬停态缩放 |
|
||||||
| `0.40.2` | improvement | `dev` | `pending` | 卫星点大小随镜头缩放动态调整,调小默认基础尺寸 |
|
| `0.40.2` | improvement | `dev` | `pending` | 卫星点大小随镜头缩放动态调整,调小默认基础尺寸 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.40.4",
|
"version": "0.40.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1",
|
"packageManager": "bun@1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -277,9 +277,10 @@ export const CABLE_STATE = {
|
|||||||
|
|
||||||
export const SATELLITE_CONFIG = {
|
export const SATELLITE_CONFIG = {
|
||||||
maxCount: -1,
|
maxCount: -1,
|
||||||
initialLoadCount: 2400,
|
initialLoadCount: null,
|
||||||
hydrateFullAfterInitialLoad: true,
|
hydrateFullAfterInitialLoad: false,
|
||||||
trailLength: 10,
|
trailLength: 10,
|
||||||
|
trailLineWidth: 3,
|
||||||
displayAltitudeOffset: 8,
|
displayAltitudeOffset: 8,
|
||||||
frontFacingDotThreshold: 0.015,
|
frontFacingDotThreshold: 0.015,
|
||||||
overlayRenderOrder: 12,
|
overlayRenderOrder: 12,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ const SURFACE_SCALE = 1.003;
|
|||||||
const SURFACE_OFFSET = 0.72;
|
const SURFACE_OFFSET = 0.72;
|
||||||
const CLUSTER_DIAMETER_KM_APPROX = 4500;
|
const CLUSTER_DIAMETER_KM_APPROX = 4500;
|
||||||
const CLUSTER_RADIUS_KM_BASE = CLUSTER_DIAMETER_KM_APPROX / 2;
|
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) {
|
function disposeMaterial(material) {
|
||||||
if (!material) return;
|
if (!material) return;
|
||||||
@@ -19,34 +23,27 @@ function disposeMaterial(material) {
|
|||||||
function disposeObjectTree(object) {
|
function disposeObjectTree(object) {
|
||||||
if (!object) return;
|
if (!object) return;
|
||||||
object.traverse((child) => {
|
object.traverse((child) => {
|
||||||
if (child.geometry) {
|
if (child.geometry) child.geometry.dispose();
|
||||||
child.geometry.dispose();
|
if (child.material) disposeMaterial(child.material);
|
||||||
}
|
|
||||||
if (child.material) {
|
|
||||||
disposeMaterial(child.material);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createIridiumClusterMaterial() {
|
function createIridiumFillMaterial() {
|
||||||
return new THREE.ShaderMaterial({
|
return new THREE.ShaderMaterial({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
depthTest: true,
|
depthTest: true,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
polygonOffset: true,
|
|
||||||
polygonOffsetFactor: -3,
|
|
||||||
polygonOffsetUnits: -3,
|
|
||||||
blending: THREE.AdditiveBlending,
|
blending: THREE.AdditiveBlending,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
uColor: { value: new THREE.Color(0x5faeff) },
|
uColor: { value: new THREE.Color(IRIDIUM_OVERLAY_COLOR) },
|
||||||
uOpacity: { value: 0.24 },
|
uOpacity: { value: 0.55 },
|
||||||
},
|
},
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
|
attribute vec2 aUv;
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vUv = uv;
|
vUv = aUv;
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -54,12 +51,10 @@ function createIridiumClusterMaterial() {
|
|||||||
uniform vec3 uColor;
|
uniform vec3 uColor;
|
||||||
uniform float uOpacity;
|
uniform float uOpacity;
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 p = vUv * 2.0 - 1.0;
|
float r2 = dot(vUv, vUv);
|
||||||
float ellipseMetric = p.x * p.x * 0.82 + p.y * p.y * 1.06;
|
float glow = exp(-r2 * 1.4) * (1.0 - smoothstep(0.72, 1.0, r2));
|
||||||
float alpha = exp(-ellipseMetric * 1.05) * (1.0 - smoothstep(0.86, 1.24, ellipseMetric));
|
float alpha = glow * uOpacity;
|
||||||
alpha *= uOpacity;
|
|
||||||
if (alpha <= 0.001) discard;
|
if (alpha <= 0.001) discard;
|
||||||
gl_FragColor = vec4(uColor, alpha);
|
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(
|
function projectOffsetToSurface(
|
||||||
centerNormal,
|
centerNormal,
|
||||||
alongTrack,
|
alongTrack,
|
||||||
@@ -88,13 +94,55 @@ function projectOffsetToSurface(
|
|||||||
|
|
||||||
function computeClusterRadiusKm(altitudeKm) {
|
function computeClusterRadiusKm(altitudeKm) {
|
||||||
const altitudeScale = THREE.MathUtils.clamp(
|
const altitudeScale = THREE.MathUtils.clamp(
|
||||||
(Number(altitudeKm) || 780) / 780,
|
(Number(altitudeKm) || IRIDIUM_REFERENCE_ALTITUDE_KM) / IRIDIUM_REFERENCE_ALTITUDE_KM,
|
||||||
0.88,
|
0.88,
|
||||||
1.18,
|
1.18,
|
||||||
);
|
);
|
||||||
return CLUSTER_RADIUS_KM_BASE * altitudeScale;
|
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({
|
export function createIridiumFootprintAdapter({
|
||||||
earthObj,
|
earthObj,
|
||||||
earthRadiusWorld,
|
earthRadiusWorld,
|
||||||
@@ -105,19 +153,21 @@ export function createIridiumFootprintAdapter({
|
|||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
group.name = "iridium-footprint-overlay";
|
group.name = "iridium-footprint-overlay";
|
||||||
group.renderOrder = renderOrder;
|
group.renderOrder = renderOrder;
|
||||||
group.userData = {
|
group.userData = { earthRadiusWorld, fill: null, outerRing: null };
|
||||||
earthRadiusWorld,
|
|
||||||
clusterGlow: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const clusterGlow = new THREE.Mesh(
|
const fill = new THREE.Mesh(buildFillGeometry(), createIridiumFillMaterial());
|
||||||
new THREE.CircleGeometry(1, 72),
|
fill.name = "iridium-cluster-fill";
|
||||||
createIridiumClusterMaterial(),
|
fill.renderOrder = renderOrder;
|
||||||
);
|
fill.frustumCulled = false;
|
||||||
clusterGlow.name = "iridium-cluster-glow";
|
group.add(fill);
|
||||||
clusterGlow.renderOrder = renderOrder - 1;
|
group.userData.fill = fill;
|
||||||
group.add(clusterGlow);
|
|
||||||
group.userData.clusterGlow = clusterGlow;
|
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);
|
earthObj.add(group);
|
||||||
return group;
|
return group;
|
||||||
@@ -129,30 +179,65 @@ export function updateIridiumFootprintAdapter(
|
|||||||
) {
|
) {
|
||||||
if (!group || !position || !alongTrack || !crossTrack) return;
|
if (!group || !position || !alongTrack || !crossTrack) return;
|
||||||
|
|
||||||
const earthRadiusWorld =
|
const earthRadiusWorld = group.userData?.earthRadiusWorld || EARTH_RADIUS_KM;
|
||||||
group.userData?.earthRadiusWorld || EARTH_RADIUS_KM;
|
|
||||||
const centerNormal = position.clone().normalize();
|
const centerNormal = position.clone().normalize();
|
||||||
const clusterRadiusKm = computeClusterRadiusKm(altitudeKm);
|
const clusterRadiusKm = computeClusterRadiusKm(altitudeKm);
|
||||||
const clusterGlow = group.userData?.clusterGlow || null;
|
const alongRadiusKm = clusterRadiusKm * 1.18;
|
||||||
const worldUnitsPerKm = earthRadiusWorld / EARTH_RADIUS_KM;
|
const crossRadiusKm = clusterRadiusKm * 0.96;
|
||||||
|
|
||||||
if (clusterGlow) {
|
const fill = group.userData?.fill;
|
||||||
const clusterCenter = projectOffsetToSurface(
|
if (fill) {
|
||||||
centerNormal,
|
const posAttr = fill.geometry.attributes.position;
|
||||||
alongTrack,
|
const uvAttr = fill.geometry.attributes.aUv;
|
||||||
crossTrack,
|
|
||||||
0,
|
// Center vertex
|
||||||
0,
|
const center = projectOffsetToSurface(
|
||||||
earthRadiusWorld,
|
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,
|
|
||||||
);
|
);
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,61 @@ const SATELLITE_CONSTELLATION_LABELS = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const TRAIL_LENGTH = SATELLITE_CONFIG.trailLength;
|
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 DOT_TEXTURE_SIZE = 32;
|
||||||
const POSITION_UPDATE_INTERVAL_MS = 250;
|
const POSITION_UPDATE_INTERVAL_MS = 250;
|
||||||
const BACKGROUND_TRAIL_RESET_DELTA_MS = 2000;
|
const BACKGROUND_TRAIL_RESET_DELTA_MS = 2000;
|
||||||
@@ -520,16 +575,32 @@ export function createSatellites(scene, earthObj) {
|
|||||||
earthObj.add(satelliteBackdropPoints);
|
earthObj.add(satelliteBackdropPoints);
|
||||||
earthObj.add(satellitePoints);
|
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({
|
const trailResolution = new THREE.Vector2(window.innerWidth, window.innerHeight);
|
||||||
vertexColors: true,
|
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,
|
transparent: true,
|
||||||
opacity: 0.3,
|
|
||||||
blending: THREE.AdditiveBlending,
|
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.visible = false;
|
||||||
satelliteTrails.userData = { type: "satelliteTrails" };
|
satelliteTrails.userData = { type: "satelliteTrails" };
|
||||||
earthObj.add(satelliteTrails);
|
earthObj.add(satelliteTrails);
|
||||||
@@ -568,15 +639,12 @@ function resetSatelliteTrailState() {
|
|||||||
function clearSatelliteTrailGeometry() {
|
function clearSatelliteTrailGeometry() {
|
||||||
if (!satelliteTrails) return;
|
if (!satelliteTrails) return;
|
||||||
|
|
||||||
const trailPositionAttr = satelliteTrails.geometry.attributes.position;
|
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
|
||||||
const trailColorAttr = satelliteTrails.geometry.attributes.color;
|
const attr = satelliteTrails.geometry.attributes[name];
|
||||||
if (trailPositionAttr?.array) {
|
if (attr?.array) {
|
||||||
trailPositionAttr.array.fill(0);
|
attr.array.fill(0);
|
||||||
trailPositionAttr.needsUpdate = true;
|
attr.needsUpdate = true;
|
||||||
}
|
}
|
||||||
if (trailColorAttr?.array) {
|
|
||||||
trailColorAttr.array.fill(0);
|
|
||||||
trailColorAttr.needsUpdate = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,10 +667,14 @@ function ensureSatelliteCapacity(count) {
|
|||||||
const previousPointAlphas = satellitePoints.geometry.attributes.alpha?.array || null;
|
const previousPointAlphas = satellitePoints.geometry.attributes.alpha?.array || null;
|
||||||
const previousBackdropAlphas =
|
const previousBackdropAlphas =
|
||||||
satelliteBackdropPoints.geometry.attributes.alpha?.array || null;
|
satelliteBackdropPoints.geometry.attributes.alpha?.array || null;
|
||||||
const previousTrailPositions =
|
const previousInstanceStarts =
|
||||||
satelliteTrails.geometry.attributes.position?.array || null;
|
satelliteTrails.geometry.attributes.instanceStart?.array || null;
|
||||||
const previousTrailColors =
|
const previousInstanceEnds =
|
||||||
satelliteTrails.geometry.attributes.color?.array || null;
|
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 previousSatellitePositions = satellitePositions;
|
||||||
const previousCapacity = satelliteCapacity;
|
const previousCapacity = satelliteCapacity;
|
||||||
|
|
||||||
@@ -668,32 +740,48 @@ function ensureSatelliteCapacity(count) {
|
|||||||
);
|
);
|
||||||
satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity));
|
satellitePoints.geometry.setDrawRange(0, Math.min(previousCapacity, nextCapacity));
|
||||||
|
|
||||||
const trailPositions = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
|
const segCount = nextCapacity * (TRAIL_LENGTH - 1);
|
||||||
const trailColors = new Float32Array(nextCapacity * TRAIL_LENGTH * 3);
|
const instanceStarts = new Float32Array(segCount * 3);
|
||||||
if (previousTrailPositions) {
|
const instanceEnds = new Float32Array(segCount * 3);
|
||||||
trailPositions.set(
|
const instanceColorStarts = new Float32Array(segCount * 3);
|
||||||
previousTrailPositions.subarray(
|
const instanceColorEnds = new Float32Array(segCount * 3);
|
||||||
0,
|
if (previousInstanceStarts) {
|
||||||
Math.min(previousTrailPositions.length, trailPositions.length),
|
instanceStarts.set(
|
||||||
),
|
previousInstanceStarts.subarray(0, Math.min(previousInstanceStarts.length, instanceStarts.length)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (previousTrailColors) {
|
if (previousInstanceEnds) {
|
||||||
trailColors.set(
|
instanceEnds.set(
|
||||||
previousTrailColors.subarray(
|
previousInstanceEnds.subarray(0, Math.min(previousInstanceEnds.length, instanceEnds.length)),
|
||||||
0,
|
);
|
||||||
Math.min(previousTrailColors.length, trailColors.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(
|
satelliteTrails.geometry.setAttribute(
|
||||||
"position",
|
"instanceStart",
|
||||||
new THREE.BufferAttribute(trailPositions, 3),
|
new THREE.InstancedBufferAttribute(instanceStarts, 3),
|
||||||
);
|
);
|
||||||
satelliteTrails.geometry.setAttribute(
|
satelliteTrails.geometry.setAttribute(
|
||||||
"color",
|
"instanceEnd",
|
||||||
new THREE.BufferAttribute(trailColors, 3),
|
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) => {
|
satellitePositions = Array.from({ length: nextCapacity }, (_, index) => {
|
||||||
const previousState = previousSatellitePositions[index];
|
const previousState = previousSatellitePositions[index];
|
||||||
@@ -903,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 radius = CONFIG.earthRadius + SATELLITE_CONFIG.displayAltitudeOffset;
|
||||||
|
|
||||||
const noradId = satellite.properties?.norad_cat_id || index;
|
const noradId = satellite.properties?.norad_cat_id || index;
|
||||||
@@ -915,9 +1003,21 @@ function generateFallbackPosition(satellite, index, total) {
|
|||||||
.split("")
|
.split("")
|
||||||
.reduce((a, b) => a + b.charCodeAt(0), 0);
|
.reduce((a, b) => a + b.charCodeAt(0), 0);
|
||||||
const randomOffset = (hash % 1000) / 1000;
|
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 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 =
|
const phi =
|
||||||
(inclination * Math.PI) / 180 + ((meanAnomaly * Math.PI) / 180) * 0.1;
|
(inclination * Math.PI) / 180 + ((meanAnomaly * Math.PI) / 180) * 0.1;
|
||||||
|
|
||||||
@@ -946,6 +1046,7 @@ export async function loadSatellites(options = {}) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
satelliteData = data.features || [];
|
satelliteData = data.features || [];
|
||||||
satelliteSatrecCache = new Map();
|
satelliteSatrecCache = new Map();
|
||||||
|
resetSatelliteTrailState();
|
||||||
ensureSatelliteCapacity(satelliteData.length);
|
ensureSatelliteCapacity(satelliteData.length);
|
||||||
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
|
positionUpdateAccumulator = POSITION_UPDATE_INTERVAL_MS;
|
||||||
return {
|
return {
|
||||||
@@ -958,7 +1059,9 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
|
if (!satellitePoints || !satelliteBackdropPoints || satelliteData.length === 0) return;
|
||||||
|
|
||||||
const shouldUpdateTrails =
|
const shouldUpdateTrails =
|
||||||
showSatellites || showTrails || lockedSatelliteIndex !== null;
|
showSatellites ||
|
||||||
|
showTrails ||
|
||||||
|
lockedSatelliteIndex !== null;
|
||||||
const shouldResetTrails =
|
const shouldResetTrails =
|
||||||
options.resetTrails ||
|
options.resetTrails ||
|
||||||
(!force && deltaTime >= BACKGROUND_TRAIL_RESET_DELTA_MS);
|
(!force && deltaTime >= BACKGROUND_TRAIL_RESET_DELTA_MS);
|
||||||
@@ -988,10 +1091,13 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
const colors = satellitePoints.geometry.attributes.color.array;
|
const colors = satellitePoints.geometry.attributes.color.array;
|
||||||
const pointAlphas = satellitePoints.geometry.attributes.alpha.array;
|
const pointAlphas = satellitePoints.geometry.attributes.alpha.array;
|
||||||
const backdropAlphas = satelliteBackdropPoints.geometry.attributes.alpha.array;
|
const backdropAlphas = satelliteBackdropPoints.geometry.attributes.alpha.array;
|
||||||
const trailPositions = satelliteTrails.geometry.attributes.position.array;
|
const instanceStarts = satelliteTrails.geometry.attributes.instanceStart.array;
|
||||||
const trailColors = satelliteTrails.geometry.attributes.color.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 baseTime = new Date(Date.now() + elapsedMs);
|
||||||
const count = Math.min(satelliteData.length, satelliteCapacity);
|
const count = Math.min(satelliteData.length, satelliteCapacity);
|
||||||
|
let trailSegmentCount = 0;
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const satellite = satelliteData[i];
|
const satellite = satelliteData[i];
|
||||||
@@ -1003,16 +1109,30 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
|
|
||||||
let pos = computeSatellitePosition(satellite, adjustedTime);
|
let pos = computeSatellitePosition(satellite, adjustedTime);
|
||||||
if (!pos) {
|
if (!pos) {
|
||||||
pos = generateFallbackPosition(satellite, i, count);
|
pos = generateFallbackPosition(satellite, i, count, adjustedTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
satellitePositions[i].current.copy(pos);
|
satellitePositions[i].current.copy(pos);
|
||||||
|
|
||||||
if (shouldUpdateTrails && i !== lockedSatelliteIndex) {
|
if (shouldUpdateTrails) {
|
||||||
const satPos = satellitePositions[i];
|
const satPos = satellitePositions[i];
|
||||||
satPos.trail[satPos.trailIndex] = pos.clone();
|
if (satPos.trailCount === 0 && TRAIL_LENGTH > 1) {
|
||||||
satPos.trailIndex = (satPos.trailIndex + 1) % TRAIL_LENGTH;
|
for (let k = 0; k < TRAIL_LENGTH; k++) {
|
||||||
if (satPos.trailCount < TRAIL_LENGTH) satPos.trailCount++;
|
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;
|
positions[i * 3] = pos.x;
|
||||||
@@ -1040,32 +1160,59 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
backdropAlphas[i] = pointAlpha;
|
backdropAlphas[i] = pointAlpha;
|
||||||
|
|
||||||
const satPosition = satellitePositions[i];
|
const satPosition = satellitePositions[i];
|
||||||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
const tc = satPosition.trailCount;
|
||||||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
let hasVisibleTrail = false;
|
||||||
|
for (let j = 0; j < TRAIL_LENGTH - 1; j++) {
|
||||||
if (j < satPosition.trailCount) {
|
if (j + 1 < tc) {
|
||||||
const idx =
|
const idxA =
|
||||||
(satPosition.trailIndex - satPosition.trailCount + j + TRAIL_LENGTH) %
|
(satPosition.trailIndex - tc + j + TRAIL_LENGTH) % TRAIL_LENGTH;
|
||||||
TRAIL_LENGTH;
|
const idxB =
|
||||||
const trailPoint = satPosition.trail[idx];
|
(satPosition.trailIndex - tc + j + 1 + TRAIL_LENGTH) % TRAIL_LENGTH;
|
||||||
if (trailPoint) {
|
const ptA = satPosition.trail[idxA];
|
||||||
trailPositions[trailIdx] = trailPoint.x;
|
const ptB = satPosition.trail[idxB];
|
||||||
trailPositions[trailIdx + 1] = trailPoint.y;
|
if (ptA && ptB && ptA.distanceToSquared(ptB) > 1e-8) {
|
||||||
trailPositions[trailIdx + 2] = trailPoint.z;
|
const base = trailSegmentCount * 3;
|
||||||
const alpha = (j + 1) / satPosition.trailCount;
|
instanceStarts[base] = ptA.x;
|
||||||
trailColors[trailIdx] = r * alpha * trailBrightness;
|
instanceStarts[base + 1] = ptA.y;
|
||||||
trailColors[trailIdx + 1] = g * alpha * trailBrightness;
|
instanceStarts[base + 2] = ptA.z;
|
||||||
trailColors[trailIdx + 2] = b * alpha * trailBrightness;
|
instanceEnds[base] = ptB.x;
|
||||||
continue;
|
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;
|
if (!hasVisibleTrail) {
|
||||||
trailPositions[trailIdx + 1] = pos.y;
|
const base = trailSegmentCount * 3;
|
||||||
trailPositions[trailIdx + 2] = pos.z;
|
const dist = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
|
||||||
trailColors[trailIdx] = 0;
|
const nx = pos.x / dist;
|
||||||
trailColors[trailIdx + 1] = 0;
|
const ny = pos.y / dist;
|
||||||
trailColors[trailIdx + 2] = 0;
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,12 +1226,23 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
pointAlphas[i] = 0;
|
pointAlphas[i] = 0;
|
||||||
backdropAlphas[i] = 0;
|
backdropAlphas[i] = 0;
|
||||||
|
|
||||||
for (let j = 0; j < TRAIL_LENGTH; j++) {
|
}
|
||||||
const trailIdx = (i * TRAIL_LENGTH + j) * 3;
|
|
||||||
trailPositions[trailIdx] = 0;
|
const trailArrayLength = instanceStarts.length / 3;
|
||||||
trailPositions[trailIdx + 1] = 0;
|
for (let i = trailSegmentCount; i < trailArrayLength; i++) {
|
||||||
trailPositions[trailIdx + 2] = 0;
|
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.position.needsUpdate = true;
|
||||||
@@ -1095,8 +1253,10 @@ export function updateSatellitePositions(deltaTime = 0, force = false, options =
|
|||||||
satelliteBackdropPoints.geometry.attributes.alpha.needsUpdate = true;
|
satelliteBackdropPoints.geometry.attributes.alpha.needsUpdate = true;
|
||||||
satelliteBackdropPoints.geometry.setDrawRange(0, count);
|
satelliteBackdropPoints.geometry.setDrawRange(0, count);
|
||||||
|
|
||||||
satelliteTrails.geometry.attributes.position.needsUpdate = true;
|
for (const name of TRAIL_INSTANCE_ATTRIBUTE_NAMES) {
|
||||||
satelliteTrails.geometry.attributes.color.needsUpdate = true;
|
satelliteTrails.geometry.attributes[name].needsUpdate = true;
|
||||||
|
}
|
||||||
|
satelliteTrails.geometry.instanceCount = trailSegmentCount;
|
||||||
|
|
||||||
// Keep the hover ring synced with the propagated satellite position even
|
// Keep the hover ring synced with the propagated satellite position even
|
||||||
// when the pointer stays still and no new hover event is emitted.
|
// when the pointer stays still and no new hover event is emitted.
|
||||||
@@ -2044,24 +2204,8 @@ function updateLockedMarkerVisual(isHovered) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRelatedSatelliteSprite(position, color = "#7dd3fc") {
|
function createRelatedSatelliteSprite(position) {
|
||||||
if (!earthObjRef) return null;
|
return createRingSprite(position, false);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showHoverRing(position, isLocked = false) {
|
export function showHoverRing(position, isLocked = false) {
|
||||||
@@ -2259,7 +2403,7 @@ export function highlightRelatedSatellites(indices, color = "#7dd3fc") {
|
|||||||
indices.forEach((index) => {
|
indices.forEach((index) => {
|
||||||
const pos = satellitePositions?.[index]?.current;
|
const pos = satellitePositions?.[index]?.current;
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
const sprite = createRelatedSatelliteSprite(pos, color);
|
const sprite = createRelatedSatelliteSprite(pos);
|
||||||
if (!sprite) return;
|
if (!sprite) return;
|
||||||
relatedSatelliteSprites.push({ index, sprite, color });
|
relatedSatelliteSprites.push({ index, sprite, color });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -242,13 +242,15 @@ export function openSearchPanel() {
|
|||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("earth:search-open-change", { detail: { open: true } }),
|
new CustomEvent("earth:search-open-change", { detail: { open: true } }),
|
||||||
);
|
);
|
||||||
window.setTimeout(() => {
|
requestAnimationFrame(() => {
|
||||||
input?.focus();
|
requestAnimationFrame(() => {
|
||||||
input?.select();
|
input?.focus();
|
||||||
runSearch().catch((error) => {
|
input?.select();
|
||||||
console.warn("Running search failed:", error);
|
runSearch().catch((error) => {
|
||||||
|
console.warn("Running search failed:", error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, 16);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusSearchInput({ select = false } = {}) {
|
export function focusSearchInput({ select = false } = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.40.4"
|
version = "0.40.5"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Reference in New Issue
Block a user