96fb75ffdf
- 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>
233 lines
8.8 KiB
HTML
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>
|