Inovativni program za generisanje i manipulaciju geodetskih kupola

https://abel.rs/as/geodetska_kupola.html

Predstavljamo napredni web program koji omogućava korisnicima da lako i intuitivno kreiraju, prilagođavaju i izvoze 3D modele geodetskih kupola. Ovaj alat kombinuje modernu tehnologiju web preglednika i moćne biblioteke Three.js, pružajući bogat skup funkcionalnosti za arhitekte, dizajnere, inženjere i entuzijaste.

Glavne funkcionalnosti programa:

1. Generisanje geodetske kupole sa podesivom frekvencijom

Program korisniku nudi izbor frekvencije kupole od 0 do 3, gde:

  • Frekvencija 0 predstavlja osnovni ikosaedar.
  • Veće frekvencije (1, 2, 3) donose detaljnije i finije mreže koje približavaju kupolu sfernom obliku.

Na osnovu odabrane frekvencije, algoritam generiše geometriju kupole koristeći principe geometrije i normalizacije vrhova na sferu. Rezultat je precizan i vizuelno atraktivan 3D model koji se prikazuje u realnom vremenu.

2. Interaktivno sečenje kupole po visini i rotaciji preseka

Jedna od posebnih karakteristika programa je mogućnost presecanja kupole duž visine (Y ose) sa dodatnim podešavanjima rotacije preseka oko X, Y i Z osa. Ovo omogućava korisniku da:

  • Izreže gornji deo kupole, dobijajući polu-kupolu ili drugačije geometrijske oblike.
  • Precizno pozicionira i orijentiše preseke radi optimizacije oblika ili pripreme za dalje modifikacije i izradu.

3. Rotacija cele kupole

Pored preseka, program nudi i kontrolu rotacije cele kupole oko X i Y osa. Ova funkcionalnost je izuzetno korisna za pregled i analizu modela iz različitih uglova, kao i za pripremu za izvoz i dalju upotrebu.

4. Undo/Redo funkcije za upravljanje promenama

Korisničko iskustvo je značajno unapređeno zahvaljujući undo i redo funkcijama koje omogućavaju:

  • Vraćanje na prethodna stanja modela nakon presecanja ili drugih izmena.
  • Ponovno vraćanje promena ako se želi testirati različite varijante bez straha od gubitka rada.

Ove funkcije čine rad s programom fleksibilnim i sigurnim.

5. Vizuelizacija u realnom vremenu koristeći Three.js

Program koristi Three.js, jednu od najpopularnijih JavaScript biblioteka za 3D grafiku u webu, što omogućava:

  • Kvalitetnu grafiku i glatku animaciju modela u realnom vremenu.
  • Dinamičko osvetljenje i senke koje poboljšavaju vizuelni doživljaj.
  • Prilagodljiv prikaz koji se automatski skalira prilikom promene veličine prozora.

U planu je da se program doradi tako da korisnik nakon što kreira i prilagodi svoju kupolu, može da je lako izveze u popularni STL format:

  • STL fajl je široko podržan u softverima za 3D štampu i CAD aplikacijama.
  • Omogućava direktnu proizvodnju modela ili dodatnu obradu u specijalizovanim alatima.

Ovaj program predstavlja moćan i pristupačan alat za sve koji žele da se bave geodetskim kupolama, bilo u edukaciji, arhitekturi, ili inženjeringu. Intuitivni interfejs, bogat skup funkcija i mogućnost izvoza modela čine ga idealnim izborom za brzo kreiranje, prilagođavanje i deljenje 3D kupola.

Isprobajte ga i otkrijte kako tehnologija može pojednostaviti i unaprediti vaše kreativne projekte!

Matematika geodetske kupole

Matematika geodetske kupole u programu

Ovaj dokument detaljno objašnjava matematičke principe korišćene u programu za generisanje geodetskih kupola. Cilj je da razumemo kako se iz osnovnog ikosaedra dobijaju složenije i preciznije strukture pomoću geometrijskih transformacija i subdivizije.

1. Osnovni poliedar: Ikosaedar

Geodetska kupola počinje od regularnog ikosaedra, koji ima:

  • 12 temena (vrhova)
  • 20 trouglastih lica
  • 30 ivica

