Files
planet/frontend/public/earth/js/earth.js
rayd1o 6cb4398f3a feat(earth): Modularize 3D Earth page with ES Modules
## Changelog

### New Features
- Modularized 3D earth HTML page from single 1918-line file into ES Modules
- Split CSS into separate module files (base, info-panel, coordinates-display, legend, earth-stats)
- Split JS into separate modules (constants, utils, ui, earth, cables, controls, main)

### 3D Earth Rendering
- Use Three.js r128 (via esm.sh CDN) for color consistency with original
- Earth with 8K satellite texture and proper material settings
- Cloud layer with transparency and additive blending
- Starfield background (8000 stars)
- Latitude/longitude grid lines that rotate with Earth

### Cable System
- Load cable data from geo.json with great circle path calculation
- Support for MultiLineString and LineString geometry types
- Cable color from geo.json properties.color field
- Landing points loading from landing-point-geo.geojson

### User Interactions
- Mouse hover: highlight cable and show details
- Mouse click: lock cable with pulsing glow effect
- Click cable to pause rotation, click elsewhere to resume
- Click rotation toggle button to resume rotation and clear highlight
- Reset view with smooth animation (800ms cubic ease-out)
- Mouse wheel zoom support
- Drag to rotate Earth

### UI/UX Improvements
- Tooltip shows latitude, longitude, and altitude
- Prevent tooltip text selection during drag
- Hide tooltip during drag operation
- Blue border tooltip styling matching original
- Cursor changes to grabbing during drag
- Front-facing cable detection (only detect cables facing camera)

### Bug Fixes
- Grid lines now rotate with Earth (added as Earth child)
- Reset view button now works correctly
- Fixed camera reference in reset view
- Fixed autoRotate state management when clearing locked cable

### Original HTML
- Copied original 3dearthmult.html to public folder for reference
2026-03-11 15:54:50 +08:00

241 lines
6.5 KiB
JavaScript

// earth.js - 3D Earth creation module
import * as THREE from 'three';
import { CONFIG } from './constants.js';
import { latLonToVector3 } from './utils.js';
export let earth = null;
export let clouds = null;
export let terrain = null;
const textureLoader = new THREE.TextureLoader();
export function createEarth(scene) {
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius, 128, 128);
const material = new THREE.MeshPhongMaterial({
color: 0xffffff,
specular: 0x111111,
shininess: 10,
emissive: 0x000000,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
earth = new THREE.Mesh(geometry, material);
earth.rotation.x = 23.5 * Math.PI / 180;
scene.add(earth);
const textureUrls = [
'./8k_earth_daymap.jpg',
'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',
'https://assets.codepen.io/982762/earth_texture_2048.jpg'
];
let textureLoaded = false;
textureLoader.load(
textureUrls[0],
function(texture) {
console.log('高分辨率地球纹理加载成功');
textureLoaded = true;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.anisotropy = 16;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
material.map = texture;
material.needsUpdate = true;
document.getElementById('loading').style.display = 'none';
},
function(xhr) {
console.log('纹理加载中: ' + (xhr.loaded / xhr.total * 100) + '%');
},
function(err) {
console.log('第一个纹理加载失败,尝试第二个...');
textureLoader.load(
textureUrls[1],
function(texture) {
console.log('第二个纹理加载成功');
textureLoaded = true;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
texture.anisotropy = 16;
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.magFilter = THREE.LinearFilter;
material.map = texture;
material.needsUpdate = true;
document.getElementById('loading').style.display = 'none';
},
null,
function(err) {
console.log('所有纹理加载失败');
document.getElementById('loading').style.display = 'none';
}
);
}
);
return earth;
}
export function createClouds(scene, earthObj) {
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius + 3, 64, 64);
const material = new THREE.MeshPhongMaterial({
transparent: true,
linewidth: 2,
opacity: 0.15,
depthTest: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide
});
clouds = new THREE.Mesh(geometry, material);
earthObj.add(clouds);
textureLoader.load(
'https://threejs.org/examples/textures/planets/earth_clouds_1024.png',
function(texture) {
material.map = texture;
material.needsUpdate = true;
},
undefined,
function(err) {
console.log('云层纹理加载失败');
}
);
return clouds;
}
export function createTerrain(scene, earthObj, simplex) {
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius, 128, 128);
const positionAttribute = geometry.getAttribute('position');
for (let i = 0; i < positionAttribute.count; i++) {
const x = positionAttribute.getX(i);
const y = positionAttribute.getY(i);
const z = positionAttribute.getZ(i);
const noise = simplex(x / 20, y / 20, z / 20);
const height = 1 + noise * 0.02;
positionAttribute.setXYZ(i, x * height, y * height, z * height);
}
geometry.computeVertexNormals();
const material = new THREE.MeshPhongMaterial({
color: 0x00aa00,
flatShading: true,
transparent: true,
opacity: 0.7
});
terrain = new THREE.Mesh(geometry, material);
terrain.visible = false;
earthObj.add(terrain);
return terrain;
}
export function toggleTerrain(visible) {
if (terrain) {
terrain.visible = visible;
}
}
export function createStars(scene) {
const starGeometry = new THREE.BufferGeometry();
const starCount = 8000;
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i += 3) {
const r = 800 + Math.random() * 200;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
starPositions[i] = r * Math.sin(phi) * Math.cos(theta);
starPositions[i + 1] = r * Math.sin(phi) * Math.sin(theta);
starPositions[i + 2] = r * Math.cos(phi);
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
transparent: true,
blending: THREE.AdditiveBlending
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
return stars;
}
let latitudeLines = [];
let longitudeLines = [];
export function createGridLines(scene, earthObj) {
latitudeLines.forEach(line => scene.remove(line));
longitudeLines.forEach(line => scene.remove(line));
latitudeLines = [];
longitudeLines = [];
const earthRadius = 100.1;
const gridMaterial = new THREE.LineBasicMaterial({
color: 0x44aaff,
transparent: true,
opacity: 0.2,
linewidth: 1
});
for (let lat = -75; lat <= 75; lat += 15) {
const points = [];
for (let lon = -180; lon <= 180; lon += 5) {
const point = latLonToVector3(lat, lon, earthRadius);
points.push(point);
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, gridMaterial);
line.userData = { type: 'latitude', value: lat };
earthObj.add(line);
latitudeLines.push(line);
}
for (let lon = -180; lon <= 180; lon += 30) {
const points = [];
for (let lat = -90; lat <= 90; lat += 5) {
const point = latLonToVector3(lat, lon, earthRadius);
points.push(point);
}
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, gridMaterial);
line.userData = { type: 'longitude', value: lon };
earthObj.add(line);
longitudeLines.push(line);
}
}
export function getEarth() {
return earth;
}
export function getClouds() {
return clouds;
}