Files
geograsim/App/sims/klima/plane-test.html
T
Adminator 96fb75ffdf Klima 3D: Flugzeug-Test-Seite + Airport-Ausrichtung vom Vulkan weg
- Neue Datei: App/sims/klima/plane-test.html
  Three.js-Sandbox mit dem makePlane-Mesh, Slidern für rotation.x/y/z,
  Achsen-Helper (X rot, Y grün, Z blau), großem roten Pfeil als
  Bewegungsrichtung (+X), "Flug simulieren"-Button und "Werte kopieren".
  Thomas kann damit die korrekte Flug-Rotation finden und zurückmelden.
- Airport-Ausrichtung: finalizeBuy überschreibt die zufällige
  placementRotation bei id='airport' mit atan2(-dz, dx) zum Vulkan —
  Runway (lokales +X) zeigt nun IMMER vom Vulkan weg, Start und
  Landung über Ozean statt Bergflanke.
- airportLocalToWorld: Vorzeichen an Three.js-Konvention angeglichen
  (x_w += lz·sinR, z_w -= lx·sinR), damit rotierte Airports ihre
  internen Offsets korrekt in Welt-Koordinaten übertragen.
- Plane-Zyklus: +60 s Grund-Offset entfernt, Flugzeug ist bei
  Airport-Bau sofort sichtbar (Phase 0 = parken auf der Runway).

Test-URL: http://localhost/geograsim/App/sims/klima/plane-test.html

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:22:34 +02:00