Koordinate temena ikosaedra koriste se kao polazne tačke za konstrukciju kupole.

Koordinate temena ikosaedra

Ako je \( t = \frac{1 + \sqrt{5}}{2} \) (zlatni rez), tada su vrhovi definisani kao:

[
  (-1,  t,  0), ( 1,  t,  0), (-1, -t,  0), ( 1, -t,  0),
  ( 0, -1,  t), ( 0,  1,  t), ( 0, -1, -t), ( 0,  1, -t),
  ( t,  0, -1), ( t,  0,  1), (-t,  0, -1), (-t,  0,  1)
]
  

Ove koordinate se zatim normalizuju na poluprečnik sfere (npr. 100 jedinica) da bi temena ležala na sferi.

2. Normalizacija vrhova na sferu

Svaki vektor temena se normalizuje (pretvara u jedinični vektor) i zatim se skaluje na željeni poluprečnik \( R \).

\[ \mathbf{v}_{norm} = R \times \frac{\mathbf{v}}{|\mathbf{v}|} \]

Gde je:

  • \(\mathbf{v} = (x, y, z)\) originalni vektor
  • \(|\mathbf{v}| = \sqrt{x^2 + y^2 + z^2}\) norma (dužina) vektora
  • \(R\) željeni poluprečnik sfere

3. Subdivizija trouglova

Svako trouglasto lice ikosaedra se deli na manje trouglove radi dobijanja glatkije i detaljnije površine kupole.

Proces subdivizije sa dubinom \( d \) radi se rekuzivno:

  • Za svaki trougao sa temenima \(\mathbf{v_1}, \mathbf{v_2}, \mathbf{v_3}\), računa se sredina svake ivice:

    \[ \mathbf{v_{12}} = \frac{\mathbf{v_1} + \mathbf{v_2}}{2}, \quad \mathbf{v_{23}} = \frac{\mathbf{v_2} + \mathbf{v_3}}{2}, \quad \mathbf{v_{31}} = \frac{\mathbf{v_3} + \mathbf{v_1}}{2} \]

  • Svaka sredina se normalizuje na poluprečnik sfere kao u prethodnom koraku.
  • Originalni trougao se zamenjuje sa četiri nova trougla: \[ (\mathbf{v_1}, \mathbf{v_{12}}, \mathbf{v_{31}}), \quad (\mathbf{v_2}, \mathbf{v_{23}}, \mathbf{v_{12}}), \quad (\mathbf{v_3}, \mathbf{v_{31}}, \mathbf{v_{23}}), \quad (\mathbf{v_{12}}, \mathbf{v_{23}}, \mathbf{v_{31}}) \]
  • Ovaj proces se ponavlja dok se ne dostigne željena dubina subdivizije, koja je u programu definisana kao frekvencija kupole.

4. Frekvencija kupole

Frekvencija (\( freq \)) određuje koliko puta se svaki trougao ikosaedra deli. Veća frekvencija znači:

  • Više manjih trouglova
  • Finoću i bolju aproksimaciju sfere

Na primer:

  • \( freq = 0 \): osnovni ikosaedar (20 trouglova)
  • \( freq = 1 \): svaki trougao podeljen na 4
  • \( freq = 2 \): svaki trougao podeljen na 16 \((4^2)\)
  • \( freq = 3 \): svaki trougao podeljen na 64 \((4^3)\)

5. Presecanje kupole

Nakon generisanja kupole, moguće je preseći je na određenoj visini \( h \) duž Y ose. Program uklanja sve trouglove koji imaju sva tri temena ispod te visine.

Matematika preseka je jednostavna:

  • Za svaki trougao proveriti Y koordinate temena \( y_1, y_2, y_3 \)
  • Ako su \( y_1, y_2, y_3 \geq h \), trougao se zadržava
  • Inače, trougao se odbacuje

6. Rotacije modela

Rotacije se primenjuju na ceo model ili na preseke, rotiranjem oko X, Y ili Z ose pomoću rotacionih matrica.

Matrica rotacije oko X ose za ugao \( \theta \): \[ R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta \\ 0 & \sin\theta & \cos\theta \end{bmatrix} \]

