Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/TrackLabColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export const TRACK_COLORS = [
new Color(178, 132, 190), // Z – lavender
];

/** Returns the Color for a track, guaranteed non-null (falls back to orange if index is out of range). */
export function getTrackColor(colorIndex: number): Color {
return TRACK_COLORS[colorIndex % TRACK_COLORS.length] ?? TRACK_COLORS[0] ?? new Color(255, 140, 0);
}

/**
* Color definitions for the TrackLab Simulations
*/
Expand Down
7 changes: 4 additions & 3 deletions src/screen-name/graph/GraphDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,15 @@ export default class GraphDataManager {
/**
* Calculate appropriate tick spacing for a given range.
* This is a static utility method that doesn't depend on instance state.
*
* @param rangeLength - The total span of the axis.
* @param targetTicks - Desired approximate number of major ticks (default 5).
*/
public static calculateTickSpacing(rangeLength: number): number {
public static calculateTickSpacing(rangeLength: number, targetTicks = 5): number {
if (!Number.isFinite(rangeLength) || rangeLength <= 0) {
return 1;
}

// Target ~5-6 ticks to avoid too many grid lines
const targetTicks = 5;
const roughSpacing = rangeLength / targetTicks;

if (roughSpacing < 1e-10) {
Expand Down
16 changes: 8 additions & 8 deletions src/screen-name/graph/kinematics-plottable-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,29 +64,29 @@ export function buildKinematicsPlottableGroups(model: SimModel): KinematicsPlott
// ── Position ──────────────────────────────────────────────────────────
position: [
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("x", model.distanceUnitProperty, (pt) => pt["x"] ?? 0),
createPlottableProperty("x", model.overlayTools.calibUnitProperty, (pt) => pt["x"] ?? 0),
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("y", model.distanceUnitProperty, (pt) => pt["y"] ?? 0),
createPlottableProperty("y", model.overlayTools.calibUnitProperty, (pt) => pt["y"] ?? 0),
],

// ── Velocity ──────────────────────────────────────────────────────────
velocity: [
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("vx", model.velocityUnitProperty, (pt) => pt["vx"] ?? 0),
createPlottableProperty("vx", model.overlayTools.velocityUnitProperty, (pt) => pt["vx"] ?? 0),
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("vy", model.velocityUnitProperty, (pt) => pt["vy"] ?? 0),
createPlottableProperty("vy", model.overlayTools.velocityUnitProperty, (pt) => pt["vy"] ?? 0),
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("speed", model.velocityUnitProperty, (pt) => pt["speed"] ?? 0),
createPlottableProperty("speed", model.overlayTools.velocityUnitProperty, (pt) => pt["speed"] ?? 0),
],

// ── Acceleration ──────────────────────────────────────────────────────
acceleration: [
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("ax", model.accelerationUnitProperty, (pt) => pt["ax"] ?? 0),
createPlottableProperty("ax", model.overlayTools.accelerationUnitProperty, (pt) => pt["ax"] ?? 0),
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("ay", model.accelerationUnitProperty, (pt) => pt["ay"] ?? 0),
createPlottableProperty("ay", model.overlayTools.accelerationUnitProperty, (pt) => pt["ay"] ?? 0),
// biome-ignore lint/complexity/useLiteralKeys: TypeScript requires bracket notation
createPlottableProperty("|a|", model.accelerationUnitProperty, (pt) => pt["aMag"] ?? 0),
createPlottableProperty("|a|", model.overlayTools.accelerationUnitProperty, (pt) => pt["aMag"] ?? 0),
],
};
}
2 changes: 1 addition & 1 deletion src/screen-name/model/KinematicsComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function computeTrackKinematics(track: Track): TrackKinematics {
return {
id: track.id,
symbol: track.symbol,
color: track.color,
colorIndex: track.colorIndex,
points: kinematicPoints,
};
}
134 changes: 134 additions & 0 deletions src/screen-name/model/OverlayToolsModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* OverlayToolsModel.ts
*
* Reactive state for all measurement and coordinate-system overlay tools:
* axes, calibration ruler, measuring tape, angle tool, and their derived
* model-view transform. Extracted from SimModel to keep video-playback and
* track-management state separate from geometric tool state.
*/

import { BooleanProperty, DerivedProperty, NumberProperty, Property, type TReadOnlyProperty } from "scenerystack/axon";
import { Range, type Transform3, Vector2 } from "scenerystack/dot";
import { CALIB_HALF_LENGTH, VIDEO_HEIGHT, VIDEO_WIDTH } from "../../TrackLabConstants.js";
import { buildModelViewTransform } from "./ModelViewTransformFactory.js";

// ── Calibration unit type ──────────────────────────────────────────────────
export const CALIBRATION_UNITS = ["mm", "cm", "m", "km", "in", "ft"] as const;
export type CalibrationUnit = (typeof CALIBRATION_UNITS)[number];
export const CALIBRATION_DISTANCE_RANGE = new Range(0.001, 100000);

// ── Video-local coordinate helpers ──────────────────────────────────────────
// All tool positions are in video-local coordinates: (0,0) = top-left of the
// video element, (VIDEO_WIDTH, VIDEO_HEIGHT) = bottom-right.
const VIDEO_LOCAL_CENTER_X = VIDEO_WIDTH / 2;
const VIDEO_LOCAL_CENTER_Y = VIDEO_HEIGHT / 2;

// ── Initial tool positions (video-local coordinates) ───────────────────────
const COORD_ORIGIN_INITIAL = new Vector2(VIDEO_WIDTH / 4, VIDEO_LOCAL_CENTER_Y);
const CALIB_CENTER_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X, (VIDEO_HEIGHT * 3) / 4);
const CALIB_P1_INITIAL = CALIB_CENTER_INITIAL.plusXY(-CALIB_HALF_LENGTH, 0);
const CALIB_P2_INITIAL = CALIB_CENTER_INITIAL.plusXY(CALIB_HALF_LENGTH, 0);

// ── Initial measuring tape positions (video-local coordinates) ─────────────
const TAPE_P1_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X - 90, VIDEO_LOCAL_CENTER_Y + 100);
const TAPE_P2_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X + 90, VIDEO_LOCAL_CENTER_Y + 100);

