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) {
|
||||
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 zoneId = zone ? zone.kind : 'land';
|
||||
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) {
|
||||
const cosR = Math.cos(airportMesh.rotation.y);
|
||||
const sinR = Math.sin(airportMesh.rotation.y);
|
||||
return {
|
||||
x: airportMesh.position.x + lx * cosR - lz * sinR,
|
||||
x: airportMesh.position.x + lx * cosR + lz * sinR,
|
||||
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_Y = 0.50; // auf Plateau-Oberkante (LIFT+Scale²≈0.26)
|
||||
|
||||
// Zyklus-Phase (Sekunden) mit individuellem Offset pro Airport, plus
|
||||
// +60 s Grund-Offset damit Plane nicht sofort beim Level-Start startet.
|
||||
const tPhase = (tNow + v.phaseOffset + 60) % PLANE_CYCLE;
|
||||
// Zyklus-Phase — beginnt sofort in Phase 0 (Plane steht auf Runway),
|
||||
// damit beim Bau des Airports das Flugzeug direkt sichtbar ist.
|
||||
const tPhase = (tNow + v.phaseOffset) % PLANE_CYCLE;
|
||||
plane.visible = true;
|
||||
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