Matrica rotacije oko Y ose za ugao \( \phi \): \[ R_y(\phi) = \begin{bmatrix} \cos\phi & 0 & \sin\phi \\ 0 & 1 & 0 \\ -\sin\phi & 0 & \cos\phi \end{bmatrix} \]

Matrica rotacije oko Z ose za ugao \( \psi \): \[ R_z(\psi) = \begin{bmatrix} \cos\psi & -\sin\psi & 0 \\ \sin\psi & \cos\psi & 0 \\ 0 & 0 & 1 \end{bmatrix} \]

Svaka tačka \( \mathbf{p} = (x, y, z) \) rotira se množenjem sa odgovarajućom rotacionom matricom.

Zaključak

Program koristi elegantnu kombinaciju geometrijskih transformacija, poliedarskih osnovnih oblika i matematičke logike da bi generisao i manipulirao kompleksnim 3D modelima geodetskih kupola. Razumevanje ovih principa pomaže u prilagođavanju i daljem razvoju softvera.


Autor: Abel | Datum: 2025


Programski kod za geodetska_kupola.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Geodetska Kupola</title>
  <style>
    body, html { margin: 0; height: 100%; overflow: hidden; font-family: sans-serif; }
    #container { display: flex; height: 100vh; }
    #renderer { flex: 1; background: #e0e0e0; }
    #controls {
      position: absolute;
      right: 0; top: 0; bottom: 0;
      width: 300px;
      padding: 20px;
      background: #f0f0f0;
      box-shadow: -2px 0 5px rgba(0,0,0,0.1);
      overflow-y: auto;
      z-index: 10;
    }
    label, select, input, button {
      display: block;
      margin-bottom: 10px;
      width: 100%;
      box-sizing: border-box;
    }
    button {
      cursor: pointer;
      padding: 8px;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="renderer"></div>
    <div id="controls">
      <h2>Parametri kupole</h2>
      <label for="frequency">Frekvencija (0-3):</label>
      <select id="frequency">
        <option value="0">0 (Ikosaedar)</option>
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>

      <label for="sliceHeight">Visina preseka (Y):</label>
      <input type="number" id="sliceHeight" value="0" step="1" />

      <label for="sliceRotX">Rotacija preseka X (°):</label>
      <input type="number" id="sliceRotX" value="0" step="0.1" />
      <label for="sliceRotY">Rotacija preseka Y (°):</label>
      <input type="number" id="sliceRotY" value="0" step="0.1" />
      <label for="sliceRotZ">Rotacija preseka Z (°):</label>
      <input type="number" id="sliceRotZ" value="0" step="0.1" />

      <label for="rotX">Rotacija kupole X (°):</label>
      <input type="number" id="rotX" value="0" step="0.1" />
      <label for="rotY">Rotacija kupole Y (°):</label>
      <input type="number" id="rotY" value="0" step="0.1" />

      <button id="showBtn">Prikaži kupolu</button>
      <button id="sliceBtn">Preseci</button>
      <button id="undoBtn">Undo</button>
      <button id="redoBtn">Redo</button>
      <button id="exportBtn">Izvezi STL</button>
    </div>
  </div>

<script src="https://cdn.jsdelivr.net/npm/three@0.153.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.153.0/examples/js/exporters/STLExporter.js"></script>
<script>
const DISPLAY_RADIUS = 100;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, (window.innerWidth - 300) / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 300);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth - 300, window.innerHeight);
document.getElementById('renderer').appendChild(renderer.domElement);

const ambient = new THREE.AmbientLight(0xffffff, 0.8);
const directional = new THREE.DirectionalLight(0xffffff, 0.6);
directional.position.set(1, 1, 1);
scene.add(ambient, directional);

const domeGroup = new THREE.Group();
scene.add(domeGroup);

// Axes helper to show coordinate system at center
const axesHelper = new THREE.AxesHelper(120);
scene.add(axesHelper);

let isDragging = false;
let prevMouse = {x: 0, y: 0};
let rotation = {x: 0, y: 0};
let rotationXInput = document.getElementById('rotX');
let rotationYInput = document.getElementById('rotY');

