diff --git a/app/components/flightlab/pfd/PfdAttitudeIndicator.vue b/app/components/flightlab/pfd/PfdAttitudeIndicator.vue
index 19dab78..e9edc29 100644
--- a/app/components/flightlab/pfd/PfdAttitudeIndicator.vue
+++ b/app/components/flightlab/pfd/PfdAttitudeIndicator.vue
@@ -16,6 +16,8 @@ const pitchScale = computed(() => props.size / 60)
const clipId = computed(() => `att-clip-${uid}`)
const skyGradId = computed(() => `att-sky-${uid}`)
const groundGradId = computed(() => `att-ground-${uid}`)
+const WHITE = '#f4f6fb'
+const YELLOW = '#ffe100'
const pitchMarks = computed(() => {
const marks: Array<{ deg: number; y: number; isLabel: boolean; width: number }> = []
@@ -82,12 +84,12 @@ const dotRadius = computed(() => 4)
-
-
+
+
-
-
+
+
@@ -117,7 +119,7 @@ const dotRadius = computed(() => 4)
:y1="0"
:x2="size * 2"
:y2="0"
- stroke="white"
+ :stroke="WHITE"
stroke-width="2"
/>
@@ -128,14 +130,14 @@ const dotRadius = computed(() => 4)
:y1="mark.y"
:x2="mark.width / 2"
:y2="mark.y"
- stroke="white"
+ :stroke="WHITE"
:stroke-width="mark.isLabel ? 1.5 : 1"
/>
4)
4)
:y1="tick.y1"
:x2="tick.x2"
:y2="tick.y2"
- stroke="#f4f6fb"
+ :stroke="WHITE"
stroke-width="1.5"
/>
@@ -190,7 +192,7 @@ const dotRadius = computed(() => 4)
:y="cy - wingThickness / 2"
:width="wingSpan"
:height="wingThickness"
- fill="#fbbf24"
+ :fill="YELLOW"
rx="1"
/>
@@ -199,7 +201,7 @@ const dotRadius = computed(() => 4)
:y="cy - wingThickness / 2"
:width="wingSpan"
:height="wingThickness"
- fill="#fbbf24"
+ :fill="YELLOW"
rx="1"
/>
@@ -207,7 +209,7 @@ const dotRadius = computed(() => 4)
:cx="cx"
:cy="cy"
:r="dotRadius"
- fill="#fbbf24"
+ :fill="YELLOW"
/>
diff --git a/app/components/flightlab/pfd/PfdContainer.vue b/app/components/flightlab/pfd/PfdContainer.vue
index 7bf515b..b4f62b4 100644
--- a/app/components/flightlab/pfd/PfdContainer.vue
+++ b/app/components/flightlab/pfd/PfdContainer.vue
@@ -56,11 +56,11 @@ const vsPos = computed(() => ({
}))
const headingPos = computed(() => ({
- left: tapeWidth.value,
+ left: attitudePos.value.left - gap.value,
top: attSize.value + gap.value,
}))
-const headingWidth = computed(() => attSize.value + tapeWidth.value * 2)
+const headingWidth = computed(() => attSize.value + gap.value * 2)
diff --git a/app/components/flightlab/pfd/PfdVerticalSpeed.vue b/app/components/flightlab/pfd/PfdVerticalSpeed.vue
index faa86ce..c4649ca 100644
--- a/app/components/flightlab/pfd/PfdVerticalSpeed.vue
+++ b/app/components/flightlab/pfd/PfdVerticalSpeed.vue
@@ -4,44 +4,55 @@ const props = withDefaults(defineProps<{
width?: number
height?: number
}>(), {
- width: 40,
+ width: 48,
height: 300,
})
-const uid = useId()
-const clipId = computed(() => `vs-clip-${uid}`)
-
const centerY = computed(() => props.height / 2)
-const scaleHeight = computed(() => props.height * 0.42)
+const axisX = computed(() => props.width * 0.56)
+const wingX = computed(() => props.width * 0.92)
+const scaleSpan = computed(() => props.height * 0.43)
+const WHITE = '#f4f6fb'
+const GREEN = '#19e34a'
+const majorMarks = [1000, 2000, 4000, 6000] as const
-function vsToY(fpm: number): number {
- const normalized = Math.sign(fpm) * Math.sqrt(Math.abs(fpm) / 6000)
- return centerY.value - normalized * scaleHeight.value
+function speedFraction(absFpm: number): number {
+ const f = Math.max(0, Math.min(6000, absFpm))
+ if (f <= 1000) return (f / 1000) * 0.24
+ if (f <= 2000) return 0.24 + ((f - 1000) / 1000) * 0.14
+ if (f <= 4000) return 0.38 + ((f - 2000) / 2000) * 0.28
+ return 0.66 + ((f - 4000) / 2000) * 0.24
}
-const majorMarks = [-6000, -4000, -2000, -1000, 0, 1000, 2000, 4000, 6000]
+function vsToY(fpm: number): number {
+ const sign = Math.sign(fpm)
+ if (sign === 0) return centerY.value
+ const frac = speedFraction(Math.abs(fpm))
+ return centerY.value - sign * frac * scaleSpan.value
+}
-const marks = computed(() => {
- return majorMarks.map(fpm => ({
- fpm,
- y: vsToY(fpm),
- label: fpm === 0 ? '0' : (Math.abs(fpm) / 1000).toString(),
+const markPositions = computed(() => {
+ return majorMarks.map((mark) => ({
+ mark,
+ topY: vsToY(mark),
+ bottomY: vsToY(-mark),
+ label: String(mark / 1000),
}))
})
-const needleY = computed(() => {
- const clamped = Math.max(-6000, Math.min(6000, props.verticalSpeed))
- return vsToY(clamped)
-})
+const clampedVs = computed(() => Math.max(-6000, Math.min(6000, props.verticalSpeed)))
+const needleY = computed(() => vsToY(clampedVs.value))
+const showReadout = computed(() => Math.abs(props.verticalSpeed) >= 100)
+const readoutText = computed(() => Math.round(Math.abs(props.verticalSpeed) / 100).toString().padStart(2, '0'))
-const readoutText = computed(() => {
- const rounded = Math.round(props.verticalSpeed / 50) * 50
- if (rounded === 0) return '0'
- return rounded > 0 ? `+${rounded}` : `${rounded}`
+const channelShape = computed(() => {
+ const left = props.width * 0.18
+ const right = props.width
+ const shoulder = props.width * 0.86
+ const topInset = props.height * 0.11
+ const bottomInset = props.height * 0.89
+ return `${left},0 ${shoulder},0 ${right},${topInset} ${right},${bottomInset} ${shoulder},${props.height} ${left},${props.height}`
})
-
-const bandY = computed(() => Math.min(centerY.value, needleY.value))
-const bandHeight = computed(() => Math.abs(needleY.value - centerY.value))
@@ -51,84 +62,105 @@ const bandHeight = computed(() => Math.abs(needleY.value - centerY.value))
:viewBox="`0 0 ${width} ${height}`"
xmlns="http://www.w3.org/2000/svg"
>
-
-
-
-
-
+
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
- {{ mark.label }}
-
-
-
-
-
-
-
-
+
+ {{ mark.label }}
+
+
+ {{ mark.label }}
+
-
+
+
+
+
+
+
{{ readoutText }}