Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
199
vendor/ruvector/examples/rvf/dashboard/src/three/AtlasGraph.ts
vendored
Normal file
199
vendor/ruvector/examples/rvf/dashboard/src/three/AtlasGraph.ts
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
domain: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
const DOMAIN_COLORS: Record<string, THREE.Color> = {
|
||||
transit: new THREE.Color(0x00E5FF),
|
||||
flare: new THREE.Color(0xFF4D4D),
|
||||
rotation: new THREE.Color(0x2ECC71),
|
||||
eclipse: new THREE.Color(0x9944ff),
|
||||
variability: new THREE.Color(0xFFB020),
|
||||
};
|
||||
|
||||
const DEFAULT_COLOR = new THREE.Color(0x8B949E);
|
||||
|
||||
function colorForDomain(domain: string): THREE.Color {
|
||||
return DOMAIN_COLORS[domain] ?? DEFAULT_COLOR;
|
||||
}
|
||||
|
||||
export class AtlasGraph {
|
||||
private nodesMesh: THREE.InstancedMesh | null = null;
|
||||
private edgesLine: THREE.LineSegments | null = null;
|
||||
private glowPoints: THREE.Points | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private nodeMap: Map<string, number> = new Map();
|
||||
|
||||
constructor(scene: THREE.Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
setNodes(nodes: GraphNode[]): void {
|
||||
this.disposeNodes();
|
||||
|
||||
// Star-like nodes using InstancedMesh with emissive material
|
||||
const geometry = new THREE.SphereGeometry(0.12, 8, 6);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
vertexColors: false,
|
||||
emissiveIntensity: 0.8,
|
||||
roughness: 0.3,
|
||||
metalness: 0.1,
|
||||
});
|
||||
const mesh = new THREE.InstancedMesh(geometry, material, nodes.length);
|
||||
|
||||
const dummy = new THREE.Object3D();
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
this.nodeMap.set(node.id, i);
|
||||
|
||||
dummy.position.set(node.x, node.y, node.z);
|
||||
const scale = 0.3 + node.weight * 0.7;
|
||||
dummy.scale.set(scale, scale, scale);
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
color.copy(colorForDomain(node.domain));
|
||||
mesh.setColorAt(i, color);
|
||||
}
|
||||
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
||||
|
||||
this.nodesMesh = mesh;
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Additive glow halo points around each node
|
||||
const glowPositions = new Float32Array(nodes.length * 3);
|
||||
const glowColors = new Float32Array(nodes.length * 3);
|
||||
const glowSizes = new Float32Array(nodes.length);
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
glowPositions[i * 3] = node.x;
|
||||
glowPositions[i * 3 + 1] = node.y;
|
||||
glowPositions[i * 3 + 2] = node.z;
|
||||
|
||||
const c = colorForDomain(node.domain);
|
||||
glowColors[i * 3] = c.r;
|
||||
glowColors[i * 3 + 1] = c.g;
|
||||
glowColors[i * 3 + 2] = c.b;
|
||||
|
||||
glowSizes[i] = 0.8 + node.weight * 1.5;
|
||||
}
|
||||
|
||||
const glowGeo = new THREE.BufferGeometry();
|
||||
glowGeo.setAttribute('position', new THREE.Float32BufferAttribute(glowPositions, 3));
|
||||
glowGeo.setAttribute('color', new THREE.Float32BufferAttribute(glowColors, 3));
|
||||
|
||||
const glowMat = new THREE.PointsMaterial({
|
||||
size: 1.2,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
});
|
||||
|
||||
this.glowPoints = new THREE.Points(glowGeo, glowMat);
|
||||
this.scene.add(this.glowPoints);
|
||||
}
|
||||
|
||||
setEdges(edges: GraphEdge[], nodes: GraphNode[]): void {
|
||||
this.disposeEdges();
|
||||
|
||||
const positions: number[] = [];
|
||||
const colors: number[] = [];
|
||||
|
||||
const nodeById = new Map<string, GraphNode>();
|
||||
for (const n of nodes) nodeById.set(n.id, n);
|
||||
|
||||
for (const edge of edges) {
|
||||
const src = nodeById.get(edge.source);
|
||||
const tgt = nodeById.get(edge.target);
|
||||
if (!src || !tgt) continue;
|
||||
|
||||
positions.push(src.x, src.y, src.z);
|
||||
positions.push(tgt.x, tgt.y, tgt.z);
|
||||
|
||||
// Cyan glow edges with weight-based opacity
|
||||
const alpha = Math.max(0.05, Math.min(0.6, edge.weight * 0.5));
|
||||
colors.push(0.0, 0.9, 1.0, alpha);
|
||||
colors.push(0.0, 0.9, 1.0, alpha);
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4));
|
||||
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
depthWrite: false,
|
||||
blending: THREE.AdditiveBlending,
|
||||
});
|
||||
|
||||
this.edgesLine = new THREE.LineSegments(geometry, material);
|
||||
this.scene.add(this.edgesLine);
|
||||
}
|
||||
|
||||
getNodeIndex(id: string): number | undefined {
|
||||
return this.nodeMap.get(id);
|
||||
}
|
||||
|
||||
/** Animate node glow pulse (0-1 range). */
|
||||
setPulse(intensity: number): void {
|
||||
if (this.glowPoints) {
|
||||
(this.glowPoints.material as THREE.PointsMaterial).opacity = 0.15 + intensity * 0.15;
|
||||
}
|
||||
if (this.nodesMesh) {
|
||||
const mat = this.nodesMesh.material as THREE.MeshStandardMaterial;
|
||||
mat.emissiveIntensity = 0.5 + intensity * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeNodes(): void {
|
||||
if (this.nodesMesh) {
|
||||
this.scene.remove(this.nodesMesh);
|
||||
this.nodesMesh.geometry.dispose();
|
||||
(this.nodesMesh.material as THREE.Material).dispose();
|
||||
this.nodesMesh = null;
|
||||
}
|
||||
if (this.glowPoints) {
|
||||
this.scene.remove(this.glowPoints);
|
||||
this.glowPoints.geometry.dispose();
|
||||
(this.glowPoints.material as THREE.Material).dispose();
|
||||
this.glowPoints = null;
|
||||
}
|
||||
this.nodeMap.clear();
|
||||
}
|
||||
|
||||
private disposeEdges(): void {
|
||||
if (this.edgesLine) {
|
||||
this.scene.remove(this.edgesLine);
|
||||
this.edgesLine.geometry.dispose();
|
||||
(this.edgesLine.material as THREE.Material).dispose();
|
||||
this.edgesLine = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposeNodes();
|
||||
this.disposeEdges();
|
||||
}
|
||||
}
|
||||
248
vendor/ruvector/examples/rvf/dashboard/src/three/CoherenceSurface.ts
vendored
Normal file
248
vendor/ruvector/examples/rvf/dashboard/src/three/CoherenceSurface.ts
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class CoherenceSurface {
|
||||
private mesh: THREE.Mesh | null = null;
|
||||
private wireframe: THREE.LineSegments | null = null;
|
||||
private contourLines: THREE.Group | null = null;
|
||||
private gridLabels: THREE.Group | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private gridWidth: number;
|
||||
private gridHeight: number;
|
||||
|
||||
constructor(scene: THREE.Scene, gridWidth = 64, gridHeight = 64) {
|
||||
this.scene = scene;
|
||||
this.gridWidth = gridWidth;
|
||||
this.gridHeight = gridHeight;
|
||||
this.createMesh();
|
||||
this.createGridLabels();
|
||||
}
|
||||
|
||||
private createMesh(): void {
|
||||
const geometry = new THREE.PlaneGeometry(
|
||||
10, 10,
|
||||
this.gridWidth - 1,
|
||||
this.gridHeight - 1,
|
||||
);
|
||||
|
||||
const vertexCount = geometry.attributes.position.count;
|
||||
const colors = new Float32Array(vertexCount * 3);
|
||||
for (let i = 0; i < vertexCount; i++) {
|
||||
colors[i * 3] = 0.0;
|
||||
colors[i * 3 + 1] = 0.3;
|
||||
colors[i * 3 + 2] = 0.5;
|
||||
}
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
vertexColors: true,
|
||||
side: THREE.DoubleSide,
|
||||
shininess: 40,
|
||||
specular: new THREE.Color(0x112233),
|
||||
flatShading: false,
|
||||
});
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
this.mesh.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.mesh);
|
||||
|
||||
// Subtle grid overlay
|
||||
const wireGeo = new THREE.WireframeGeometry(geometry);
|
||||
const wireMat = new THREE.LineBasicMaterial({
|
||||
color: 0x1C2333,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
});
|
||||
this.wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||||
this.wireframe.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.wireframe);
|
||||
}
|
||||
|
||||
private createGridLabels(): void {
|
||||
this.gridLabels = new THREE.Group();
|
||||
|
||||
// Base grid plane at y=0 with faint lines
|
||||
const gridHelper = new THREE.GridHelper(10, 8, 0x1C2333, 0x131A22);
|
||||
gridHelper.position.y = -0.01;
|
||||
this.gridLabels.add(gridHelper);
|
||||
|
||||
// Axis lines
|
||||
const axisMat = new THREE.LineBasicMaterial({ color: 0x2A3444, transparent: true, opacity: 0.5 });
|
||||
const xAxisGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(-5.5, 0, 5.5),
|
||||
new THREE.Vector3(5.5, 0, 5.5),
|
||||
]);
|
||||
this.gridLabels.add(new THREE.Line(xAxisGeo, axisMat));
|
||||
|
||||
const zAxisGeo = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(-5.5, 0, 5.5),
|
||||
new THREE.Vector3(-5.5, 0, -5.5),
|
||||
]);
|
||||
this.gridLabels.add(new THREE.Line(zAxisGeo, axisMat));
|
||||
|
||||
this.scene.add(this.gridLabels);
|
||||
}
|
||||
|
||||
/** Map coherence value [0,1] to a clear multi-stop color ramp. */
|
||||
private valueToColor(v: number, color: THREE.Color): void {
|
||||
// 1.0 = deep stable blue, 0.85 = cyan, 0.75 = yellow warning, <0.7 = red critical
|
||||
if (v > 0.85) {
|
||||
// Blue -> Cyan (stable zone)
|
||||
const t = (v - 0.85) / 0.15;
|
||||
color.setRGB(0.0, 0.4 + t * 0.1, 0.6 + t * 0.4);
|
||||
} else if (v > 0.75) {
|
||||
// Cyan -> Yellow (transition)
|
||||
const t = (v - 0.75) / 0.1;
|
||||
color.setRGB(1.0 - t * 1.0, 0.7 + t * 0.2, t * 0.6);
|
||||
} else if (v > 0.65) {
|
||||
// Yellow -> Orange (warning)
|
||||
const t = (v - 0.65) / 0.1;
|
||||
color.setRGB(1.0, 0.5 + t * 0.2, t * 0.1);
|
||||
} else {
|
||||
// Orange -> Red (critical)
|
||||
const t = Math.max(0, v / 0.65);
|
||||
color.setRGB(0.9 + t * 0.1, 0.15 + t * 0.35, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
setValues(values: number[]): void {
|
||||
if (!this.mesh) return;
|
||||
|
||||
const geometry = this.mesh.geometry;
|
||||
const colorAttr = geometry.attributes.color;
|
||||
const posAttr = geometry.attributes.position;
|
||||
const count = Math.min(values.length, colorAttr.count);
|
||||
|
||||
const color = new THREE.Color();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const v = Math.max(0, Math.min(1, values[i]));
|
||||
this.valueToColor(v, color);
|
||||
colorAttr.setXYZ(i, color.r, color.g, color.b);
|
||||
|
||||
// Elevation: higher coherence = flat, lower = raised (shows "pressure")
|
||||
const elevation = (1 - v) * 2.5;
|
||||
posAttr.setZ(i, elevation);
|
||||
}
|
||||
|
||||
colorAttr.needsUpdate = true;
|
||||
posAttr.needsUpdate = true;
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
this.updateWireframe(geometry);
|
||||
this.updateContours(values);
|
||||
}
|
||||
|
||||
private updateWireframe(geometry: THREE.PlaneGeometry): void {
|
||||
if (this.wireframe) {
|
||||
this.scene.remove(this.wireframe);
|
||||
this.wireframe.geometry.dispose();
|
||||
(this.wireframe.material as THREE.Material).dispose();
|
||||
}
|
||||
|
||||
const wireGeo = new THREE.WireframeGeometry(geometry);
|
||||
const wireMat = new THREE.LineBasicMaterial({
|
||||
color: 0x1C2333,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
});
|
||||
this.wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||||
this.wireframe.rotation.x = -Math.PI / 2;
|
||||
this.scene.add(this.wireframe);
|
||||
}
|
||||
|
||||
/** Draw contour rings at threshold boundaries (0.8 warning, 0.7 critical). */
|
||||
private updateContours(values: number[]): void {
|
||||
if (this.contourLines) {
|
||||
this.scene.remove(this.contourLines);
|
||||
this.contourLines.traverse((obj) => {
|
||||
if (obj instanceof THREE.Line) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.contourLines = new THREE.Group();
|
||||
const gw = this.gridWidth;
|
||||
const gh = this.gridHeight;
|
||||
const halfW = 5;
|
||||
|
||||
const thresholds = [
|
||||
{ level: 0.80, color: 0xFFB020, opacity: 0.6 }, // warning
|
||||
{ level: 0.70, color: 0xFF4D4D, opacity: 0.7 }, // critical
|
||||
];
|
||||
|
||||
for (const thresh of thresholds) {
|
||||
const points: THREE.Vector3[] = [];
|
||||
|
||||
for (let y = 0; y < gh - 1; y++) {
|
||||
for (let x = 0; x < gw - 1; x++) {
|
||||
const v00 = values[y * gw + x] ?? 1;
|
||||
const v10 = values[y * gw + x + 1] ?? 1;
|
||||
const v01 = values[(y + 1) * gw + x] ?? 1;
|
||||
|
||||
// Horizontal edge crossing
|
||||
if ((v00 - thresh.level) * (v10 - thresh.level) < 0) {
|
||||
const t = (thresh.level - v00) / (v10 - v00);
|
||||
const wx = -halfW + ((x + t) / (gw - 1)) * halfW * 2;
|
||||
const wz = -halfW + (y / (gh - 1)) * halfW * 2;
|
||||
const elev = (1 - thresh.level) * 2.5;
|
||||
points.push(new THREE.Vector3(wx, elev + 0.02, wz));
|
||||
}
|
||||
|
||||
// Vertical edge crossing
|
||||
if ((v00 - thresh.level) * (v01 - thresh.level) < 0) {
|
||||
const t = (thresh.level - v00) / (v01 - v00);
|
||||
const wx = -halfW + (x / (gw - 1)) * halfW * 2;
|
||||
const wz = -halfW + ((y + t) / (gh - 1)) * halfW * 2;
|
||||
const elev = (1 - thresh.level) * 2.5;
|
||||
points.push(new THREE.Vector3(wx, elev + 0.02, wz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length > 1) {
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(points);
|
||||
const mat = new THREE.PointsMaterial({
|
||||
color: thresh.color,
|
||||
size: 0.08,
|
||||
transparent: true,
|
||||
opacity: thresh.opacity,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.contourLines.add(new THREE.Points(geo, mat));
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.add(this.contourLines);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.mesh) {
|
||||
this.scene.remove(this.mesh);
|
||||
this.mesh.geometry.dispose();
|
||||
(this.mesh.material as THREE.Material).dispose();
|
||||
this.mesh = null;
|
||||
}
|
||||
if (this.wireframe) {
|
||||
this.scene.remove(this.wireframe);
|
||||
this.wireframe.geometry.dispose();
|
||||
(this.wireframe.material as THREE.Material).dispose();
|
||||
this.wireframe = null;
|
||||
}
|
||||
if (this.contourLines) {
|
||||
this.scene.remove(this.contourLines);
|
||||
this.contourLines.traverse((obj) => {
|
||||
if (obj instanceof THREE.Line || obj instanceof THREE.Points) {
|
||||
obj.geometry.dispose();
|
||||
(obj.material as THREE.Material).dispose();
|
||||
}
|
||||
});
|
||||
this.contourLines = null;
|
||||
}
|
||||
if (this.gridLabels) {
|
||||
this.scene.remove(this.gridLabels);
|
||||
this.gridLabels = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
390
vendor/ruvector/examples/rvf/dashboard/src/three/DysonSphere3D.ts
vendored
Normal file
390
vendor/ruvector/examples/rvf/dashboard/src/three/DysonSphere3D.ts
vendored
Normal file
@@ -0,0 +1,390 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
/**
|
||||
* Interactive 3D Dyson sphere visualization with galactic context.
|
||||
*
|
||||
* Renders:
|
||||
* - Deep-field starfield + galactic plane
|
||||
* - Central star (emissive sphere, color from spectral type)
|
||||
* - Partial Dyson swarm shell (coverage_fraction controls opacity mask)
|
||||
* - IR waste heat glow halo
|
||||
* - Orbiting collector panels as instanced quads
|
||||
*
|
||||
* Interaction:
|
||||
* - OrbitControls: drag to rotate, scroll to zoom, right-drag to pan
|
||||
* - Speed control via setSpeed()
|
||||
* - Reset view via resetCamera()
|
||||
*/
|
||||
|
||||
export interface DysonParams {
|
||||
coverageFraction: number;
|
||||
warmTempK: number;
|
||||
spectralType: string;
|
||||
w3Excess: number;
|
||||
w4Excess: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const SPECTRAL_COLORS: Record<string, number> = {
|
||||
O: 0x9bb0ff, B: 0xaabfff, A: 0xcad7ff, F: 0xf8f7ff,
|
||||
G: 0xfff4ea, K: 0xffd2a1, M: 0xffb56c, L: 0xff8833,
|
||||
};
|
||||
|
||||
function starColor(spectralType: string): number {
|
||||
const letter = spectralType.charAt(0).toUpperCase();
|
||||
return SPECTRAL_COLORS[letter] ?? 0xffd2a1;
|
||||
}
|
||||
|
||||
function warmColor(tempK: number): THREE.Color {
|
||||
const t = Math.max(0, Math.min(1, (tempK - 100) / 400));
|
||||
return new THREE.Color().setHSL(0.02 + t * 0.06, 0.9, 0.3 + t * 0.2);
|
||||
}
|
||||
|
||||
function seededRandom(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 16807 + 0) % 2147483647;
|
||||
return (s - 1) / 2147483646;
|
||||
};
|
||||
}
|
||||
|
||||
export class DysonSphere3D {
|
||||
private scene: THREE.Scene;
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private controls: OrbitControls;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private shellMesh: THREE.Mesh | null = null;
|
||||
private glowMesh: THREE.Mesh | null = null;
|
||||
private panelInstances: THREE.InstancedMesh | null = null;
|
||||
private animId = 0;
|
||||
private time = 0;
|
||||
private speedMultiplier = 1;
|
||||
private autoRotate = true;
|
||||
private defaultCamPos = new THREE.Vector3(0, 1.5, 4);
|
||||
private bgGroup: THREE.Group | null = null;
|
||||
|
||||
constructor(private container: HTMLElement) {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x020408);
|
||||
|
||||
const w = container.clientWidth || 400;
|
||||
const h = container.clientHeight || 300;
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 2000);
|
||||
this.camera.position.set(0, 1.5, 4);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.setSize(w, h);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// OrbitControls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.08;
|
||||
this.controls.minDistance = 1;
|
||||
this.controls.maxDistance = 400;
|
||||
this.controls.enablePan = true;
|
||||
this.controls.zoomSpeed = 1.2;
|
||||
this.controls.rotateSpeed = 0.8;
|
||||
this.controls.addEventListener('start', () => { this.autoRotate = false; });
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0x222244, 0.3));
|
||||
this.buildBackground();
|
||||
}
|
||||
|
||||
// ── Public controls ──
|
||||
|
||||
setSpeed(multiplier: number): void {
|
||||
this.speedMultiplier = multiplier;
|
||||
}
|
||||
|
||||
resetCamera(): void {
|
||||
this.autoRotate = true;
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
toggleAutoRotate(): void {
|
||||
this.autoRotate = !this.autoRotate;
|
||||
}
|
||||
|
||||
getAutoRotate(): boolean {
|
||||
return this.autoRotate;
|
||||
}
|
||||
|
||||
// ── Background ──
|
||||
|
||||
private buildBackground(): void {
|
||||
this.bgGroup = new THREE.Group();
|
||||
const rand = seededRandom(77);
|
||||
|
||||
// Starfield
|
||||
const starCount = 4000;
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
const colors = new Float32Array(starCount * 3);
|
||||
const tints = [
|
||||
new THREE.Color(0xffffff), new THREE.Color(0xaaccff),
|
||||
new THREE.Color(0xfff4ea), new THREE.Color(0xffd2a1),
|
||||
new THREE.Color(0xffb56c), new THREE.Color(0xccddff),
|
||||
];
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const theta = rand() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * rand() - 1);
|
||||
const r = 300 + rand() * 500;
|
||||
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i * 3 + 2] = r * Math.cos(phi);
|
||||
const tint = tints[Math.floor(rand() * tints.length)];
|
||||
const b = 0.4 + rand() * 0.6;
|
||||
colors[i * 3] = tint.r * b;
|
||||
colors[i * 3 + 1] = tint.g * b;
|
||||
colors[i * 3 + 2] = tint.b * b;
|
||||
}
|
||||
|
||||
const sGeo = new THREE.BufferGeometry();
|
||||
sGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
sGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
this.bgGroup.add(new THREE.Points(sGeo, new THREE.PointsMaterial({
|
||||
size: 1.5, vertexColors: true, transparent: true, opacity: 0.9,
|
||||
sizeAttenuation: true, depthWrite: false,
|
||||
})));
|
||||
|
||||
// Galactic plane
|
||||
const galCount = 5000;
|
||||
const gp = new Float32Array(galCount * 3);
|
||||
const gc = new Float32Array(galCount * 3);
|
||||
for (let i = 0; i < galCount; i++) {
|
||||
const a = rand() * Math.PI * 2;
|
||||
const d = Math.pow(rand(), 0.5) * 500;
|
||||
const h = (rand() - 0.5) * (12 + d * 0.02);
|
||||
gp[i * 3] = d * Math.cos(a);
|
||||
gp[i * 3 + 1] = h;
|
||||
gp[i * 3 + 2] = d * Math.sin(a);
|
||||
const cp = 1 - Math.min(1, d / 500);
|
||||
gc[i * 3] = 0.5 + cp * 0.35;
|
||||
gc[i * 3 + 1] = 0.5 + cp * 0.25;
|
||||
gc[i * 3 + 2] = 0.6 + rand() * 0.1;
|
||||
}
|
||||
const gGeo = new THREE.BufferGeometry();
|
||||
gGeo.setAttribute('position', new THREE.BufferAttribute(gp, 3));
|
||||
gGeo.setAttribute('color', new THREE.BufferAttribute(gc, 3));
|
||||
const gal = new THREE.Points(gGeo, new THREE.PointsMaterial({
|
||||
size: 0.8, vertexColors: true, transparent: true, opacity: 0.2,
|
||||
sizeAttenuation: true, depthWrite: false,
|
||||
}));
|
||||
gal.rotation.x = Math.PI * 0.35;
|
||||
gal.rotation.z = Math.PI * 0.15;
|
||||
gal.position.set(0, 80, -180);
|
||||
this.bgGroup.add(gal);
|
||||
|
||||
// Nebulae
|
||||
const nebColors = [0x3344aa, 0xaa3355, 0x2288aa, 0x8844aa];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128; canvas.height = 128;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const grad = ctx.createRadialGradient(64, 64, 4, 64, 64, 64);
|
||||
const col = new THREE.Color(nebColors[i % nebColors.length]);
|
||||
grad.addColorStop(0, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.25)`);
|
||||
grad.addColorStop(0.4, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.06)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });
|
||||
const sp = new THREE.Sprite(mat);
|
||||
const t2 = rand() * Math.PI * 2;
|
||||
const p2 = (rand() - 0.5) * Math.PI * 0.5;
|
||||
const r2 = 200 + rand() * 350;
|
||||
sp.position.set(r2 * Math.cos(p2) * Math.cos(t2), r2 * Math.sin(p2), r2 * Math.cos(p2) * Math.sin(t2));
|
||||
sp.scale.setScalar(50 + rand() * 100);
|
||||
this.bgGroup.add(sp);
|
||||
}
|
||||
|
||||
this.scene.add(this.bgGroup);
|
||||
}
|
||||
|
||||
update(params: DysonParams): void {
|
||||
this.clearSystem();
|
||||
|
||||
const sc = starColor(params.spectralType);
|
||||
|
||||
// ── Central Star ──
|
||||
const starGeo = new THREE.SphereGeometry(0.5, 32, 32);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: sc });
|
||||
this.starMesh = new THREE.Mesh(starGeo, starMat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
const starLight = new THREE.PointLight(sc, 2, 20);
|
||||
starLight.position.set(0, 0, 0);
|
||||
this.scene.add(starLight);
|
||||
|
||||
// ── Dyson Shell ──
|
||||
const shellRadius = 1.5;
|
||||
const shellGeo = new THREE.SphereGeometry(shellRadius, 64, 64);
|
||||
const wc = warmColor(params.warmTempK);
|
||||
|
||||
const positions = shellGeo.attributes.position;
|
||||
const vertColors = new Float32Array(positions.count * 4);
|
||||
const coverage = params.coverageFraction;
|
||||
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
const x = positions.getX(i);
|
||||
const y = positions.getY(i);
|
||||
const z = positions.getZ(i);
|
||||
const theta = Math.atan2(Math.sqrt(x * x + z * z), y);
|
||||
const phi = Math.atan2(z, x);
|
||||
const pattern =
|
||||
0.5 + 0.2 * Math.sin(theta * 5 + phi * 3) +
|
||||
0.15 * Math.sin(theta * 8 - phi * 5) +
|
||||
0.15 * Math.cos(phi * 7 + theta * 2);
|
||||
const visible = pattern < coverage;
|
||||
const alpha = visible ? 0.6 + coverage * 0.3 : 0.02;
|
||||
vertColors[i * 4] = visible ? wc.r : 0.05;
|
||||
vertColors[i * 4 + 1] = visible ? wc.g : 0.05;
|
||||
vertColors[i * 4 + 2] = visible ? wc.b : 0.05;
|
||||
vertColors[i * 4 + 3] = alpha;
|
||||
}
|
||||
shellGeo.setAttribute('color', new THREE.BufferAttribute(vertColors, 4));
|
||||
|
||||
const shellMat = new THREE.MeshBasicMaterial({
|
||||
vertexColors: true, transparent: true, opacity: 0.7,
|
||||
side: THREE.DoubleSide, depthWrite: false,
|
||||
});
|
||||
this.shellMesh = new THREE.Mesh(shellGeo, shellMat);
|
||||
this.scene.add(this.shellMesh);
|
||||
|
||||
// Wireframe overlay
|
||||
const wireGeo = new THREE.SphereGeometry(shellRadius + 0.01, 24, 24);
|
||||
const wireMat = new THREE.MeshBasicMaterial({ color: wc, transparent: true, opacity: 0.08, wireframe: true });
|
||||
this.scene.add(new THREE.Mesh(wireGeo, wireMat));
|
||||
|
||||
// ── IR Glow ──
|
||||
const glowRadius = shellRadius + 0.3 + params.w4Excess * 0.1;
|
||||
const glowGeo = new THREE.SphereGeometry(glowRadius, 32, 32);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: wc, transparent: true, opacity: 0.04 + coverage * 0.06,
|
||||
side: THREE.BackSide, depthWrite: false,
|
||||
});
|
||||
this.glowMesh = new THREE.Mesh(glowGeo, glowMat);
|
||||
this.scene.add(this.glowMesh);
|
||||
|
||||
// ── Collector Panels ──
|
||||
const panelCount = Math.floor(coverage * 400);
|
||||
if (panelCount > 0) {
|
||||
const panelGeo = new THREE.PlaneGeometry(0.06, 0.06);
|
||||
const panelMat = new THREE.MeshBasicMaterial({ color: wc, transparent: true, opacity: 0.9, side: THREE.DoubleSide });
|
||||
this.panelInstances = new THREE.InstancedMesh(panelGeo, panelMat, panelCount);
|
||||
|
||||
const dummy = new THREE.Object3D();
|
||||
for (let i = 0; i < panelCount; i++) {
|
||||
const t = i / panelCount;
|
||||
const incl = Math.acos(1 - 2 * t);
|
||||
const azim = Math.PI * (1 + Math.sqrt(5)) * i;
|
||||
const r = shellRadius + 0.02 + Math.random() * 0.05;
|
||||
dummy.position.set(
|
||||
r * Math.sin(incl) * Math.cos(azim),
|
||||
r * Math.cos(incl),
|
||||
r * Math.sin(incl) * Math.sin(azim),
|
||||
);
|
||||
dummy.lookAt(0, 0, 0);
|
||||
dummy.updateMatrix();
|
||||
this.panelInstances.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
this.panelInstances.instanceMatrix.needsUpdate = true;
|
||||
this.scene.add(this.panelInstances);
|
||||
}
|
||||
|
||||
this.defaultCamPos.set(2.5, 1.5, 3.5);
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
this.autoRotate = true;
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private clearSystem(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
const toRemove: THREE.Object3D[] = [];
|
||||
this.scene.traverse((obj) => {
|
||||
if (obj !== this.scene && obj !== this.bgGroup && obj.parent === this.scene && !(obj instanceof THREE.AmbientLight)) {
|
||||
toRemove.push(obj);
|
||||
}
|
||||
});
|
||||
for (const obj of toRemove) {
|
||||
this.scene.remove(obj);
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
}
|
||||
let hasAmbient = false;
|
||||
this.scene.traverse((o) => { if (o instanceof THREE.AmbientLight) hasAmbient = true; });
|
||||
if (!hasAmbient) this.scene.add(new THREE.AmbientLight(0x222244, 0.3));
|
||||
|
||||
this.starMesh = null;
|
||||
this.shellMesh = null;
|
||||
this.glowMesh = null;
|
||||
this.panelInstances = null;
|
||||
}
|
||||
|
||||
private animate = (): void => {
|
||||
this.animId = requestAnimationFrame(this.animate);
|
||||
this.time += 0.005 * this.speedMultiplier;
|
||||
|
||||
if (this.shellMesh) this.shellMesh.rotation.y = this.time * 0.3;
|
||||
if (this.panelInstances) this.panelInstances.rotation.y = this.time * 0.3;
|
||||
|
||||
// Auto-rotate camera
|
||||
if (this.autoRotate) {
|
||||
const camR = 4;
|
||||
this.camera.position.x = camR * Math.sin(this.time * 0.15);
|
||||
this.camera.position.z = camR * Math.cos(this.time * 0.15);
|
||||
this.camera.position.y = 1.2 + 0.3 * Math.sin(this.time * 0.1);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
}
|
||||
|
||||
if (this.starMesh) {
|
||||
const scale = 1 + 0.03 * Math.sin(this.time * 3);
|
||||
this.starMesh.scale.setScalar(scale);
|
||||
}
|
||||
|
||||
if (this.glowMesh) {
|
||||
const mat = this.glowMesh.material as THREE.MeshBasicMaterial;
|
||||
mat.opacity = 0.04 + 0.02 * Math.sin(this.time * 2);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
|
||||
resize(): void {
|
||||
const w = this.container.clientWidth || 400;
|
||||
const h = this.container.clientHeight || 300;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
this.controls.dispose();
|
||||
this.clearSystem();
|
||||
if (this.bgGroup) {
|
||||
this.scene.remove(this.bgGroup);
|
||||
this.bgGroup.traverse((obj) => {
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
});
|
||||
this.bgGroup = null;
|
||||
}
|
||||
this.renderer.dispose();
|
||||
if (this.renderer.domElement.parentElement) {
|
||||
this.renderer.domElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
204
vendor/ruvector/examples/rvf/dashboard/src/three/OrbitPreview.ts
vendored
Normal file
204
vendor/ruvector/examples/rvf/dashboard/src/three/OrbitPreview.ts
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class OrbitPreview {
|
||||
private line: THREE.Line | null = null;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private starGlow: THREE.Sprite | null = null;
|
||||
private planetMesh: THREE.Mesh | null = null;
|
||||
private hzRing: THREE.Line | null = null;
|
||||
private gridHelper: THREE.GridHelper | null = null;
|
||||
private scene: THREE.Scene;
|
||||
private orbitPoints: THREE.Vector3[] = [];
|
||||
private orbitAngle = 0;
|
||||
private orbitSpeed = 0.005;
|
||||
private paramOverlay: HTMLElement | null = null;
|
||||
private parentEl: HTMLElement | null = null;
|
||||
|
||||
constructor(scene: THREE.Scene) {
|
||||
this.scene = scene;
|
||||
this.addStar();
|
||||
this.addGrid();
|
||||
}
|
||||
|
||||
private addStar(): void {
|
||||
// Solid sphere
|
||||
const geo = new THREE.SphereGeometry(0.18, 24, 16);
|
||||
const mat = new THREE.MeshBasicMaterial({ color: 0xffdd44 });
|
||||
this.starMesh = new THREE.Mesh(geo, mat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
// Glow sprite
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const grad = ctx.createRadialGradient(32, 32, 2, 32, 32, 32);
|
||||
grad.addColorStop(0, 'rgba(255,221,68,0.6)');
|
||||
grad.addColorStop(0.4, 'rgba(255,200,50,0.15)');
|
||||
grad.addColorStop(1, 'rgba(255,200,50,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 64, 64);
|
||||
}
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true, blending: THREE.AdditiveBlending });
|
||||
this.starGlow = new THREE.Sprite(spriteMat);
|
||||
this.starGlow.scale.set(1.2, 1.2, 1);
|
||||
this.scene.add(this.starGlow);
|
||||
}
|
||||
|
||||
private addGrid(): void {
|
||||
this.gridHelper = new THREE.GridHelper(8, 8, 0x1C2333, 0x151B23);
|
||||
this.gridHelper.position.y = -0.5;
|
||||
this.scene.add(this.gridHelper);
|
||||
}
|
||||
|
||||
setOrbit(
|
||||
semiMajorAxis: number,
|
||||
eccentricity: number,
|
||||
inclination: number,
|
||||
parentElement?: HTMLElement,
|
||||
): void {
|
||||
this.disposeLine();
|
||||
this.disposePlanet();
|
||||
this.disposeHzRing();
|
||||
this.disposeOverlay();
|
||||
|
||||
const segments = 128;
|
||||
this.orbitPoints = [];
|
||||
const a = semiMajorAxis;
|
||||
const e = Math.min(Math.max(eccentricity, 0), 0.99);
|
||||
const incRad = (inclination * Math.PI) / 180;
|
||||
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const theta = (i / segments) * Math.PI * 2;
|
||||
const r = (a * (1 - e * e)) / (1 + e * Math.cos(theta));
|
||||
const x = r * Math.cos(theta);
|
||||
const z = r * Math.sin(theta) * Math.cos(incRad);
|
||||
const y = r * Math.sin(theta) * Math.sin(incRad);
|
||||
this.orbitPoints.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
|
||||
// Orbit path
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(this.orbitPoints);
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: 0x4488ff,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
});
|
||||
this.line = new THREE.Line(geometry, material);
|
||||
this.scene.add(this.line);
|
||||
|
||||
// Planet dot
|
||||
const planetGeo = new THREE.SphereGeometry(0.08, 12, 8);
|
||||
const planetMat = new THREE.MeshStandardMaterial({ color: 0x4488ff, emissive: 0x2244aa, emissiveIntensity: 0.3 });
|
||||
this.planetMesh = new THREE.Mesh(planetGeo, planetMat);
|
||||
this.planetMesh.position.copy(this.orbitPoints[0]);
|
||||
this.scene.add(this.planetMesh);
|
||||
|
||||
// Habitable zone ring (0.95-1.37 AU scaled)
|
||||
const hzInner = 0.95 * (a / 1.5);
|
||||
const hzOuter = 1.37 * (a / 1.5);
|
||||
const hzMid = (hzInner + hzOuter) / 2;
|
||||
const hzPts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 64; i++) {
|
||||
const theta = (i / 64) * Math.PI * 2;
|
||||
hzPts.push(new THREE.Vector3(hzMid * Math.cos(theta), -0.48, hzMid * Math.sin(theta)));
|
||||
}
|
||||
const hzGeo = new THREE.BufferGeometry().setFromPoints(hzPts);
|
||||
const hzMat = new THREE.LineBasicMaterial({ color: 0x2ECC71, transparent: true, opacity: 0.25 });
|
||||
this.hzRing = new THREE.Line(hzGeo, hzMat);
|
||||
this.scene.add(this.hzRing);
|
||||
|
||||
// Orbit speed based on period (faster for shorter periods)
|
||||
this.orbitSpeed = 0.003 + (1 / (a * 10)) * 0.02;
|
||||
this.orbitAngle = 0;
|
||||
|
||||
// Param overlay
|
||||
if (parentElement) {
|
||||
this.parentEl = parentElement;
|
||||
this.paramOverlay = document.createElement('div');
|
||||
this.paramOverlay.style.cssText =
|
||||
'position:absolute;bottom:8px;left:8px;' +
|
||||
'background:rgba(11,15,20,0.85);border:1px solid var(--border);border-radius:4px;' +
|
||||
'padding:6px 10px;font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);' +
|
||||
'line-height:1.6;z-index:10;pointer-events:none';
|
||||
this.paramOverlay.innerHTML =
|
||||
`<div style="color:var(--text-primary);font-weight:600;margin-bottom:2px">Orbit Parameters</div>` +
|
||||
`<div>Semi-major: <span style="color:var(--accent)">${a.toFixed(2)} AU</span></div>` +
|
||||
`<div>Eccentricity: <span style="color:var(--accent)">${e.toFixed(3)}</span></div>` +
|
||||
`<div>Inclination: <span style="color:var(--accent)">${inclination.toFixed(1)}°</span></div>` +
|
||||
`<div style="margin-top:4px;color:#2ECC71;font-size:9px">● Habitable zone</div>`;
|
||||
parentElement.appendChild(this.paramOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
/** Call each frame to animate the planet along the orbit. */
|
||||
tick(): void {
|
||||
if (!this.planetMesh || this.orbitPoints.length < 2) return;
|
||||
this.orbitAngle = (this.orbitAngle + this.orbitSpeed) % 1;
|
||||
const idx = Math.floor(this.orbitAngle * (this.orbitPoints.length - 1));
|
||||
this.planetMesh.position.copy(this.orbitPoints[idx]);
|
||||
}
|
||||
|
||||
private disposeLine(): void {
|
||||
if (this.line) {
|
||||
this.scene.remove(this.line);
|
||||
this.line.geometry.dispose();
|
||||
(this.line.material as THREE.Material).dispose();
|
||||
this.line = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposePlanet(): void {
|
||||
if (this.planetMesh) {
|
||||
this.scene.remove(this.planetMesh);
|
||||
this.planetMesh.geometry.dispose();
|
||||
(this.planetMesh.material as THREE.Material).dispose();
|
||||
this.planetMesh = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeHzRing(): void {
|
||||
if (this.hzRing) {
|
||||
this.scene.remove(this.hzRing);
|
||||
this.hzRing.geometry.dispose();
|
||||
(this.hzRing.material as THREE.Material).dispose();
|
||||
this.hzRing = null;
|
||||
}
|
||||
}
|
||||
|
||||
private disposeOverlay(): void {
|
||||
if (this.paramOverlay && this.parentEl) {
|
||||
this.parentEl.removeChild(this.paramOverlay);
|
||||
this.paramOverlay = null;
|
||||
this.parentEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposeLine();
|
||||
this.disposePlanet();
|
||||
this.disposeHzRing();
|
||||
this.disposeOverlay();
|
||||
|
||||
if (this.starMesh) {
|
||||
this.scene.remove(this.starMesh);
|
||||
this.starMesh.geometry.dispose();
|
||||
(this.starMesh.material as THREE.Material).dispose();
|
||||
this.starMesh = null;
|
||||
}
|
||||
if (this.starGlow) {
|
||||
this.scene.remove(this.starGlow);
|
||||
this.starGlow.material.map?.dispose();
|
||||
this.starGlow.material.dispose();
|
||||
this.starGlow = null;
|
||||
}
|
||||
if (this.gridHelper) {
|
||||
this.scene.remove(this.gridHelper);
|
||||
this.gridHelper.geometry.dispose();
|
||||
(this.gridHelper.material as THREE.Material).dispose();
|
||||
this.gridHelper = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
544
vendor/ruvector/examples/rvf/dashboard/src/three/PlanetSystem3D.ts
vendored
Normal file
544
vendor/ruvector/examples/rvf/dashboard/src/three/PlanetSystem3D.ts
vendored
Normal file
@@ -0,0 +1,544 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
/**
|
||||
* Interactive 3D exoplanet system visualization with galactic context.
|
||||
*
|
||||
* Renders:
|
||||
* - Deep-field starfield (4000 background stars)
|
||||
* - Milky Way galactic plane disc
|
||||
* - Distant nebula patches
|
||||
* - Central host star (color from effective temperature)
|
||||
* - Planet on animated orbital path (size from radius_earth)
|
||||
* - Habitable zone annulus (green band)
|
||||
* - Orbit ellipse line
|
||||
* - AU scale labels
|
||||
*
|
||||
* Interaction:
|
||||
* - OrbitControls: drag to rotate, scroll to zoom, right-drag to pan
|
||||
* - Speed control via setSpeed()
|
||||
* - Reset view via resetCamera()
|
||||
*/
|
||||
|
||||
export interface PlanetSystemParams {
|
||||
label: string;
|
||||
radiusEarth: number;
|
||||
semiMajorAxisAU: number;
|
||||
eqTempK: number;
|
||||
stellarTempK: number;
|
||||
stellarRadiusSolar: number;
|
||||
periodDays: number;
|
||||
hzMember: boolean;
|
||||
esiScore: number;
|
||||
transitDepth: number;
|
||||
}
|
||||
|
||||
function starColorFromTemp(teff: number): number {
|
||||
if (teff > 7500) return 0xaabfff;
|
||||
if (teff > 6000) return 0xf8f7ff;
|
||||
if (teff > 5200) return 0xfff4ea;
|
||||
if (teff > 3700) return 0xffd2a1;
|
||||
return 0xffb56c;
|
||||
}
|
||||
|
||||
function planetColor(eqTempK: number): number {
|
||||
if (eqTempK < 200) return 0x4488cc;
|
||||
if (eqTempK < 260) return 0x44aa77;
|
||||
if (eqTempK < 320) return 0x55bb55;
|
||||
if (eqTempK < 500) return 0xddaa44;
|
||||
return 0xff6644;
|
||||
}
|
||||
|
||||
/** Deterministic pseudo-random from seed. */
|
||||
function seededRandom(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 16807 + 0) % 2147483647;
|
||||
return (s - 1) / 2147483646;
|
||||
};
|
||||
}
|
||||
|
||||
export class PlanetSystem3D {
|
||||
private scene: THREE.Scene;
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private controls: OrbitControls;
|
||||
private starMesh: THREE.Mesh | null = null;
|
||||
private planetMesh: THREE.Mesh | null = null;
|
||||
private orbitLine: THREE.Line | null = null;
|
||||
private hzInnerRing: THREE.Mesh | null = null;
|
||||
private animId = 0;
|
||||
private time = 0;
|
||||
private orbitPoints: THREE.Vector3[] = [];
|
||||
private orbitSpeed = 0.003;
|
||||
private orbitAngle = 0;
|
||||
private speedMultiplier = 1;
|
||||
private autoRotate = true;
|
||||
private defaultCamPos = new THREE.Vector3(0, 3, 6);
|
||||
private bgGroup: THREE.Group | null = null;
|
||||
private labelSprites: THREE.Sprite[] = [];
|
||||
private currentParams: PlanetSystemParams | null = null;
|
||||
|
||||
constructor(private container: HTMLElement) {
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x020408);
|
||||
|
||||
const w = container.clientWidth || 400;
|
||||
const h = container.clientHeight || 300;
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.01, 2000);
|
||||
this.camera.position.set(0, 3, 6);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
this.renderer.setSize(w, h);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
container.appendChild(this.renderer.domElement);
|
||||
|
||||
// OrbitControls for mouse interaction
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.08;
|
||||
this.controls.minDistance = 1;
|
||||
this.controls.maxDistance = 500;
|
||||
this.controls.enablePan = true;
|
||||
this.controls.autoRotate = false; // We handle auto-rotate ourselves
|
||||
this.controls.zoomSpeed = 1.2;
|
||||
this.controls.rotateSpeed = 0.8;
|
||||
|
||||
// Stop auto-rotation when user interacts
|
||||
this.controls.addEventListener('start', () => { this.autoRotate = false; });
|
||||
|
||||
this.scene.add(new THREE.AmbientLight(0x222244, 0.4));
|
||||
|
||||
// Build immutable background (stars, galaxy, nebulae)
|
||||
this.buildBackground();
|
||||
}
|
||||
|
||||
// ── Public controls ──
|
||||
|
||||
setSpeed(multiplier: number): void {
|
||||
this.speedMultiplier = multiplier;
|
||||
}
|
||||
|
||||
resetCamera(): void {
|
||||
this.autoRotate = true;
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
toggleAutoRotate(): void {
|
||||
this.autoRotate = !this.autoRotate;
|
||||
}
|
||||
|
||||
getAutoRotate(): boolean {
|
||||
return this.autoRotate;
|
||||
}
|
||||
|
||||
// ── Background: starfield, galaxy, nebulae ──
|
||||
|
||||
private buildBackground(): void {
|
||||
this.bgGroup = new THREE.Group();
|
||||
|
||||
// ── Starfield: 4000 background stars ──
|
||||
const starCount = 4000;
|
||||
const positions = new Float32Array(starCount * 3);
|
||||
const colors = new Float32Array(starCount * 3);
|
||||
const sizes = new Float32Array(starCount);
|
||||
const rand = seededRandom(42);
|
||||
|
||||
const starTints = [
|
||||
new THREE.Color(0xffffff), // white
|
||||
new THREE.Color(0xaaccff), // blue-white
|
||||
new THREE.Color(0xfff4ea), // yellow-white
|
||||
new THREE.Color(0xffd2a1), // orange
|
||||
new THREE.Color(0xffb56c), // red-orange
|
||||
new THREE.Color(0xccddff), // pale blue
|
||||
];
|
||||
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
// Distribute on a large sphere shell (300-800 units away)
|
||||
const theta = rand() * Math.PI * 2;
|
||||
const phi = Math.acos(2 * rand() - 1);
|
||||
const r = 300 + rand() * 500;
|
||||
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
||||
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
||||
positions[i * 3 + 2] = r * Math.cos(phi);
|
||||
|
||||
const tint = starTints[Math.floor(rand() * starTints.length)];
|
||||
const brightness = 0.4 + rand() * 0.6;
|
||||
colors[i * 3] = tint.r * brightness;
|
||||
colors[i * 3 + 1] = tint.g * brightness;
|
||||
colors[i * 3 + 2] = tint.b * brightness;
|
||||
|
||||
sizes[i] = 0.5 + rand() * 2.0;
|
||||
}
|
||||
|
||||
const starGeo = new THREE.BufferGeometry();
|
||||
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||||
starGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
starGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||||
|
||||
const starMat = new THREE.PointsMaterial({
|
||||
size: 1.5,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.bgGroup.add(new THREE.Points(starGeo, starMat));
|
||||
|
||||
// ── Milky Way galactic plane ──
|
||||
// A large tilted disc with dense star concentration
|
||||
const galaxyCount = 6000;
|
||||
const galPos = new Float32Array(galaxyCount * 3);
|
||||
const galCol = new Float32Array(galaxyCount * 3);
|
||||
for (let i = 0; i < galaxyCount; i++) {
|
||||
// Flat disc distribution, concentrated toward center
|
||||
const angle = rand() * Math.PI * 2;
|
||||
const dist = Math.pow(rand(), 0.5) * 600; // More concentrated near center
|
||||
const height = (rand() - 0.5) * (15 + dist * 0.02); // Thin disc, thicker outward
|
||||
|
||||
galPos[i * 3] = dist * Math.cos(angle);
|
||||
galPos[i * 3 + 1] = height;
|
||||
galPos[i * 3 + 2] = dist * Math.sin(angle);
|
||||
|
||||
// Milky Way is blueish-white with warm core
|
||||
const coreProx = 1 - Math.min(1, dist / 600);
|
||||
const r2 = rand();
|
||||
galCol[i * 3] = 0.5 + coreProx * 0.4 + r2 * 0.1;
|
||||
galCol[i * 3 + 1] = 0.5 + coreProx * 0.3 + r2 * 0.1;
|
||||
galCol[i * 3 + 2] = 0.6 + r2 * 0.15;
|
||||
}
|
||||
|
||||
const galGeo = new THREE.BufferGeometry();
|
||||
galGeo.setAttribute('position', new THREE.BufferAttribute(galPos, 3));
|
||||
galGeo.setAttribute('color', new THREE.BufferAttribute(galCol, 3));
|
||||
const galMat = new THREE.PointsMaterial({
|
||||
size: 0.8,
|
||||
vertexColors: true,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
sizeAttenuation: true,
|
||||
depthWrite: false,
|
||||
});
|
||||
const galaxy = new THREE.Points(galGeo, galMat);
|
||||
// Tilt the galactic plane ~60 degrees (we see the Milky Way at an angle)
|
||||
galaxy.rotation.x = Math.PI * 0.35;
|
||||
galaxy.rotation.z = Math.PI * 0.15;
|
||||
galaxy.position.set(0, 100, -200);
|
||||
this.bgGroup.add(galaxy);
|
||||
|
||||
// ── Galactic core glow ──
|
||||
const coreGlowGeo = new THREE.SphereGeometry(40, 16, 16);
|
||||
const coreGlowMat = new THREE.MeshBasicMaterial({
|
||||
color: 0xeeddcc,
|
||||
transparent: true,
|
||||
opacity: 0.04,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
const coreGlow = new THREE.Mesh(coreGlowGeo, coreGlowMat);
|
||||
coreGlow.position.copy(galaxy.position);
|
||||
this.bgGroup.add(coreGlow);
|
||||
|
||||
// ── Nebula patches (colored sprite billboards) ──
|
||||
const nebulaColors = [0x3344aa, 0xaa3355, 0x2288aa, 0x8844aa, 0x44aa66];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const grad = ctx.createRadialGradient(64, 64, 4, 64, 64, 64);
|
||||
const col = new THREE.Color(nebulaColors[i % nebulaColors.length]);
|
||||
grad.addColorStop(0, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.3)`);
|
||||
grad.addColorStop(0.4, `rgba(${Math.floor(col.r * 255)},${Math.floor(col.g * 255)},${Math.floor(col.b * 255)},0.08)`);
|
||||
grad.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 128, 128);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const spriteMat = new THREE.SpriteMaterial({
|
||||
map: tex,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
const theta2 = rand() * Math.PI * 2;
|
||||
const phi2 = (rand() - 0.5) * Math.PI * 0.6;
|
||||
const r2 = 200 + rand() * 400;
|
||||
sprite.position.set(
|
||||
r2 * Math.cos(phi2) * Math.cos(theta2),
|
||||
r2 * Math.sin(phi2),
|
||||
r2 * Math.cos(phi2) * Math.sin(theta2),
|
||||
);
|
||||
sprite.scale.setScalar(60 + rand() * 120);
|
||||
this.bgGroup.add(sprite);
|
||||
}
|
||||
|
||||
this.scene.add(this.bgGroup);
|
||||
}
|
||||
|
||||
update(params: PlanetSystemParams): void {
|
||||
this.clearSystem();
|
||||
this.currentParams = params;
|
||||
|
||||
const sc = starColorFromTemp(params.stellarTempK);
|
||||
const pc = planetColor(params.eqTempK);
|
||||
const orbitRadius = Math.max(0.8, Math.min(4.0, params.semiMajorAxisAU * 2.5));
|
||||
|
||||
// ── Host Star ──
|
||||
const starVisualRadius = 0.25 + params.stellarRadiusSolar * 0.2;
|
||||
const starGeo = new THREE.SphereGeometry(starVisualRadius, 32, 32);
|
||||
const starMat = new THREE.MeshBasicMaterial({ color: sc });
|
||||
this.starMesh = new THREE.Mesh(starGeo, starMat);
|
||||
this.scene.add(this.starMesh);
|
||||
|
||||
const starLight = new THREE.PointLight(sc, 2.5, 30);
|
||||
starLight.position.set(0, 0, 0);
|
||||
this.scene.add(starLight);
|
||||
|
||||
// Star corona
|
||||
const glowGeo = new THREE.SphereGeometry(starVisualRadius * 2.5, 24, 24);
|
||||
const glowMat = new THREE.MeshBasicMaterial({
|
||||
color: sc,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.scene.add(new THREE.Mesh(glowGeo, glowMat));
|
||||
|
||||
// ── Habitable Zone ──
|
||||
if (params.hzMember) {
|
||||
const hzInner = orbitRadius * 0.75;
|
||||
const hzOuter = orbitRadius * 1.35;
|
||||
const hzGeo = new THREE.RingGeometry(hzInner, hzOuter, 64);
|
||||
const hzMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x2ecc71,
|
||||
transparent: true,
|
||||
opacity: 0.07,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.hzInnerRing = new THREE.Mesh(hzGeo, hzMat);
|
||||
this.hzInnerRing.rotation.x = -Math.PI / 2;
|
||||
this.hzInnerRing.position.y = -0.01;
|
||||
this.scene.add(this.hzInnerRing);
|
||||
|
||||
const makeHzCircle = (r: number, color: number, opacity: number) => {
|
||||
const pts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 128; i++) {
|
||||
const th = (i / 128) * Math.PI * 2;
|
||||
pts.push(new THREE.Vector3(r * Math.cos(th), 0, r * Math.sin(th)));
|
||||
}
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity });
|
||||
this.scene.add(new THREE.Line(geo, mat));
|
||||
};
|
||||
makeHzCircle(hzInner, 0x2ecc71, 0.25);
|
||||
makeHzCircle(hzOuter, 0x2ecc71, 0.12);
|
||||
}
|
||||
|
||||
// ── Orbit Path ──
|
||||
this.orbitPoints = [];
|
||||
const segments = 256;
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const theta = (i / segments) * Math.PI * 2;
|
||||
this.orbitPoints.push(new THREE.Vector3(
|
||||
orbitRadius * Math.cos(theta), 0, orbitRadius * Math.sin(theta),
|
||||
));
|
||||
}
|
||||
const orbitGeo = new THREE.BufferGeometry().setFromPoints(this.orbitPoints);
|
||||
const orbitMat = new THREE.LineBasicMaterial({ color: pc, transparent: true, opacity: 0.5 });
|
||||
this.orbitLine = new THREE.Line(orbitGeo, orbitMat);
|
||||
this.scene.add(this.orbitLine);
|
||||
|
||||
// Reference rings
|
||||
const makeRefRing = (r: number) => {
|
||||
const pts: THREE.Vector3[] = [];
|
||||
for (let i = 0; i <= 128; i++) {
|
||||
const th = (i / 128) * Math.PI * 2;
|
||||
pts.push(new THREE.Vector3(r * Math.cos(th), 0, r * Math.sin(th)));
|
||||
}
|
||||
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
||||
const mat = new THREE.LineBasicMaterial({ color: 0x1c2333, transparent: true, opacity: 0.3 });
|
||||
this.scene.add(new THREE.Line(geo, mat));
|
||||
};
|
||||
makeRefRing(0.5 * 2.5);
|
||||
makeRefRing(1.5 * 2.5);
|
||||
|
||||
// AU scale labels
|
||||
this.addScaleLabel('0.5 AU', 0.5 * 2.5 + 0.2, 0.3, 0);
|
||||
this.addScaleLabel('1.0 AU', 1.0 * 2.5 + 0.2, 0.3, 0);
|
||||
this.addScaleLabel('1.5 AU', 1.5 * 2.5 + 0.2, 0.3, 0);
|
||||
if (params.hzMember) {
|
||||
this.addScaleLabel('HZ', orbitRadius * 1.05, 0.5, 0, '#2ecc71');
|
||||
}
|
||||
|
||||
// ── Planet ──
|
||||
const planetVisualRadius = Math.max(0.06, Math.min(0.2, params.radiusEarth * 0.1));
|
||||
const planetGeo = new THREE.SphereGeometry(planetVisualRadius, 24, 24);
|
||||
const planetMat = new THREE.MeshStandardMaterial({
|
||||
color: pc,
|
||||
emissive: pc,
|
||||
emissiveIntensity: 0.2,
|
||||
roughness: 0.7,
|
||||
metalness: 0.1,
|
||||
});
|
||||
this.planetMesh = new THREE.Mesh(planetGeo, planetMat);
|
||||
this.planetMesh.position.copy(this.orbitPoints[0]);
|
||||
this.scene.add(this.planetMesh);
|
||||
|
||||
// Atmosphere halo for habitable candidates
|
||||
if (params.hzMember && params.eqTempK > 180 && params.eqTempK < 350) {
|
||||
const atmoGeo = new THREE.SphereGeometry(planetVisualRadius * 1.2, 24, 24);
|
||||
const atmoMat = new THREE.MeshBasicMaterial({
|
||||
color: 0x66ccff,
|
||||
transparent: true,
|
||||
opacity: 0.12,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this.planetMesh.add(new THREE.Mesh(atmoGeo, atmoMat));
|
||||
}
|
||||
|
||||
// Planet label
|
||||
this.addScaleLabel(
|
||||
params.label,
|
||||
this.orbitPoints[0].x,
|
||||
this.orbitPoints[0].y + planetVisualRadius + 0.15,
|
||||
this.orbitPoints[0].z,
|
||||
'#00e5ff',
|
||||
);
|
||||
|
||||
// ── Grid ──
|
||||
const gridHelper = new THREE.GridHelper(12, 12, 0x151b23, 0x0d1117);
|
||||
gridHelper.position.y = -0.3;
|
||||
this.scene.add(gridHelper);
|
||||
|
||||
// Speed and camera
|
||||
this.orbitSpeed = 0.002 + (1 / Math.max(params.periodDays, 10)) * 0.8;
|
||||
this.orbitAngle = 0;
|
||||
|
||||
const camDist = orbitRadius * 1.8 + 2;
|
||||
this.defaultCamPos.set(camDist * 0.6, camDist * 0.45, camDist * 0.7);
|
||||
this.camera.position.copy(this.defaultCamPos);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
this.controls.update();
|
||||
this.autoRotate = true;
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
private addScaleLabel(text: string, x: number, y: number, z: number, color = '#556677'): void {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.font = '14px monospace';
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(text, 64, 20);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
const mat = new THREE.SpriteMaterial({
|
||||
map: tex,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
});
|
||||
const sprite = new THREE.Sprite(mat);
|
||||
sprite.position.set(x, y, z);
|
||||
sprite.scale.set(1.2, 0.3, 1);
|
||||
this.scene.add(sprite);
|
||||
this.labelSprites.push(sprite);
|
||||
}
|
||||
|
||||
/** Remove system objects but keep background. */
|
||||
private clearSystem(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
const toRemove: THREE.Object3D[] = [];
|
||||
this.scene.traverse((obj) => {
|
||||
if (obj !== this.scene && obj !== this.bgGroup && obj.parent === this.scene && !(obj instanceof THREE.AmbientLight)) {
|
||||
toRemove.push(obj);
|
||||
}
|
||||
});
|
||||
for (const obj of toRemove) {
|
||||
this.scene.remove(obj);
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
}
|
||||
// Re-add ambient if missing
|
||||
let hasAmbient = false;
|
||||
this.scene.traverse((o) => { if (o instanceof THREE.AmbientLight) hasAmbient = true; });
|
||||
if (!hasAmbient) this.scene.add(new THREE.AmbientLight(0x222244, 0.4));
|
||||
|
||||
this.starMesh = null;
|
||||
this.planetMesh = null;
|
||||
this.orbitLine = null;
|
||||
this.hzInnerRing = null;
|
||||
this.labelSprites = [];
|
||||
}
|
||||
|
||||
private animate = (): void => {
|
||||
this.animId = requestAnimationFrame(this.animate);
|
||||
this.time += 0.005 * this.speedMultiplier;
|
||||
|
||||
// Planet orbit
|
||||
if (this.planetMesh && this.orbitPoints.length > 1) {
|
||||
this.orbitAngle = (this.orbitAngle + this.orbitSpeed * this.speedMultiplier) % 1;
|
||||
const idx = Math.floor(this.orbitAngle * (this.orbitPoints.length - 1));
|
||||
this.planetMesh.position.copy(this.orbitPoints[idx]);
|
||||
this.planetMesh.rotation.y += 0.01 * this.speedMultiplier;
|
||||
}
|
||||
|
||||
// Star pulse
|
||||
if (this.starMesh) {
|
||||
const scale = 1 + 0.02 * Math.sin(this.time * 3);
|
||||
this.starMesh.scale.setScalar(scale);
|
||||
}
|
||||
|
||||
// Auto-rotate camera (only if user hasn't grabbed controls)
|
||||
if (this.autoRotate) {
|
||||
const camDist = this.camera.position.length();
|
||||
this.camera.position.x = camDist * 0.7 * Math.sin(this.time * 0.1);
|
||||
this.camera.position.z = camDist * 0.7 * Math.cos(this.time * 0.1);
|
||||
this.camera.position.y = camDist * 0.35 + 0.5 * Math.sin(this.time * 0.07);
|
||||
this.controls.target.set(0, 0, 0);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
|
||||
resize(): void {
|
||||
const w = this.container.clientWidth || 400;
|
||||
const h = this.container.clientHeight || 300;
|
||||
this.camera.aspect = w / h;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(w, h);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
cancelAnimationFrame(this.animId);
|
||||
this.controls.dispose();
|
||||
this.clearSystem();
|
||||
// Also clear background
|
||||
if (this.bgGroup) {
|
||||
this.scene.remove(this.bgGroup);
|
||||
this.bgGroup.traverse((obj) => {
|
||||
if ((obj as THREE.Mesh).geometry) (obj as THREE.Mesh).geometry.dispose();
|
||||
});
|
||||
this.bgGroup = null;
|
||||
}
|
||||
this.renderer.dispose();
|
||||
if (this.renderer.domElement.parentElement) {
|
||||
this.renderer.domElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user