renderer.domElement.addEventListener('mousedown', e => {
  isDragging = true;
  prevMouse = {x: e.clientX, y: e.clientY};
});
renderer.domElement.addEventListener('mouseup', e => {
  isDragging = false;
});
renderer.domElement.addEventListener('mousemove', e => {
  if (!isDragging) return;
  const dx = e.clientX - prevMouse.x;
  const dy = e.clientY - prevMouse.y;
  rotation.y += dx * 0.005;
  rotation.x += dy * 0.005;
  prevMouse = {x: e.clientX, y: e.clientY};
  updateRotationInputs();
});

rotationXInput.addEventListener('input', e => {
  let val = parseFloat(rotationXInput.value);
  if (!isNaN(val)) {
    rotation.x = THREE.MathUtils.degToRad(val);
  }
});
rotationYInput.addEventListener('input', e => {
  let val = parseFloat(rotationYInput.value);
  if (!isNaN(val)) {
    rotation.y = THREE.MathUtils.degToRad(val);
  }
});

function updateRotationInputs(){
  rotationXInput.value = THREE.MathUtils.radToDeg(rotation.x).toFixed(1);
  rotationYInput.value = THREE.MathUtils.radToDeg(rotation.y).toFixed(1);
}

// Icosahedron base vertices and faces
const t = (1 + Math.sqrt(5)) / 2;
const icoVertices = [
  [-1,  t,  0], [1,  t,  0], [-1, -t,  0], [1, -t,  0],
  [0, -1,  t], [0,  1,  t], [0, -1, -t], [0,  1, -t],
  [ t,  0, -1], [ t,  0,  1], [-t,  0, -1], [-t,  0,  1]
].map(v => new THREE.Vector3(...v));

const icoFaces = [
  [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
  [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
  [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
  [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]
];

function normalizeAndScale(v, r) {
  return v.clone().normalize().multiplyScalar(r);
}

function subdivideTriangle(v1, v2, v3, depth) {
  if (depth === 0) return [[v1, v2, v3]];
  let v12 = normalizeAndScale(v1.clone().add(v2).multiplyScalar(0.5), DISPLAY_RADIUS);
  let v23 = normalizeAndScale(v2.clone().add(v3).multiplyScalar(0.5), DISPLAY_RADIUS);
  let v31 = normalizeAndScale(v3.clone().add(v1).multiplyScalar(0.5), DISPLAY_RADIUS);
  return [
    ...subdivideTriangle(v1, v12, v31, depth - 1),
    ...subdivideTriangle(v2, v23, v12, depth - 1),
    ...subdivideTriangle(v3, v31, v23, depth - 1),
    ...subdivideTriangle(v12, v23, v31, depth - 1)
  ];
}

let currentTris = [];
let currentFreq = 0;

function createDome(freq) {
  domeGroup.clear();
  currentFreq = freq;
  currentTris = [];
  for (let face of icoFaces) {
    const [i1, i2, i3] = face;
    const v1 = normalizeAndScale(icoVertices[i1], DISPLAY_RADIUS);
    const v2 = normalizeAndScale(icoVertices[i2], DISPLAY_RADIUS);
    const v3 = normalizeAndScale(icoVertices[i3], DISPLAY_RADIUS);
    currentTris.push(...subdivideTriangle(v1, v2, v3, freq));
  }
  drawDomeLines(currentTris);
}

function drawDomeLines(tris) {
  domeGroup.clear();
  const mat = new THREE.LineBasicMaterial({ color: 0x0077ff });
  for (let tri of tris) {
    const geo = new THREE.BufferGeometry().setFromPoints([...tri, tri[0]]);
    const line = new THREE.Line(geo, mat);
    domeGroup.add(line);
  }
  addSliceLine();
}

// Undo/Redo stacks
const undoStack = [];
const redoStack = [];

function pushState() {
  undoStack.push({
    tris: currentTris.map(tri => tri.map(v => v.clone())),
    freq: currentFreq,
    rotationX: rotation.x,
    rotationY: rotation.y,
    sliceHeight: parseFloat(sliceHeightInput.value),
    sliceRotX: parseFloat(sliceRotXInput.value),
    sliceRotY: parseFloat(sliceRotYInput.value),
    sliceRotZ: parseFloat(sliceRotZInput.value)
  });
  if (undoStack.length > 50) undoStack.shift();
}

function popState() {
  if (undoStack.length === 0) return null;
  return undoStack.pop();
}

function pushRedoState(state) {
  redoStack.push(state);
  if (redoStack.length > 50) redoStack.shift();
}

function popRedoState() {
  if (redoStack.length === 0) return null;
  return redoStack.pop();
}

function restoreState(state) {
  if (!state) return;
  currentTris = state.tris.map(tri => tri.map(v => v.clone()));
  currentFreq = state.freq;
  rotation.x = state.rotationX;
  rotation.y = state.rotationY;
  sliceHeightInput.value = state.sliceHeight;
  sliceRotXInput.value = state.sliceRotX;
  sliceRotYInput.value = state.sliceRotY;
  sliceRotZInput.value = state.sliceRotZ;
  updateRotationInputs();
  drawDomeLines(currentTris);
  applyRotationToDome();
  updateSlicePlane();
}

function clearRedoStack() {
  redoStack.length = 0;
}

// Slice plane and helpers
const sliceHeightInput = document.getElementById('sliceHeight');
const sliceRotXInput = document.getElementById('sliceRotX');
const sliceRotYInput = document.getElementById('sliceRotY');
const sliceRotZInput = document.getElementById('sliceRotZ');

const sliceLineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 2 });
let sliceLine = null;

// Slice plane is a THREE.Plane with rotation and height controls
const slicePlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);

