import { createNoise2D } from 'simplex-noise'; import { getTerrainHeight, generateClusteredPoints, generateCatenaryCurve, Vec2, Vec3, distance2D, distance3D } from './math'; export type NodeType = 'wind' | 'solar' | 'city_building' | 'substation' | 'tower' | 'tree'; export interface SceneNode { id: string; type: NodeType; position: Vec3; rotation: Vec3; scale: Vec3; clusterId?: string; subType?: 'tall' | 'medium' | 'small' | string; } export interface SceneEdge { id: string; source: string; target: string; type: 'underground' | 'transmission'; // transmission flies in the air, underground is flat on ground path: Vec3[]; } export interface SceneData { nodes: SceneNode[]; edges: SceneEdge[]; } function rand(seed: number) { const x = Math.sin(seed) * 10000; return x - Math.floor(x); } export function generateSceneData(seed: number): SceneData { let rSeed = seed; const rng = () => rand(rSeed++); const noise2D = createNoise2D(() => rng()); const nodes: SceneNode[] = []; const edges: SceneEdge[] = []; const substations: SceneNode[] = []; // 1. Generation Areas (Wind Parks, Solar Farms, Cities) // We want them spread across a large landscape (-400 to 400) const mapSize = 800; // Choose random central points for clusters const windParks: Vec2[] = []; const solarFarms: Vec2[] = []; const cities: Vec2[] = []; for (let i = 0; i < 4; i++) { windParks.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]); solarFarms.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]); cities.push([(rng() - 0.5) * mapSize, (rng() - 0.5) * mapSize]); } // Push substations away from the centers slightly const placeSubstation = (center: Vec2, clusterId: string) => { const angle = rng() * Math.PI * 2; const r = 20 + rng() * 10; const sx = center[0] + Math.cos(angle) * r; const sz = center[1] + Math.sin(angle) * r; const sy = getTerrainHeight(noise2D, sx, sz); const substation: SceneNode = { id: `sub_${clusterId}`, type: 'substation', position: [sx, sy, sz], rotation: [0, rng() * Math.PI, 0], scale: [1, 1, 1], clusterId }; nodes.push(substation); substations.push(substation); return substation; }; // Generate Wind Parks windParks.forEach((center, idx) => { const cid = `wind_park_${idx}`; const sub = placeSubstation(center, cid); const count = 10 + Math.floor(rng() * 10); // 10-20 turbines const points = generateClusteredPoints(rng, center, count, 60, 15); points.forEach((p, pIdx) => { const y = getTerrainHeight(noise2D, p[0], p[1]); const tid = `${cid}_t_${pIdx}`; nodes.push({ id: tid, type: 'wind', position: [p[0], y, p[1]], rotation: [0, rng() * Math.PI, 0], scale: [1 + rng() * 0.2, 1 + rng() * 0.2, 1 + rng() * 0.2], clusterId: cid }); // Underground cable to substation edges.push({ id: `edge_${tid}_${sub.id}`, source: tid, target: sub.id, type: 'underground', path: [[p[0], y, p[1]], sub.position] }); }); }); // Generate Solar Farms solarFarms.forEach((center, idx) => { const cid = `solar_farm_${idx}`; const sub = placeSubstation(center, cid); const count = 30 + Math.floor(rng() * 20); // Grid-like placement for solar const points: Vec2[] = []; for (let r = -3; r <= 3; r++) { for (let c = -3; c <= 3; c++) { if (rng() > 0.3) { const sx = center[0] + r * 6 + (rng() - 0.5) * 2; const sz = center[1] + c * 4 + (rng() - 0.5) * 2; points.push([sx, sz]); } } } points.forEach((p, pIdx) => { const y = getTerrainHeight(noise2D, p[0], p[1]); const tid = `${cid}_s_${pIdx}`; nodes.push({ id: tid, type: 'solar', position: [p[0], y, p[1]], rotation: [0, 0, Math.PI * 0.1], // slight tilt up scale: [1, 1, 1], clusterId: cid }); // Group cables not needed for every single solar panel, maybe just one underground per farm for visual }); // Add one main underground cable from center of grid to substation const yC = getTerrainHeight(noise2D, center[0], center[1]); edges.push({ id: `edge_solar_main_${cid}_${sub.id}`, source: cid, target: sub.id, type: 'underground', path: [[center[0], yC, center[1]], sub.position] }); }); // Generate Cities cities.forEach((center, idx) => { const cid = `city_${idx}`; const sub = placeSubstation(center, cid); const count = 20 + Math.floor(rng() * 20); const points = generateClusteredPoints(rng, center, count, 50, 6); points.forEach((p, pIdx) => { const y = getTerrainHeight(noise2D, p[0], p[1]); const tid = `${cid}_b_${pIdx}`; const distToCenter = distance2D(p, center); // taller buildings in center const typeRand = rng(); let subType = 'small'; if (distToCenter < 20 && typeRand > 0.4) subType = 'tall'; else if (distToCenter < 35 && typeRand > 0.3) subType = 'medium'; const sY = subType === 'tall' ? 4 + rng() * 4 : subType === 'medium' ? 2 + rng() * 2 : 1 + rng(); nodes.push({ id: tid, type: 'city_building', position: [p[0], y + sY / 2, p[1]], // elevate by half height so it sits on ground rotation: [0, rng() * Math.PI, 0], scale: [1 + rng(), sY, 1 + rng()], clusterId: cid, subType }); }); }); // Connect Substations via high-voltage lines // A simple minimum spanning tree (MST) or nearest neighbor chain const unvisited = [...substations]; const visited: SceneNode[] = []; if (unvisited.length > 0) { visited.push(unvisited.pop()!); } const towersIdMap = new Map(); let towerCount = 0; while (unvisited.length > 0) { let bestDist = Infinity; let bestFrom: SceneNode | null = null; let bestTo: SceneNode | null = null; let bestIdx = -1; for (const v of visited) { for (let i = 0; i < unvisited.length; i++) { const u = unvisited[i]; const d = distance3D(v.position, u.position); if (d < bestDist) { bestDist = d; bestFrom = v; bestTo = u; bestIdx = i; } } } if (bestFrom && bestTo) { visited.push(bestTo); unvisited.splice(bestIdx, 1); // Interpolate towers between bestFrom and bestTo const segLen = 60; const steps = Math.max(1, Math.floor(bestDist / segLen)); let prevPoint: SceneNode = bestFrom; for (let s = 1; s <= steps; s++) { const t = s / (steps + 1); const tx = bestFrom.position[0] + (bestTo.position[0] - bestFrom.position[0]) * t; const tz = bestFrom.position[2] + (bestTo.position[2] - bestFrom.position[2]) * t; const ty = getTerrainHeight(noise2D, tx, tz); const isLast = s === steps + 1; // wait, loop is to steps. Next is bestTo. const newTower: SceneNode = { id: `tower_${++towerCount}`, type: 'tower', position: [tx, ty, tz], rotation: [0, Math.atan2(bestTo.position[0] - bestFrom.position[0], bestTo.position[2] - bestFrom.position[2]), 0], scale: [1, 1, 1] }; nodes.push(newTower); // Cable from prevPoint to newTower // Tower height offset const h1 = prevPoint.type === 'tower' ? 12 : 3; const h2 = newTower.type === 'tower' ? 12 : 3; const p1: Vec3 = [prevPoint.position[0], prevPoint.position[1] + h1, prevPoint.position[2]]; const p2: Vec3 = [newTower.position[0], newTower.position[1] + h2, newTower.position[2]]; edges.push({ id: `edge_hv_${prevPoint.id}_${newTower.id}`, source: prevPoint.id, target: newTower.id, type: 'transmission', path: generateCatenaryCurve(p1, p2, 5, 8) // sag of 5, 8 segments }); prevPoint = newTower; } // connect prevPoint to bestTo const h1 = prevPoint.type === 'tower' ? 12 : 3; const h2 = bestTo.type === 'tower' ? 12 : 3; const p1: Vec3 = [prevPoint.position[0], prevPoint.position[1] + h1, prevPoint.position[2]]; const p2: Vec3 = [bestTo.position[0], bestTo.position[1] + h2, bestTo.position[2]]; edges.push({ id: `edge_hv_${prevPoint.id}_${bestTo.id}`, source: prevPoint.id, target: bestTo.id, type: 'transmission', path: generateCatenaryCurve(p1, p2, 5, 8) }); } } // Generate Trees over empty areas // 1000 trees using random noise clustering for (let i = 0; i < 800; i++) { const tx = (rng() - 0.5) * mapSize; const tz = (rng() - 0.5) * mapSize; // Quick check to avoid intersecting cities/substations (simple distance) let tooClose = false; for (const sub of substations) { if (distance2D([tx, tz], [sub.position[0], sub.position[2]]) < 40) { tooClose = true; break; } } if (!tooClose) { const ty = getTerrainHeight(noise2D, tx, tz); // Only place trees slightly lower on hills, not in water (if any) and clustered const n = noise2D(tx * 0.005, tz * 0.005); if (n > -0.2) { nodes.push({ id: `tree_${i}`, type: 'tree', position: [tx, ty, tz], rotation: [0, rng() * Math.PI, 0], scale: [1 + rng() * 0.5, 1 + rng() * 0.5, 1 + rng() * 0.5] }); } } } return { nodes, edges }; }