From 43ffedbf789e88d5fd063b86ccf0662b85288a74 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Wed, 24 Jun 2026 10:18:23 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20GripAnalysisView=20=E2=80=94=20car=20gr?= =?UTF-8?q?ip=20envelope=20visualization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/assets/main.css | 6 +- src/router/index.ts | 6 + src/types/index.ts | 40 ++++- src/utils/analysis.ts | 10 +- src/views/GripAnalysisView.vue | 300 ++++++++++++++++++++++++++++++++ src/views/SessionDetailView.vue | 36 +++- 6 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 src/views/GripAnalysisView.vue diff --git a/src/assets/main.css b/src/assets/main.css index 432fcec..0a5bff9 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -196,8 +196,12 @@ input, select, textarea { grid-template-columns: repeat(3, 1fr); } +.grid-4 { + grid-template-columns: repeat(4, 1fr); +} + @media (max-width: 768px) { - .grid-2, .grid-3 { + .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } } diff --git a/src/router/index.ts b/src/router/index.ts index 865a1e7..905216e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -30,6 +30,12 @@ const router = createRouter({ component: () => import('@/views/ShiftPointsView.vue'), props: true, }, + { + path: '/analysis/grip/:id', + name: 'grip-analysis', + component: () => import('@/views/GripAnalysisView.vue'), + props: true, + }, { path: '/tracks', name: 'tracks', diff --git a/src/types/index.ts b/src/types/index.ts index b0b9954..5d3fbc2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,7 @@ export type UseCase = | 'shift_points' + | 'car_grip' | 'racing_line' | 'braking_analysis' | 'lap_times' @@ -161,6 +162,43 @@ export interface ShiftPointAnalysis { total_frames_analyzed: number } +// Car Grip Analysis (use_case = "car_grip") +export interface GripEnvelopePoint { + speed_kmh: number + max_lateral_g: number + max_accel_g: number + max_brake_g: number + sample_count: number +} + +export interface GripCirclePoint { + longitudinal_g: number + lateral_g: number +} + +export interface SteeringResponsePoint { + steer: number + lateral_g: number + speed_kmh: number +} + +export interface GripSummary { + max_lateral_g: number + max_accel_g: number + max_brake_g: number + max_combined_g: number + peak_lateral_speed_kmh: number +} + +export interface CarGripAnalysis { + summary: GripSummary + grip_envelope: GripEnvelopePoint[] + grip_circle: GripCirclePoint[] + steering_response: SteeringResponsePoint[] + runs_analyzed: number + total_frames_analyzed: number +} + // Track Map Analysis (use_case = "track_mapping") export interface TrackPoint { x: number @@ -237,7 +275,7 @@ export interface BuildTrackModelRequest { export type TrackMappingAnalysis = TrackMapAnalysis -export type AnalysisResult = ShiftPointAnalysis | TrackMapAnalysis | NotImplementedResponse +export type AnalysisResult = ShiftPointAnalysis | CarGripAnalysis | TrackMapAnalysis | NotImplementedResponse // ── Characteristics (aggregated across sessions) ── diff --git a/src/utils/analysis.ts b/src/utils/analysis.ts index 370f80f..9debb18 100644 --- a/src/utils/analysis.ts +++ b/src/utils/analysis.ts @@ -1,4 +1,4 @@ -import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types' +import type { ShiftPointAnalysis, CarGripAnalysis, TrackMapAnalysis } from '@/types' /** * Runtime type guard: checks if an unknown value is a ShiftPointAnalysis. @@ -8,6 +8,14 @@ export function isShiftPointAnalysis(data: unknown): data is ShiftPointAnalysis return typeof data === 'object' && data !== null && 'gear_curves' in data } +/** + * Runtime type guard: checks if an unknown value is a CarGripAnalysis. + * Uses the 'grip_envelope' discriminator field. + */ +export function isCarGripAnalysis(data: unknown): data is CarGripAnalysis { + return typeof data === 'object' && data !== null && 'grip_envelope' in data +} + /** * Runtime type guard: checks if an unknown value is a TrackMapAnalysis. * Uses the 'boundaries' discriminator field that the backend includes. diff --git a/src/views/GripAnalysisView.vue b/src/views/GripAnalysisView.vue new file mode 100644 index 0000000..6114d25 --- /dev/null +++ b/src/views/GripAnalysisView.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/src/views/SessionDetailView.vue b/src/views/SessionDetailView.vue index fcc27b5..0e8b2f2 100644 --- a/src/views/SessionDetailView.vue +++ b/src/views/SessionDetailView.vue @@ -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() { + +
+
+ Max Lateral G + {{ gripAnalysis.summary.max_lateral_g.toFixed(2) }} +
+
+ Max Accel G + {{ gripAnalysis.summary.max_accel_g.toFixed(2) }} +
+
+ Max Brake G + {{ gripAnalysis.summary.max_brake_g.toFixed(2) }} +
+
+ Max Combined G + {{ gripAnalysis.summary.max_combined_g.toFixed(2) }} +
+
+
-