function addSliceLine() {
  if (sliceLine) {
    domeGroup.remove(sliceLine);
  }
  // Compute a large line along intersection with sphere, approximated by circle intersection with plane
  // We'll draw a red circle on sphere showing intersection
  const points = [];
  const segments = 128;
  const radius = DISPLAY_RADIUS;
  // slicePlane is defined by normal and constant d: normal.dot(point) + d = 0
  // We'll sample points on a circle on sphere intersected by the plane:
  // We find two orthogonal vectors in plane:
  let normal = slicePlane.normal.clone();
  let u = new THREE.Vector3();
  if (Math.abs(normal.x) > 0.1 || Math.abs(normal.z) > 0.1) {
    u.set(-normal.z, 0, normal.x).normalize();
  } else {
    u.set(1, 0, 0);
  }
  let v = new THREE.Vector3().crossVectors(normal, u);
  for(let i=0; i<=segments; i++){
    let angle = (i/segments)*Math.PI*2;
    // point on plane circle:
    let p = new THREE.Vector3().addScaledVector(u, Math.cos(angle)*radius)
                               .addScaledVector(v, Math.sin(angle)*radius);
    // project onto plane:
    let distToPlane = normal.dot(p) + slicePlane.constant;
    p.addScaledVector(normal, -distToPlane);
    // normalize to radius to lie on sphere surface:
    p.normalize().multiplyScalar(radius);
    points.push(p);
  }
  const geo = new THREE.BufferGeometry().setFromPoints(points);
  sliceLine = new THREE.LineLoop(geo, sliceLineMaterial);
  domeGroup.add(sliceLine);
}

// Update slicePlane from inputs
function updateSlicePlane(){
  // Rotation inputs are in degrees
  let rx = THREE.MathUtils.degToRad(parseFloat(sliceRotXInput.value) || 0);
  let ry = THREE.MathUtils.degToRad(parseFloat(sliceRotYInput.value) || 0);
  let rz = THREE.MathUtils.degToRad(parseFloat(sliceRotZInput.value) || 0);

  // Start normal vector (0,1,0)
  let normal = new THREE.Vector3(0,1,0);
  // Apply rotations (order ZYX)
  let euler = new THREE.Euler(rx, ry, rz, 'ZYX');
  normal.applyEuler(euler).normalize();

  // Distance from origin (negative sliceHeight to match THREE.Plane sign convention)
  let d = -parseFloat(sliceHeightInput.value) || 0;

  slicePlane.set(normal, d);

  // Redraw line
  addSliceLine();
}

// Apply rotation from inputs to domeGroup
function applyRotationToDome() {
  domeGroup.rotation.x = rotation.x;
  domeGroup.rotation.y = rotation.y;
}