// ── Initial angle tool positions (video-local coordinates) ─────────────────
const ANGLE_VERTEX_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X, VIDEO_LOCAL_CENTER_Y + 80);
const ANGLE_ARM1_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X + 90, VIDEO_LOCAL_CENTER_Y + 20);
const ANGLE_ARM2_INITIAL = new Vector2(VIDEO_LOCAL_CENTER_X + 90, VIDEO_LOCAL_CENTER_Y + 140);

// ── Bounds for clamping the coordinate-system origin ─────────────────────────
// The origin must stay within the video area so the axes are always visible.
const COORD_ORIGIN_BOUNDS_MIN_X = 0;
const COORD_ORIGIN_BOUNDS_MAX_X = VIDEO_WIDTH;
const COORD_ORIGIN_BOUNDS_MIN_Y = 0;
const COORD_ORIGIN_BOUNDS_MAX_Y = VIDEO_HEIGHT;

/**
* Owns all reactive state for the geometric overlay tools: coordinate system,
* calibration ruler, measuring tape, and angle tool. Also provides the
* derived model-view transform and unit-string properties used for display.
*/
export class OverlayToolsModel {
// ── Overlay visibility ────────────────────────────────────────────────────
public readonly axesVisibleProperty = new BooleanProperty(true);
public readonly calibrationVisibleProperty = new BooleanProperty(true);
public readonly magnifyVideoProperty = new BooleanProperty(false);
public readonly autoTrackingProperty = new BooleanProperty(false);
public readonly measuringTapeVisibleProperty = new BooleanProperty(false);
public readonly angleToolVisibleProperty = new BooleanProperty(false);

// ── Measuring tape endpoint positions (video-local coordinates) ────────────
public readonly tapPoint1Property = new Property<Vector2>(TAPE_P1_INITIAL.copy());
public readonly tapPoint2Property = new Property<Vector2>(TAPE_P2_INITIAL.copy());

// ── Angle tool positions (video-local coordinates) ─────────────────────────
public readonly angleVertexProperty = new Property<Vector2>(ANGLE_VERTEX_INITIAL.copy());
public readonly angleArm1Property = new Property<Vector2>(ANGLE_ARM1_INITIAL.copy());
public readonly angleArm2Property = new Property<Vector2>(ANGLE_ARM2_INITIAL.copy());

// ── Coordinate system tool state (video-local coordinates) ────────────────
public readonly coordOriginProperty = new Property<Vector2>(COORD_ORIGIN_INITIAL.copy());
public readonly coordAngleProperty = new NumberProperty(0);

// ── Calibration tool state ─────────────────────────────────────────────────
public readonly calibPoint1Property = new Property<Vector2>(CALIB_P1_INITIAL.copy());
public readonly calibPoint2Property = new Property<Vector2>(CALIB_P2_INITIAL.copy());
public readonly calibDistanceProperty = new NumberProperty(1, {
range: CALIBRATION_DISTANCE_RANGE,
});
public readonly calibUnitProperty = new Property<CalibrationUnit>("m");

// ── Derived unit strings (for display in graphs and tables) ───────────────
public readonly velocityUnitProperty: TReadOnlyProperty<string> = new DerivedProperty(
[this.calibUnitProperty],
(unit) => `${unit}/s`,
);
public readonly accelerationUnitProperty: TReadOnlyProperty<string> = new DerivedProperty(
[this.calibUnitProperty],
(unit) => `${unit}/s²`,
);

// ── Model-view transform (derived; the view never writes to this) ─────────
public readonly modelViewTransformProperty: TReadOnlyProperty<Transform3> = new DerivedProperty(
[
this.coordOriginProperty,
this.coordAngleProperty,
this.calibPoint1Property,
this.calibPoint2Property,
this.calibDistanceProperty,
],
(origin, angle, p1, p2, dist) => buildModelViewTransform(origin, angle, p1, p2, dist),
);

/**
* Clamps a position to keep the coordinate system origin within video bounds.
* Used by drag listeners to constrain the origin to the visible video area.
*/
public clampCoordOrigin(pos: Vector2): Vector2 {
const clampedX = Math.max(COORD_ORIGIN_BOUNDS_MIN_X, Math.min(COORD_ORIGIN_BOUNDS_MAX_X, pos.x));
const clampedY = Math.max(COORD_ORIGIN_BOUNDS_MIN_Y, Math.min(COORD_ORIGIN_BOUNDS_MAX_Y, pos.y));
return new Vector2(clampedX, clampedY);
}

public reset(): void {
this.axesVisibleProperty.reset();
this.calibrationVisibleProperty.reset();
this.magnifyVideoProperty.reset();
this.autoTrackingProperty.reset();
this.measuringTapeVisibleProperty.reset();
this.angleToolVisibleProperty.reset();
this.tapPoint1Property.reset();
this.tapPoint2Property.reset();
this.angleVertexProperty.reset();
this.angleArm1Property.reset();
this.angleArm2Property.reset();
this.coordOriginProperty.reset();
this.coordAngleProperty.reset();
this.calibPoint1Property.reset();
this.calibPoint2Property.reset();
this.calibDistanceProperty.reset();
this.calibUnitProperty.reset();
}
}
Loading