mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
fix(pfd): correct 3D aircraft orientation and heading indicator layout
Aircraft model rebuilt with proper Three.js coordinate system: - Wings along X axis (horizontal), fuselage along Z (forward=-Z), Y=up - Pitch rotates around X, bank around Z - Camera positioned for front-left view Heading indicator fixed to match Airbus PFD style: - Yellow heading readout box at top with pointer triangle below - Ticks grow upward from bottom edge - Labels positioned above ticks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,56 +29,62 @@ let targetPitchRad = 0
|
||||
let targetBankRad = 0
|
||||
|
||||
function buildAircraft(): THREE.Group {
|
||||
// Three.js coords: X=right, Y=up, Z=toward viewer
|
||||
// Aircraft: nose toward -Z, wings along X, Y is up
|
||||
const group = new THREE.Group()
|
||||
|
||||
// Fuselage - cylinder rotated to lie along X axis
|
||||
const bodyColor = 0xdcdcdc
|
||||
const darkColor = 0x2a2a2a
|
||||
const engineColor = 0x555555
|
||||
|
||||
// Fuselage — cylinder along Z axis (default cylinder is along Y, rotate to Z)
|
||||
const fuselageGeo = new THREE.CylinderGeometry(0.4, 0.35, 5, 12)
|
||||
const fuselageMat = new THREE.MeshStandardMaterial({ color: 0xdcdcdc })
|
||||
const fuselageMat = new THREE.MeshStandardMaterial({ color: bodyColor })
|
||||
const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat)
|
||||
fuselage.rotation.z = Math.PI / 2
|
||||
fuselage.rotation.x = Math.PI / 2 // rotate Y-cylinder to lie along Z
|
||||
group.add(fuselage)
|
||||
|
||||
// Nose cone
|
||||
// Nose cone — pointing toward -Z (forward)
|
||||
const noseGeo = new THREE.ConeGeometry(0.35, 1, 12)
|
||||
const noseMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2a })
|
||||
const noseMat = new THREE.MeshStandardMaterial({ color: darkColor })
|
||||
const nose = new THREE.Mesh(noseGeo, noseMat)
|
||||
nose.rotation.z = -Math.PI / 2
|
||||
nose.position.set(3, 0, 0)
|
||||
nose.rotation.x = Math.PI / 2 // cone tip points along -Z
|
||||
nose.position.set(0, 0, -3)
|
||||
group.add(nose)
|
||||
|
||||
// Main wings
|
||||
const wingGeo = new THREE.BoxGeometry(0.6, 4.5, 0.08)
|
||||
const wingMat = new THREE.MeshStandardMaterial({ color: 0xdcdcdc })
|
||||
// Main wings — wide along X (wingspan), thin along Z (chord), very thin along Y
|
||||
const wingGeo = new THREE.BoxGeometry(4.5, 0.08, 0.6) // X=span, Y=thickness, Z=chord
|
||||
const wingMat = new THREE.MeshStandardMaterial({ color: bodyColor })
|
||||
const wings = new THREE.Mesh(wingGeo, wingMat)
|
||||
wings.position.set(-0.2, 0, 0)
|
||||
wings.position.set(0, 0, 0.2)
|
||||
group.add(wings)
|
||||
|
||||
// Horizontal stabilizer
|
||||
const hStabGeo = new THREE.BoxGeometry(0.3, 1.8, 0.06)
|
||||
const hStabMat = new THREE.MeshStandardMaterial({ color: 0xdcdcdc })
|
||||
// Horizontal stabilizer — same orientation as wings, smaller
|
||||
const hStabGeo = new THREE.BoxGeometry(1.8, 0.06, 0.3) // X=span, Y=thick, Z=chord
|
||||
const hStabMat = new THREE.MeshStandardMaterial({ color: bodyColor })
|
||||
const hStab = new THREE.Mesh(hStabGeo, hStabMat)
|
||||
hStab.position.set(-2.3, 0, 0)
|
||||
hStab.position.set(0, 0, 2.3)
|
||||
group.add(hStab)
|
||||
|
||||
// Vertical stabilizer
|
||||
const vStabGeo = new THREE.BoxGeometry(0.6, 0.06, 1.2)
|
||||
const vStabMat = new THREE.MeshStandardMaterial({ color: 0xdcdcdc })
|
||||
// Vertical stabilizer — tall along Y, along Z (chord)
|
||||
const vStabGeo = new THREE.BoxGeometry(0.06, 1.2, 0.6) // X=thin, Y=height, Z=chord
|
||||
const vStabMat = new THREE.MeshStandardMaterial({ color: bodyColor })
|
||||
const vStab = new THREE.Mesh(vStabGeo, vStabMat)
|
||||
vStab.position.set(-2.1, 0, 0.6)
|
||||
vStab.position.set(0, 0.6, 2.1)
|
||||
group.add(vStab)
|
||||
|
||||
// Engines (two, under each wing)
|
||||
// Engines (two, under each wing) — cylinders along Z
|
||||
const engineGeo = new THREE.CylinderGeometry(0.18, 0.22, 0.8, 8)
|
||||
const engineMat = new THREE.MeshStandardMaterial({ color: 0x555555 })
|
||||
const engineMat = new THREE.MeshStandardMaterial({ color: engineColor })
|
||||
|
||||
const engineLeft = new THREE.Mesh(engineGeo, engineMat)
|
||||
engineLeft.rotation.z = Math.PI / 2
|
||||
engineLeft.position.set(0.2, -1.5, -0.35)
|
||||
engineLeft.rotation.x = Math.PI / 2 // align along Z
|
||||
engineLeft.position.set(-1.5, -0.35, -0.2)
|
||||
group.add(engineLeft)
|
||||
|
||||
const engineRight = new THREE.Mesh(engineGeo, engineMat)
|
||||
engineRight.rotation.z = Math.PI / 2
|
||||
engineRight.position.set(0.2, 1.5, -0.35)
|
||||
engineRight.rotation.x = Math.PI / 2 // align along Z
|
||||
engineRight.position.set(1.5, -0.35, -0.2)
|
||||
group.add(engineRight)
|
||||
|
||||
return group
|
||||
@@ -96,10 +102,10 @@ function initScene() {
|
||||
scene.background = new THREE.Color(0x1a5fb4)
|
||||
scene.fog = new THREE.Fog(0x62a0ea, 30, 80)
|
||||
|
||||
// Camera
|
||||
// Camera — view from front-left, slightly above
|
||||
// Aircraft nose points -Z, so camera at +Z looks at front
|
||||
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 200)
|
||||
// View from front-right, slightly above — shows pitch and bank clearly
|
||||
camera.position.set(6, 2, 4)
|
||||
camera.position.set(-4, 3, -6)
|
||||
camera.lookAt(0, 0, 0)
|
||||
|
||||
// Lights
|
||||
@@ -156,11 +162,11 @@ function animate() {
|
||||
animFrameId = requestAnimationFrame(animate)
|
||||
|
||||
if (aircraft) {
|
||||
// Aircraft flies along +X, wings along Y, Z is up
|
||||
// Pitch: rotate around Y axis (wing axis) — positive pitch = nose up
|
||||
aircraft.rotation.y = lerp(aircraft.rotation.y, targetPitchRad, 0.1)
|
||||
// Bank: rotate around X axis (longitudinal axis) — positive bank = right wing down
|
||||
aircraft.rotation.x = lerp(aircraft.rotation.x, targetBankRad, 0.1)
|
||||
// Aircraft: nose toward -Z, wings along X, Y is up
|
||||
// Pitch: rotate around X axis — positive pitch = nose up (rotates nose away from -Z toward +Y)
|
||||
aircraft.rotation.x = lerp(aircraft.rotation.x, targetPitchRad, 0.1)
|
||||
// Bank: rotate around Z axis — positive bank = right wing down
|
||||
aircraft.rotation.z = lerp(aircraft.rotation.z, -targetBankRad, 0.1)
|
||||
}
|
||||
|
||||
if (renderer && scene && camera) {
|
||||
|
||||
@@ -93,23 +93,52 @@ const readoutBoxHeight = 18
|
||||
rx="2"
|
||||
/>
|
||||
|
||||
<!-- Scrolling compass tape (clipped) -->
|
||||
<!-- Readout box at top center (yellow heading value) -->
|
||||
<rect
|
||||
:x="centerX - readoutBoxWidth / 2"
|
||||
y="2"
|
||||
:width="readoutBoxWidth"
|
||||
:height="readoutBoxHeight"
|
||||
fill="rgba(10,10,30,0.95)"
|
||||
stroke="#facc15"
|
||||
stroke-width="1"
|
||||
rx="2"
|
||||
/>
|
||||
<text
|
||||
:x="centerX"
|
||||
:y="16"
|
||||
fill="#facc15"
|
||||
font-size="12"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
font-family="monospace"
|
||||
>
|
||||
{{ readoutText }}
|
||||
</text>
|
||||
|
||||
<!-- Center pointer triangle (yellow, pointing down from readout box) -->
|
||||
<polygon
|
||||
:points="`${centerX},${readoutBoxHeight + 6} ${centerX - 4},${readoutBoxHeight + 2} ${centerX + 4},${readoutBoxHeight + 2}`"
|
||||
fill="#facc15"
|
||||
/>
|
||||
|
||||
<!-- Scrolling compass tape (clipped) — ticks grow upward from bottom -->
|
||||
<g :clip-path="`url(#${clipId})`">
|
||||
<g v-for="mark in marks" :key="mark.deg">
|
||||
<!-- Tick mark -->
|
||||
<!-- Tick mark (from bottom upward) -->
|
||||
<line
|
||||
:x1="mark.x"
|
||||
y1="0"
|
||||
:y1="height"
|
||||
:x2="mark.x"
|
||||
:y2="mark.isMajor ? 12 : 7"
|
||||
:y2="mark.isMajor ? height - 12 : height - 7"
|
||||
stroke="white"
|
||||
:stroke-width="mark.isMajor ? 1.5 : 1"
|
||||
/>
|
||||
<!-- Label -->
|
||||
<!-- Label (above tick) -->
|
||||
<text
|
||||
v-if="mark.isMajor"
|
||||
:x="mark.x"
|
||||
y="24"
|
||||
:y="height - 15"
|
||||
fill="white"
|
||||
font-size="11"
|
||||
text-anchor="middle"
|
||||
@@ -119,34 +148,5 @@ const readoutBoxHeight = 18
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Center pointer triangle (cyan) -->
|
||||
<polygon
|
||||
:points="`${centerX},2 ${centerX - 5},0 ${centerX + 5},0`"
|
||||
fill="#22d3ee"
|
||||
/>
|
||||
|
||||
<!-- Readout box at bottom -->
|
||||
<rect
|
||||
:x="centerX - readoutBoxWidth / 2"
|
||||
:y="height - readoutBoxHeight - 2"
|
||||
:width="readoutBoxWidth"
|
||||
:height="readoutBoxHeight"
|
||||
fill="rgba(10,10,30,0.95)"
|
||||
stroke="#22d3ee"
|
||||
stroke-width="1"
|
||||
rx="2"
|
||||
/>
|
||||
<text
|
||||
:x="centerX"
|
||||
:y="height - 6"
|
||||
fill="#22d3ee"
|
||||
font-size="12"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
font-family="monospace"
|
||||
>
|
||||
{{ readoutText }}
|
||||
</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user