233 lines
8.8 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Flugzeug-Orientierungs-Test</title>
<style>
body { margin: 0; font-family: sans-serif; background: #e4ecf0; }
#scene { width: 100vw; height: calc(100vh - 200px); }
#controls {
position: fixed; bottom: 0; left: 0; right: 0; height: 200px;
background: #1a2a32; color: #fff; padding: 14px 22px;
display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px 24px;
box-shadow: 0 -4px 12px rgba(0,0,0,0.3);
}
.slider-row { display: flex; flex-direction: column; gap: 2px; }
.slider-row label { font-size: 12px; font-weight: 700; color: #b8d0e0; }
.slider-row input[type=range] { width: 100%; }
.slider-row .val { font-family: monospace; font-size: 13px; color: #7cfae0; }
.note { grid-column: 1 / -1; font-size: 12px; color: #a0b8c8; line-height: 1.5; }
.big { grid-column: 1 / -1; text-align: center; font-size: 15px; font-weight: 700; }
button { padding: 6px 14px; background: #3a7a8a; color: #fff; border: none;
border-radius: 5px; font-weight: 700; cursor: pointer; }
button:hover { background: #4a8a9a; }
</style>
</head>
<body>
<div id="scene"></div>
<div id="controls">
<div class="big">Flugzeug-Orientierungs-Test · Bewegungsrichtung = roter Pfeil (→ weltliches +X)</div>
<div class="slider-row">
<label>rotation.x (Pitch) · <span class="val" id="val-x">0.000</span></label>
<input id="rx" type="range" min="-3.14" max="3.14" step="0.01" value="0">
</div>
<div class="slider-row">
<label>rotation.y (Yaw) · <span class="val" id="val-y">0.000</span></label>
<input id="ry" type="range" min="-3.14" max="3.14" step="0.01" value="0">
</div>
<div class="slider-row">
<label>rotation.z (Roll) · <span class="val" id="val-z">0.000</span></label>
<input id="rz" type="range" min="-3.14" max="3.14" step="0.01" value="0">
</div>
<div class="slider-row">
<button id="btn-animate">Flug simulieren</button>
<button id="btn-reset">Reset</button>
<button id="btn-copy">Werte kopieren</button>
</div>
<div class="note">
Der rote Pfeil zeigt die Bewegungsrichtung bei „Flug simulieren".<br>
Finde die rotation.y-Werte, bei denen die Nase des Flugzeugs GENAU auf dem Pfeil steht (vorwärts fliegt),
und meld mir die Werte (oder drück „Werte kopieren"). π ≈ 3.14, π/2 ≈ 1.57.
</div>
</div>
<script src="https://unpkg.com/three@0.155.0/build/three.min.js"></script>
<script>
'use strict';
// --- Scene ---
const host = document.getElementById('scene');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xbcd5e6);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(2, devicePixelRatio));
renderer.setSize(host.clientWidth, host.clientHeight);
host.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(45, host.clientWidth / host.clientHeight, 0.1, 100);
camera.position.set(4, 4, 4);
camera.lookAt(0, 0, 0);
addEventListener('resize', () => {
renderer.setSize(host.clientWidth, host.clientHeight);
camera.aspect = host.clientWidth / host.clientHeight;
camera.updateProjectionMatrix();
});
// Orbit (drag)
let dragging = false, lastX = 0, lastY = 0, theta = Math.PI / 4, phi = Math.PI / 4, r = 7;
function apply() {
camera.position.set(
Math.sin(theta) * Math.sin(phi) * r,
Math.cos(phi) * r,
Math.cos(theta) * Math.sin(phi) * r
);
camera.lookAt(0, 0.3, 0);
}
apply();
renderer.domElement.addEventListener('pointerdown', (e) => { dragging = true; lastX = e.clientX; lastY = e.clientY; });
addEventListener('pointermove', (e) => {
if (!dragging) return;
theta -= (e.clientX - lastX) * 0.005;
phi = Math.max(0.1, Math.min(Math.PI * 0.48, phi - (e.clientY - lastY) * 0.005));
lastX = e.clientX; lastY = e.clientY;
apply();
});
addEventListener('pointerup', () => { dragging = false; });
renderer.domElement.addEventListener('wheel', (e) => {
e.preventDefault();
r = Math.max(2, Math.min(20, r + e.deltaY * 0.01));
apply();
}, { passive: false });
// Licht
scene.add(new THREE.AmbientLight(0xfff0d8, 0.8));
const sun = new THREE.DirectionalLight(0xffe8c0, 1.0);
sun.position.set(5, 8, 3); scene.add(sun);
// Boden-Grid
const grid = new THREE.GridHelper(20, 20, 0x808080, 0xb0b0b0);
grid.position.y = -0.5;
scene.add(grid);
// Achsen-Helper: X rot, Y grün, Z blau
const axes = new THREE.AxesHelper(1.5);
axes.position.y = -0.49;
scene.add(axes);
// Achsen-Beschriftungen (kleine Sprites)
function labelSprite(text, color) {
const c = document.createElement('canvas');
c.width = c.height = 64;
const ctx = c.getContext('2d');
ctx.font = 'bold 44px sans-serif';
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, 32, 32);
const tex = new THREE.CanvasTexture(c);
const mat = new THREE.SpriteMaterial({ map: tex });
return new THREE.Sprite(mat);
}
const lx = labelSprite('+X (fwd)', '#c03030'); lx.position.set(1.8, -0.2, 0); lx.scale.set(0.8, 0.8, 1); scene.add(lx);
const ly = labelSprite('+Y (up)', '#309030'); ly.position.set(0, 1.8, 0); ly.scale.set(0.8, 0.8, 1); scene.add(ly);
const lz = labelSprite('+Z', '#3030c0'); lz.position.set(0, -0.2, 1.8); lz.scale.set(0.8, 0.8, 1); scene.add(lz);
// Großer roter Pfeil: zeigt die Bewegungsrichtung (+X)
const arrow = new THREE.ArrowHelper(
new THREE.Vector3(1, 0, 0),
new THREE.Vector3(0, -0.35, 0),
2.5,
0xd02020,
0.5, 0.3
);
scene.add(arrow);
// --- Flugzeug (genau wie in game-3d.html makePlane) ---
function makePlane() {
const g = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xe8e8ea, roughness: 0.7 });
const trimMat = new THREE.MeshStandardMaterial({ color: 0x2a4a6a, roughness: 0.8 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 1 });
const fuselage = new THREE.Mesh(
new THREE.CylinderGeometry(0.14, 0.10, 1.2, 10), bodyMat);
fuselage.rotation.z = Math.PI / 2;
g.add(fuselage);
const cockpit = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.13, 0.22), trimMat);
cockpit.position.set(0.12, 0.13, 0); g.add(cockpit);
const wing = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.04, 1.6), bodyMat);
wing.position.set(0.0, 0.20, 0); g.add(wing);
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.25, 0.04), bodyMat);
tail.position.set(-0.55, 0.25, 0); g.add(tail);
const htail = new THREE.Mesh(new THREE.BoxGeometry(0.20, 0.03, 0.5), bodyMat);
htail.position.set(-0.55, 0.18, 0); g.add(htail);
const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.08, 8), darkMat);
hub.rotation.z = Math.PI / 2;
hub.position.set(0.62, 0, 0); g.add(hub);
const prop = new THREE.Group();
prop.position.set(0.66, 0, 0);
const blade = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.55, 0.04), darkMat);
prop.add(blade);
g.add(prop);
g.userData.propRef = prop;
for (const sx of [-0.1, 0.1]) {
const strut = new THREE.Mesh(new THREE.CylinderGeometry(0.01, 0.01, 0.15, 6), darkMat);
strut.position.set(sx, -0.14, 0); g.add(strut);
const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.04, 10), darkMat);
wheel.rotation.x = Math.PI / 2;
wheel.position.set(sx, -0.22, 0); g.add(wheel);
}
return g;
}
const plane = makePlane();
plane.position.set(0, 0.1, 0);
scene.add(plane);
// --- UI-Handling ---
const rx = document.getElementById('rx');
const ry = document.getElementById('ry');
const rz = document.getElementById('rz');
const vx = document.getElementById('val-x');
const vy = document.getElementById('val-y');
const vz = document.getElementById('val-z');
function updatePlane() {
plane.rotation.set(+rx.value, +ry.value, +rz.value);
vx.textContent = (+rx.value).toFixed(3);
vy.textContent = (+ry.value).toFixed(3);
vz.textContent = (+rz.value).toFixed(3);
}
[rx, ry, rz].forEach(s => s.addEventListener('input', updatePlane));
updatePlane();
document.getElementById('btn-reset').addEventListener('click', () => {
rx.value = 0; ry.value = 0; rz.value = 0;
updatePlane();
plane.position.set(0, 0.1, 0);
});
document.getElementById('btn-copy').addEventListener('click', () => {
const text = `plane.rotation.set(${(+rx.value).toFixed(3)}, ${(+ry.value).toFixed(3)}, ${(+rz.value).toFixed(3)});`;
navigator.clipboard?.writeText(text).then(() => alert('Kopiert:\n' + text)).catch(() => alert(text));
});
let animating = false, animStart = 0;
document.getElementById('btn-animate').addEventListener('click', () => {
animating = !animating;
animStart = performance.now();
plane.position.set(-3, 0.1, 0);
});
// --- Render-Loop ---
function loop() {
requestAnimationFrame(loop);
plane.userData.propRef.rotation.x += 0.3;
if (animating) {
const dt = (performance.now() - animStart) / 1000;
// Bewegung entlang +X (wie der rote Pfeil zeigt)
plane.position.x = -3 + (dt * 1.2) % 6;
}
renderer.render(scene, camera);
}
loop();
</script>
</body>
</html>