feat: GripAnalysisView — car grip envelope visualization

- New GripAnalysisView with 4 scatter charts:
  1. Grip Envelope (peak G vs speed: lateral/accel/brake)
  2. Grip Circle (longitudinal vs lateral G scatter)
  3. Cornering Grip vs Speed
  4. Steering Response (steer angle vs lateral G)
- Summary stats: max lateral/accel/brake/combined G, peak lateral speed
- GripAnalysis types + type guard
- car_grip use case in SessionDetailView (quick stats + view button)
- Route /analysis/grip/:id
- Global .grid-4 CSS utility
This commit is contained in:
Jan-Peter von Hunnius 2026-06-24 10:18:23 +02:00
parent c6bcab486c
commit 43ffedbf78
6 changed files with 392 additions and 6 deletions

View file

@ -2,8 +2,8 @@
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useSessionsStore } from '@/stores/sessions'
import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types'
import { isShiftPointAnalysis, isTrackMapAnalysis } from '@/utils/analysis'
import type { ShiftPointAnalysis, CarGripAnalysis, TrackMapAnalysis } from '@/types'
import { isShiftPointAnalysis, isCarGripAnalysis, isTrackMapAnalysis } from '@/utils/analysis'
import * as api from '@/api/client'
const props = defineProps<{
@ -56,6 +56,14 @@ const trackAnalysis = computed((): TrackMapAnalysis | null => {
return null
})
const gripAnalysis = computed((): CarGripAnalysis | null => {
if (!analysisData.value) return null
if (isCarGripAnalysis(analysisData.value)) {
return analysisData.value
}
return null
})
onMounted(async () => {
await store.fetchSession(sessionId.value)
})
@ -101,6 +109,8 @@ function viewAnalysis() {
if (!session.value) return
if (session.value.use_case === 'shift_points') {
router.push({ name: 'shift-points', params: { id: session.value.id } })
} else if (session.value.use_case === 'car_grip') {
router.push({ name: 'grip-analysis', params: { id: session.value.id } })
}
}
@ -339,8 +349,28 @@ async function handleBuildTrack() {
</div>
</div>
<!-- Quick stats for car grip -->
<div v-if="gripAnalysis" class="analysis-stats grid grid-4 mb-2">
<div class="stat-card">
<span class="stat-label">Max Lateral G</span>
<span class="stat-value mono">{{ gripAnalysis.summary.max_lateral_g.toFixed(2) }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Max Accel G</span>
<span class="stat-value mono">{{ gripAnalysis.summary.max_accel_g.toFixed(2) }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Max Brake G</span>
<span class="stat-value mono">{{ gripAnalysis.summary.max_brake_g.toFixed(2) }}</span>
</div>
<div class="stat-card">
<span class="stat-label">Max Combined G</span>
<span class="stat-value mono">{{ gripAnalysis.summary.max_combined_g.toFixed(2) }}</span>
</div>
</div>
<div class="flex gap-2">
<button v-if="session.use_case === 'shift_points'" class="btn btn-primary" @click="viewAnalysis">
<button v-if="session.use_case === 'shift_points' || session.use_case === 'car_grip'" class="btn btn-primary" @click="viewAnalysis">
📊 View Full Analysis
</button>
<button class="btn btn-secondary" @click="handleReAnalyze">