diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..20644a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,52 @@ +# TrackLab — Claude Code Guide + +## Project overview + +TrackLab is a browser-based physics video analysis tool. Users load a video, place a coordinate system, calibrate real-world distances, and auto-track objects across frames using OpenCV. + +## Where to work + +Most feature development happens in two directories: + +``` +src/screen-name/model/ ← application state +src/screen-name/view/ ← all UI components +``` + +**Model** (`src/screen-name/model/SimModel.ts`) holds every piece of reactive state as Axon `Property` objects (playback position, duration, overlay visibility, model-view transform). If you need a new piece of shared state, add it here. + +**View** (`src/screen-name/view/`) contains all SceneryStack nodes. Key files: + +| File | Responsibility | +|------|----------------| +| `VideoPlayerNode.ts` | Video element, playback controls, scrubber, time/frame display | +| `SimScreenView.ts` | Root layout, model-view transform computation | +| `CoordinateSystemNode.ts` | Draggable/rotatable axes overlay | +| `CalibrationToolNode.ts` | Reference distance calibration tool | +| `AutoTrackerNode.ts` | Tracking selection box and trail rendering | +| `ControlPanel.ts` | Left-side toggle panel | +| `WebcamPanel.ts` | Webcam recording dialog | + +The other source directories are less frequently modified: + +- `src/tracking/` — OpenCV template-matching pipeline (touch only for tracking algorithm changes) +- `src/i18n/` — Localization strings (English and French) +- `src/` root files (`main.ts`, `init.ts`, `TrackLabColors.ts`, etc.) — bootstrapping and global config + +## Development commands + +```bash +npm start # start Vite dev server at http://localhost:5173 +npm run build # type-check (tsc) then bundle (vite build) +npm run check # TypeScript type check only +npm run lint # Biome lint +npm run format # Biome format +npm run fix # fix lint + format issues together +``` + +## Architecture notes + +- **Reactive state**: all model values are Axon `Property` / `BooleanProperty` / `DerivedProperty`. Views observe properties and update automatically — avoid manual imperative sync. +- **Model-view transform**: `SimScreenView` computes a `modelViewTransformProperty` from the coordinate system pose and calibration data. Use it to convert between real-world units and video-pixel coordinates. +- **Frame rate**: the codebase assumes 30 fps (`FRAME_DURATION = 1/30` in `VideoPlayerNode.ts`). Frame stepping and the frame counter both rely on this constant. +- **SceneryStack layout**: use `HBox` / `VBox` for rows and columns. Prefer `align: 'center'` and explicit `spacing` values. Do not set absolute pixel positions unless absolutely necessary. diff --git a/src/screen-name/view/VideoPlayerNode.ts b/src/screen-name/view/VideoPlayerNode.ts index c96a02e..781c762 100644 --- a/src/screen-name/view/VideoPlayerNode.ts +++ b/src/screen-name/view/VideoPlayerNode.ts @@ -155,6 +155,38 @@ export class VideoPlayerNode extends Node { } } ); + // ── Time and frame info display ──────────────────────────────────────── + const formatDuration = ( seconds: number ): string => { + if ( !Number.isFinite( seconds ) || seconds <= 0 ) return '0:00'; + const mins = Math.floor( seconds / 60 ); + const secs = Math.floor( seconds % 60 ); + return `${ mins }:${ String( secs ).padStart( 2, '0' ) }`; + }; + + const totalTimeTextProperty = new DerivedProperty( + [ model.durationProperty ], + ( duration: number ) => formatDuration( duration ) + ); + + const frameCountTextProperty = new DerivedProperty( + [ model.currentTimeProperty, model.durationProperty ], + ( time: number, duration: number ) => { + if ( duration <= 0 ) return '0/0'; + const current = Math.round( time / FRAME_DURATION ); + const total = Math.round( duration / FRAME_DURATION ); + return `${ current }/${ total }`; + } + ); + + const totalTimeLabel = new Text( totalTimeTextProperty, { font: LABEL_FONT } ); + const frameCountLabel = new Text( frameCountTextProperty, { font: LABEL_FONT } ); + + const infoDisplay = new VBox( { + children: [ totalTimeLabel, frameCountLabel ], + spacing: 2, + align: 'left', + } ); + // ── Video source ComboBox ───────────────────────────────────────────── const selectedVideoProperty = new Property( null ); @@ -219,7 +251,7 @@ export class VideoPlayerNode extends Node { } ); const controlsRow = new HBox( { - children: [ timeControlNode, scrubber ], + children: [ infoDisplay, timeControlNode, scrubber ], spacing: 16, align: 'center', } );