function sliceDome() {
  // Slice dome: remove all triangles with all 3 points below the slice plane (plane normal.dot(point)+constant > 0 is above)
  let normal = slicePlane.normal;
  let constant = slicePlane.constant;

  const filteredTris = currentTris.filter(tri => {
    // If at least one vertex is above or on the plane (normal.dot(v)+d <=0), keep
    for (let v of tri) {
      if (normal.dot(v) + constant <= 0) return true;
    }
    return false;
  });
  currentTris = filteredTris;
  drawDomeLines(currentTris);
  applyRotationToDome();
}

function exportSTL() {
  const exporter = new THREE.STLExporter();
  // Build geometry from current triangles:
  let geometry = new THREE.BufferGeometry();
  let positions = [];
  for(let tri of currentTris) {
    tri.forEach(v => {
      positions.push(v.x, v.y, v.z);
    });
  }
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex([...Array(currentTris.length).keys()].flatMap(i => [i*3, i*3+1, i*3+2]));
  // Create mesh for export
  let material = new THREE.MeshStandardMaterial();
  let mesh = new THREE.Mesh(geometry, material);
  mesh.rotation.x = rotation.x;
  mesh.rotation.y = rotation.y;
  const stlString = exporter.parse(mesh);
  const freq = document.getElementById('frequency').value;
  const rx = rotationXInput.value;
  const ry = rotationYInput.value;
  const sliceH = sliceHeightInput.value;
  const fileName = `kupola_freq${freq}_rotX${rx}_rotY${ry}_sliceH${sliceH}.stl`;
  const blob = new Blob([stlString], {type: 'text/plain'});
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
  URL.revokeObjectURL(link.href);
}

// Event listeners for buttons
document.getElementById('showBtn').addEventListener('click', () => {
  pushState();
  clearRedoStack();
  const freq = parseInt(document.getElementById('frequency').value);
  createDome(freq);
  applyRotationToDome();
  updateRotationInputs();
  updateSlicePlane();
});

document.getElementById('sliceBtn').addEventListener('click', () => {
  pushState();
  clearRedoStack();
  sliceDome();
});

document.getElementById('undoBtn').addEventListener('click', () => {
  let state = popState();
  if(state) {
    pushRedoState({
      tris: currentTris.map(tri => tri.map(v => v.clone())),
      freq: currentFreq,
      rotationX: rotation.x,
      rotationY: rotation.y,
      sliceHeight: parseFloat(sliceHeightInput.value),
      sliceRotX: parseFloat(sliceRotXInput.value),
      sliceRotY: parseFloat(sliceRotYInput.value),
      sliceRotZ: parseFloat(sliceRotZInput.value)
    });
    restoreState(state);
  }
});

document.getElementById('redoBtn').addEventListener('click', () => {
  let state = popRedoState();
  if(state) {
    pushState();
    restoreState(state);
  }
});

document.getElementById('exportBtn').addEventListener('click', () => {
  exportSTL();
});

// Update slicePlane and line when slice inputs change
sliceHeightInput.addEventListener('input', () => {
  updateSlicePlane();
});
sliceRotXInput.addEventListener('input', () => {
  updateSlicePlane();
});
sliceRotYInput.addEventListener('input', () => {
  updateSlicePlane();
});
sliceRotZInput.addEventListener('input', () => {
  updateSlicePlane();
});

// Keyboard controls for rotation
window.addEventListener('keydown', e => {
  const step = THREE.MathUtils.degToRad(1);
  switch(e.key) {
    case 'ArrowUp': rotation.x -= step; updateRotationInputs(); break;
    case 'ArrowDown': rotation.x += step; updateRotationInputs(); break;
    case 'ArrowLeft': rotation.y -= step; updateRotationInputs(); break;
    case 'ArrowRight': rotation.y += step; updateRotationInputs(); break;
  }
});

// Window resize
window.addEventListener('resize', () => {
  camera.aspect = (window.innerWidth - 300) / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth - 300, window.innerHeight);
});

function animate(){
  requestAnimationFrame(animate);
  domeGroup.rotation.x = rotation.x;
  domeGroup.rotation.y = rotation.y;
  renderer.render(scene, camera);
}

animate();

// Init: show ico 0 freq
createDome(0);
updateRotationInputs();
updateSlicePlane();
applyRotationToDome();

</script>
</body>
</html>

By Abel

Leave a Reply

Your email address will not be published. Required fields are marked *