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>
This commit is contained in:
@@ -1539,7 +1539,16 @@ function buyMeasure(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finalizeBuy(id, hitPos) {
|
function finalizeBuy(id, hitPos) {
|
||||||
const rotation = +placementRotation.toFixed(3);
|
let rotation = +placementRotation.toFixed(3);
|
||||||
|
// Flughafen-Ausrichtung: Runway (lokales +X = Start-Richtung) zeigt
|
||||||
|
// IMMER vom Vulkan weg, damit Starts und Landungen in den offenen Ozean
|
||||||
|
// gehen und nicht in die Bergflanke. Überschreibt die zufällige
|
||||||
|
// placementRotation für Airports.
|
||||||
|
if (id === 'airport') {
|
||||||
|
const dx = hitPos.x - VOLCANO_X;
|
||||||
|
const dz = hitPos.z - VOLCANO_Z;
|
||||||
|
rotation = +Math.atan2(-dz, dx).toFixed(3);
|
||||||
|
}
|
||||||
const zone = MEASURE_ZONES_3D[id];
|
const zone = MEASURE_ZONES_3D[id];
|
||||||
const zoneId = zone ? zone.kind : 'land';
|
const zoneId = zone ? zone.kind : 'land';
|
||||||
const resolveMeta = () => ({ pos: hitPos, rotation, zoneId });
|
const resolveMeta = () => ({ pos: hitPos, rotation, zoneId });
|
||||||
@@ -3980,14 +3989,18 @@ function updateAirportVehicles(tNow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lokale→Welt-Transformation für Airport-relative Punkte (inkl. Airport-Rotation)
|
// Lokale→Welt-Transformation für Airport-relative Punkte. Entspricht exakt
|
||||||
|
// dem, was Three.js intern für Child-Meshes macht (rotation.y um Y-Achse):
|
||||||
|
// (lx, ly, lz) → (Px + lx·cosR + lz·sinR, Py + ly, Pz − lx·sinR + lz·cosR)
|
||||||
|
// Frühere Version hatte falsche Vorzeichen → Flugzeug flog bei rotierten
|
||||||
|
// Flughäfen rückwärts, Heli-Lande-Position war schräg versetzt.
|
||||||
function airportLocalToWorld(airportMesh, lx, ly, lz) {
|
function airportLocalToWorld(airportMesh, lx, ly, lz) {
|
||||||
const cosR = Math.cos(airportMesh.rotation.y);
|
const cosR = Math.cos(airportMesh.rotation.y);
|
||||||
const sinR = Math.sin(airportMesh.rotation.y);
|
const sinR = Math.sin(airportMesh.rotation.y);
|
||||||
return {
|
return {
|
||||||
x: airportMesh.position.x + lx * cosR - lz * sinR,
|
x: airportMesh.position.x + lx * cosR + lz * sinR,
|
||||||
y: airportMesh.position.y + ly,
|
y: airportMesh.position.y + ly,
|
||||||
z: airportMesh.position.z + lx * sinR + lz * cosR,
|
z: airportMesh.position.z - lx * sinR + lz * cosR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4072,9 +4085,9 @@ function updateSinglePlane(v, tNow, airportMesh) {
|
|||||||
const RUNWAY_END = 2.2; // lokal, nach 2x-Scale also 4.4 in Welt
|
const RUNWAY_END = 2.2; // lokal, nach 2x-Scale also 4.4 in Welt
|
||||||
const RUNWAY_Y = 0.50; // auf Plateau-Oberkante (LIFT+Scale²≈0.26)
|
const RUNWAY_Y = 0.50; // auf Plateau-Oberkante (LIFT+Scale²≈0.26)
|
||||||
|
|
||||||
// Zyklus-Phase (Sekunden) mit individuellem Offset pro Airport, plus
|
// Zyklus-Phase — beginnt sofort in Phase 0 (Plane steht auf Runway),
|
||||||
// +60 s Grund-Offset damit Plane nicht sofort beim Level-Start startet.
|
// damit beim Bau des Airports das Flugzeug direkt sichtbar ist.
|
||||||
const tPhase = (tNow + v.phaseOffset + 60) % PLANE_CYCLE;
|
const tPhase = (tNow + v.phaseOffset) % PLANE_CYCLE;
|
||||||
plane.visible = true;
|
plane.visible = true;
|
||||||
const prop = plane.userData.propRef;
|
const prop = plane.userData.propRef;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<!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>
|
||||||
Reference in New Issue
Block a user