chore(workspace): add gitea repository url to all packages
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 3m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 3m48s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
This commit is contained in:
307
components/home/hero-webgl/Generator.ts
Normal file
307
components/home/hero-webgl/Generator.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
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<string, SceneNode>();
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user