Track Map Implementation Tasks — rusty-telemetry & racecraft

# Track Map Implementation Tasks — rusty-telemetry & racecraft

> **Parent note:** [Sim Racing Telemetry Analysis Platform — Project Plan](joplin://6c0dcb2a567348fd9796f50c790082e4) (§17)
> **Created:** 2026-06-08
> **Context:** Concrete implementation tasks to make the full track map use case work end-to-end, including PCARS support. Currently only a basic spline-position-based track map exists (v0.5.0 / v0.1.0). The §17.4 TrackModel with persistent storage, boundary recording, and corner detection is NOT yet implemented.

---

## Current State (as of 2026-06-08)

| Component | What Exists | What's Missing |
|---|---|---|
| rusty-telemetry `analysis.rs` | Basic spline-aligned interpolation for center line (uses `spline_position` 0.0–1.0) | Coordinate-based analysis (PCARS), persistent TrackModel, boundary recording, corner/sector detection |
| rusty-telemetry API | `track_map` listed in use cases, analysis via session endpoints | No `/api/tracks/*` endpoints, no TrackModel CRUD |
| racecraft `TrackMapView.vue` | Canvas 2D boundary vis + center line overlay | No boundary recording UI, no corner/sector display, no track library |
| PCARS position data | Parser extracts `full_position` (V2) and `world_position` (V1) | Not verified that position data flows into TelemetryFrame correctly; no coordinate-based track map analysis |

### Key Problem: `spline_position` vs. World Coordinates

- **AC / Telemetry Tool:** Provides `spline_position` (0.0–1.0, normalized track position). The current track map analysis uses this.
- **PCARS V2:** Provides `full_position` (offset 542, 3 × float32 — world X/Y/Z). No spline position.
- **PCARS V1:** Provides `world_position` per participant (3 × int16). No spline position.
- **DiRT Rally (future):** Will provide world coordinates. No spline position.

The track map analysis must support **both** position types. The `track_mapping` use case with deliberate boundary recording solves this for all games — boundaries are recorded in whatever coordinate system the game provides, and the resulting TrackModel is normalized.

---

## Implementation Tasks

### rusty-telemetry (API Server) — 6 task groups

#### A. Verify & Fix PCARS Position Data in TelemetryFrame

- [ ] Verify PCARS V2 `full_position` (offset 542, 3×f32) maps to TelemetryFrame position fields
- [ ] Verify PCARS V1 `world_position` (per participant, 3×i16) maps correctly
- [ ] Add position fields to TelemetryFrame if not already present (e.g., `world_x`, `world_y`, `world_z` as `Option<f32>`)
- [ ] Ensure both parsers (V1 and V2) populate these fields
- [ ] Add unit tests for position field extraction from PCARS packets

#### B. Track Map Analysis — Dual Position Mode

- [ ] Refactor `analysis.rs` track map analysis to work with **either** position source:
  - **Spline mode** (AC): Use `spline_position` to order frames along the track, then plot using world coordinates if available, or just the spline as x-axis
  - **Coordinate mode** (PCARS, future games): Use `world_x`/`world_z` directly for 2D track plotting
- [ ] Auto-detect which mode to use based on available TelemetryFrame fields
- [ ] Return position data in analysis results (for frontend Canvas rendering)

#### C. Persistent TrackModel Data Structure

- [ ] Define `TrackModel` struct:
  ```rust
  struct TrackModel {
      game: String,
      track_name: String,
      center_line: Vec<(f64, f64)>,      // normalized (x, z) points
      left_boundary: Vec<(f64, f64)>,
      right_boundary: Vec<(f64, f64)>,
      track_width: Vec<f64>,              // width at each point
      heading: Vec<f64>,                  // direction at each point (radians)
      curvature: Vec<f64>,                // κ(s) at each point
      corners: Vec<Corner>,
      sectors: Vec<Sector>,
      source: String,                     // "boundary_recording" | "auto_estimated"
      created_at: String,
  }
  
  struct Corner {
      index: usize,                       // center_line index
      number: u32,
      entry_s: f64,                       // spline distance
      apex_s: f64,
      exit_s: f64,
      curvature_peak: f64,
  }
  
  struct Sector {
      index: u32,
      start_s: f64,
      end_s: f64,
  }
  ```
- [ ] Storage at `data/tracks/<game>/<track_name>.json`
- [ ] Load existing TrackModel on session creation (if track match found)
- [ ] Serialize/deserialize with serde_json

#### D. Track API Endpoints

- [ ] `GET /api/tracks` — list all stored track models (returns array of `{game, track_name, source, created_at, corner_count}`)
- [ ] `GET /api/tracks/{game}/{track_name}` — get full TrackModel
- [ ] `POST /api/tracks/build` — build TrackModel from a completed `track_mapping` session (params: `session_id`)
- [ ] `DELETE /api/tracks/{game}/{track_name}` — delete stored TrackModel
- [ ] `GET /api/tracks/{game}/{track_name}/corners` — get corners only (for overlay on other sessions)
- [ ] Update `/api/use-cases` to include `track_mapping`

#### E. Deliberate Boundary Recording Use Case

- [ ] New use case: `track_mapping`
- [ ] Session guidance text: step-by-step instructions for the user
  - "Run 1: Drive a slow, clean lap hugging the LEFT edge of the track"
  - "Run 2: Drive a slow, clean lap hugging the RIGHT edge of the track"
- [ ] Session completion logic: auto-build TrackModel when 2 boundary runs are complete
- [ ] Validate boundary data (min point count, check for off-track outliers)
- [ ] Link TrackModel to future analysis sessions on same `(game, track_name)`

#### F. Track Model Building Algorithm

- [ ] Resample both boundary runs to equal-length point arrays
- [ ] Compute center line as midpoint of left/right boundaries
- [ ] Compute track width as distance between boundaries at each point
- [ ] Compute heading from consecutive center line points (atan2)
- [ ] Compute curvature κ(s) from heading changes
- [ ] Auto-detect corners: curvature peaks above threshold (e.g., κ > 0.005)
- [ ] Number corners sequentially
- [ ] Define sectors: divide track into 3 roughly equal sectors, or by corner groupings
- [ ] Normalize coordinates: translate and scale so center fits in [0,1] × [0,1] for Canvas rendering

---

### racecraft (Web Client) — 5 task groups

#### G. Track Mapping Workflow UI

- [ ] New session creation: add `track_mapping` to use case selector
- [ ] Guidance panel for `track_mapping` sessions:
  - Step indicator: "Step 1 of 2: Left Boundary"
  - Instruction text: "Drive a slow lap hugging the LEFT edge"
  - Run 1 complete → "Step 2 of 2: Right Boundary"
  - Both runs complete → "Track model ready! Building..."
- [ ] Auto-navigate to track map view after model builds

#### H. Enhanced TrackMapView

- [ ] Render left boundary (e.g., blue line)
- [ ] Render right boundary (e.g., red line)
- [ ] Render center line (e.g., white/yellow dashed)
- [ ] Render corner markers with numbers
- [ ] Render sector boundaries
- [ ] Handle coordinate-based data (PCARS) — normalize to canvas bounds
- [ ] Handle spline-based data (AC) — existing behavior
- [ ] Zoom and pan (stretch goal)

#### I. Track Library Browser

- [ ] New page: `/tracks` — list all stored track models
- [ ] Track card: game icon, track name, corner count, source, created date
- [ ] Click track → detail view with mini map preview
- [ ] Delete track (with confirmation)

#### J. API Client Updates (`src/api/client.ts`)

- [ ] `listTracks(): Promise<TrackSummary[]>` → `GET /api/tracks`
- [ ] `getTrack(game, trackName): Promise<TrackModel>` → `GET /api/tracks/{game}/{track_name}`
- [ ] `buildTrack(sessionId): Promise<TrackModel>` → `POST /api/tracks/build`
- [ ] `deleteTrack(game, trackName): Promise<void>` → `DELETE /api/tracks/{game}/{track_name}`
- [ ] `getTrackCorners(game, trackName): Promise<Corner[]>` → `GET /api/tracks/{game}/{track_name}/corners`

#### K. TypeScript Types (`src/types/index.ts`)

- [ ] `TrackModel` interface: center_line, boundaries, corners, sectors, metadata
- [ ] `Corner` interface: number, entry/apex/exit spline distances, curvature peak
- [ ] `Sector` interface: index, start/end spline distance
- [ ] `TrackSummary` interface: game, track_name, source, created_at, corner_count
- [ ] Update analysis result type guards to handle track map results with new fields

---

## Implementation Order

The backend (rusty-telemetry) must be done first. The frontend depends on the new API endpoints and data structures.

| Order | Repo | Task | Depends On | Est. Effort |
|-------|------|------|-----------|-------------|
| 1 | rusty-telemetry | A. Verify PCARS position data | — | 0.5 day |
| 2 | rusty-telemetry | B. Dual position mode in analysis | A | 1 day |
| 3 | rusty-telemetry | C. TrackModel data structure + storage | — | 1 day |
| 4 | rusty-telemetry | F. Track model building algorithm | C | 1.5 days |
| 5 | rusty-telemetry | E. Boundary recording use case | C, F | 0.5 day |
| 6 | rusty-telemetry | D. Track API endpoints | C, E | 1 day |
| 7 | racecraft | K. TypeScript types | D | 0.5 day |
| 8 | racecraft | J. API client functions | K | 0.5 day |
| 9 | racecraft | G. Track mapping workflow UI | J | 1 day |
| 10 | racecraft | H. Enhanced TrackMapView | J | 1.5 days |
| 11 | racecraft | I. Track library browser | J | 1 day |
| | | **Total** | | **~9.5 days** |

---

## PCARS-Specific Notes

### Position Data Sources

| Source | Offset | Type | Resolution | Games |
|--------|--------|------|-----------|-------|
| Telemetry Tool `spline_position` | AC-specific | float32 (0.0–1.0) | High (normalized) | AC only |
| PCARS V2 `full_position` | 542 | 3 × float32 | High (world coords) | PCARS 2 |
| PCARS V1 `world_position` | per-participant | 3 × int16 | Low (integer meters) | PCARS 1 |
| PCARS V1 `current_lap_distance` | per-participant | uint16 | Track distance | PCARS 1 & 2 |

### PCARS V1 Limitation

The 3 × int16 world position has limited range and precision. For track mapping purposes, it may still work if the coordinates are consistent across frames. This needs verification with live data. If insufficient, PCARS V1 track maps may need to use `current_lap_distance` + `heading` to reconstruct the track shape instead.

### PCARS Configuration for Testing

1. In-game: Options → System → UDP Protocol
2. Set target IP to analysis machine, port 5606
3. Format: **2** (PCARS2 native, recommended) or **1** (PCARS1 compatible)
4. rusty-telemetry auto-binds to `$LISTEN:5606`

---

## Cross-References

- **Main project plan:** [Sim Racing Telemetry Analysis Platform — Project Plan](joplin://6c0dcb2a567348fd9796f50c790082e4)
- **API reference:** [rusty-telemetry — REST API Reference](joplin://0c837f4e6b7e462a997cbc19e47c864a)
- **racecraft notes:** [racecraft — Vue.js Web Client Notes](joplin://50a09627d5d347009197b94bcee90411)
- **PCARS protocol spec:** [Project CARS 1 & 2 — UDP Telemetry Protocol Specification](joplin://c6bd2c45938246fa9d61776deae9874b)
- **Architecture:** [Architecture & Infrastructure](joplin://c1c3a7b2055642268ab230b95551f470)

---

*Created: 2026-06-08*
*Status: ☐ Pending — tasks not yet started in either repo*

id: 58ccf2b13ac04e619528e1df12dd1e24
parent_id: 0e8e00b432a840628faa4df5bc2068bc
created_time: 2026-06-08T10:49:01.116Z
updated_time: 2026-06-08T10:49:01.116Z
is_conflict: 0
latitude: 0.00000000
longitude: 0.00000000
altitude: 0.0000
author: 
source_url: 
is_todo: 0
todo_due: 0
todo_completed: 0
source: joplin-desktop
source_application: net.cozic.joplin-desktop
application_data: 
order: 1780915741116
user_created_time: 2026-06-08T10:49:01.116Z
user_updated_time: 2026-06-08T10:49:01.116Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 0
type_: 1