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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Physics for educators: `doc/model.md`. Architecture: `doc/implementation-notes.m

- `ModelViewTransformFactory.buildModelViewTransform()` maps real-world units to video pixels from coordinate-system position/rotation and calibration endpoints. Moving axes or calibration **re-expresses** digitized track points so they stay pinned on the video.
- Velocity and acceleration are **finite-difference estimates** from position series — noise amplifies on differentiation.
- Up to 26 tracks (labels A–Z) with independent colors for graphs and table rows.
- Up to 4 concurrent tracks (labels A–Z available; symbols are not reused) with independent colors for graphs and table rows.

## Accessibility

Expand Down
4 changes: 2 additions & 2 deletions doc/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ The key ideas a student should take away:
| Velocity | v_x, v_y, speed | user unit / s | Finite-difference estimate |
| Acceleration | a_x, a_y, \|a\| | user unit / s² | Finite-difference of velocity |

Up to **26 tracks** (labels A–Z) can be recorded simultaneously, each with its own color for graphs
and the data table.
Up to **4 concurrent tracks** (labels drawn from A–Z; symbols are not reused after removal) can be
recorded, each with its own color for graphs and the data table.

## Governing equations

Expand Down
51 changes: 44 additions & 7 deletions src/TrackLabConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* Central repository for numeric constants shared across the application.
* Component-specific constants that are only used within a single file live
* at the top of that file instead.
*
* Units: layout sizes and spacings in pixels (px) unless noted; speeds in
* pixels/second (px/s); dimensionless fractions are marked as such.
*/

// ── Panel styling ─────────────────────────────────────────────────────────────
Expand All @@ -23,8 +26,10 @@ export const CALIB_HALF_LENGTH = 100;

// ── Screen layout offsets ─────────────────────────────────────────────────────
export const CONTROL_PANEL_LEFT_MARGIN = 10; // control panel inset from layout left edge
export const TRACK_LIST_LEFT_SPACING = -100; // offset from video right edge (negative moves panels left)
export const DATA_TABLE_TOP_SPACING = 8; // gap between track list bottom and data table
export const SCREEN_TOP_MARGIN = 10; // inset from layout top edge for control panel and video
export const VIDEO_PLAYER_LEFT_SPACING = 60; // gap between control panel right and video player left
export const DATA_TABLE_TOP_SPACING = 8; // gap between control panel bottom and track list / table
export const KINEMATICS_GRAPH_BOTTOM_MARGIN = 50; // gap between kinematics graph bottom and reset button top
export const RESET_BUTTON_MARGIN = 10; // reset button inset from layout right/bottom edges

// ── Track symbol limits ───────────────────────────────────────────────────────
Expand All @@ -35,6 +40,7 @@ export const TRACK_SYMBOL_LAST_CODE = 90; // ASCII 'Z'

// Maximum number of simultaneous tracks a user may have active.
// Symbols are never reused: removing track B and adding a new one yields E, not B.
// The A–Z palette supports up to 26, but the UI caps concurrent tracks here.
export const MAX_TRACKS = 4;

// ── Model-view transform precision thresholds ─────────────────────────────────
Expand All @@ -43,6 +49,10 @@ export const MAX_TRACKS = 4;
export const MIN_PIXEL_DISTANCE = 1e-6; // minimum pixel distance between calibration points
export const MIN_CALIB_DISTANCE = 1e-9; // minimum real-world calibration distance

// ── Numeric display / export precision ────────────────────────────────────────
// Shared decimal places for table cells and CSV export of track coordinates.
export const DATA_DECIMAL_PLACES = 4;

// ── Button sizing ──────────────────────────────────────────────────────────────
// Shared margins applied to every RectangularPushButton (and TextPushButton) in
// the application, so all rectangular push buttons have a uniform appearance.
Expand Down Expand Up @@ -76,6 +86,10 @@ export const DIGITIZING_DIM_OPACITY = 0.35;
export const OVERLAY_DRAG_SPEED = 200; // normal keyboard drag
export const OVERLAY_SHIFT_DRAG_SPEED = 40; // shift-key fine adjustment

// Graph pan uses the same normal speed, but a slightly faster shift speed so
// fine panning remains usable across large model ranges.
export const GRAPH_PAN_SHIFT_DRAG_SPEED = 60;

// ── Shared overlay label panel styling ────────────────────────────────────────
// Label panels that float near the measuring tape midpoint and angle tool vertex.
export const LABEL_PANEL_CORNER_RADIUS = 4;
Expand All @@ -86,18 +100,41 @@ export const LABEL_PANEL_SCALE = 0.8;
// ── Shared overlay endpoint touch dilation ────────────────────────────────────
export const OVERLAY_TOUCH_DILATION = 12;

// ── Video panel drag & resize ──────────────────────────────────────────────────
// Height of the thin header bar between the source controls and video content.
// This bar is the drag target for moving the video panel.
export const PANEL_HEADER_HEIGHT = 6;
// ── Shared resize-handle geometry ─────────────────────────────────────────────
// Corner resize knobs used by the data table and kinematics graph panels.
export const RESIZE_HANDLE_SIZE = 12; // width and height of each corner handle (px)
export const RESIZE_HANDLE_OFFSET = -6; // centers the handle on each corner
export const RESIZE_HANDLE_TOUCH_DILATION = 6;
export const RESIZE_HANDLE_MOUSE_DILATION = 4;
export const RESIZE_HANDLE_CORNER_RADIUS = 2;
export const RESIZE_HANDLE_LINE_WIDTH = 2;

// ── Control panel icon and layout ─────────────────────────────────────────────
// Shared by ControlPanel, MeasurementToolsPanel, and InfoDialogNode.
// Shared by ControlPanel, MeasurementToolsPanel, InfoDialogNode, and TrackLabIcons.
export const CONTROL_ICON_SIZE = 20;
export const CONTROL_PANEL_ROWS_SPACING = 12;
export const CONTROL_PANEL_X_MARGIN = 12;
export const CONTROL_PANEL_Y_MARGIN = 12;

// Geometry for the shared control / info-dialog icons (axes, calibration,
// magnifier, tracking). Sizes are in pixels; fractions are of CONTROL_ICON_SIZE.
export const CONTROL_ICON_ARROW_HEAD_SIZE = 5;
export const CONTROL_ICON_ARROW_TAIL_WIDTH = 1.5;
export const CONTROL_ICON_LINE_WIDTH_THICK = 1.5;
export const CONTROL_ICON_LINE_WIDTH_THIN = 1;
export const CONTROL_ICON_LINE_WIDTH_MAGNIFIER = 2;
export const CONTROL_ICON_DOT_RADIUS = 3;
export const CONTROL_ICON_CENTER_DOT_RADIUS = 2;
export const CONTROL_ICON_LINE_DASH: readonly number[] = [3, 2];
export const CONTROL_ICON_ORIGIN_FRACTION = 0.7;
export const CONTROL_ICON_X_ARROW_END_FRACTION = 0.85;
export const CONTROL_ICON_Y_ARROW_END_FRACTION = 0.05;
export const CONTROL_ICON_HALF_FRACTION = 0.4;
export const CONTROL_ICON_CENTER_FRACTION = 0.5;
export const CONTROL_ICON_MAGNIFIER_RADIUS_FRACTION = 0.32;
export const CONTROL_ICON_TRACKING_RADIUS_FRACTION = 0.35;
export const CONTROL_ICON_TRACKING_GAP_FRACTION = 0.15;

// ── Graph zoom ─────────────────────────────────────────────────────────────
// Multiplicative zoom step applied by wheel ticks and the zoom-in/zoom-out
// buttons on the configurable graph. A value of 1.1 zooms by 10% per step.
Expand Down
146 changes: 145 additions & 1 deletion src/TrackLabIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,28 @@
*/

import { Shape } from "scenerystack/kite";
import { Line, Node, Path, Rectangle } from "scenerystack/scenery";
import { Circle, Line, Node, Path, Rectangle } from "scenerystack/scenery";
import { ArrowNode } from "scenerystack/scenery-phet";
import TrackLabColors from "./TrackLabColors.js";
import {
CONTROL_ICON_ARROW_HEAD_SIZE,
CONTROL_ICON_ARROW_TAIL_WIDTH,
CONTROL_ICON_CENTER_DOT_RADIUS,
CONTROL_ICON_CENTER_FRACTION,
CONTROL_ICON_DOT_RADIUS,
CONTROL_ICON_HALF_FRACTION,
CONTROL_ICON_LINE_DASH,
CONTROL_ICON_LINE_WIDTH_MAGNIFIER,
CONTROL_ICON_LINE_WIDTH_THICK,
CONTROL_ICON_LINE_WIDTH_THIN,
CONTROL_ICON_MAGNIFIER_RADIUS_FRACTION,
CONTROL_ICON_ORIGIN_FRACTION,
CONTROL_ICON_SIZE,
CONTROL_ICON_TRACKING_GAP_FRACTION,
CONTROL_ICON_TRACKING_RADIUS_FRACTION,
CONTROL_ICON_X_ARROW_END_FRACTION,
CONTROL_ICON_Y_ARROW_END_FRACTION,
} from "./TrackLabConstants.js";

/**
* Download icon: downward arrow with a tray bar.
Expand Down Expand Up @@ -122,3 +142,127 @@ export function makeTrashIcon(): Node {

return new Node({ children: [handle, lid, body, l1, l2, l3] });
}

/**
* Two small XY arrows for the coordinate-system control / info-dialog row.
*/
export function makeAxesIcon(): Node {
const xArrow = new ArrowNode(
0,
CONTROL_ICON_SIZE * CONTROL_ICON_ORIGIN_FRACTION,
CONTROL_ICON_SIZE * CONTROL_ICON_X_ARROW_END_FRACTION,
CONTROL_ICON_SIZE * CONTROL_ICON_ORIGIN_FRACTION,
{
fill: TrackLabColors.axisXColorProperty,
stroke: null,
headWidth: CONTROL_ICON_ARROW_HEAD_SIZE,
headHeight: CONTROL_ICON_ARROW_HEAD_SIZE,
tailWidth: CONTROL_ICON_ARROW_TAIL_WIDTH,
},
);
const yArrow = new ArrowNode(
0,
CONTROL_ICON_SIZE * CONTROL_ICON_ORIGIN_FRACTION,
0,
CONTROL_ICON_SIZE * CONTROL_ICON_Y_ARROW_END_FRACTION,
{
fill: TrackLabColors.axisYColorProperty,
stroke: null,
headWidth: CONTROL_ICON_ARROW_HEAD_SIZE,
headHeight: CONTROL_ICON_ARROW_HEAD_SIZE,
tailWidth: CONTROL_ICON_ARROW_TAIL_WIDTH,
},
);
return new Node({ children: [xArrow, yArrow] });
}

/**
* Two endpoint dots joined by a dashed line — calibration segment glyph.
*/
export function makeCalibrationIcon(): Node {
const cx = CONTROL_ICON_SIZE * CONTROL_ICON_CENTER_FRACTION;
const cy = CONTROL_ICON_SIZE * CONTROL_ICON_CENTER_FRACTION;
const half = CONTROL_ICON_SIZE * CONTROL_ICON_HALF_FRACTION;
const calColor = TrackLabColors.calibrationFillProperty;
return new Node({
children: [
new Line(cx - half, cy, cx + half, cy, {
stroke: calColor,
lineWidth: CONTROL_ICON_LINE_WIDTH_THICK,
lineDash: [...CONTROL_ICON_LINE_DASH],
}),
new Circle(CONTROL_ICON_DOT_RADIUS, { fill: calColor, x: cx - half, y: cy }),
new Circle(CONTROL_ICON_DOT_RADIUS, { fill: calColor, x: cx + half, y: cy }),
],
});
}

/**
* Circle with a diagonal handle — magnifying glass silhouette.
*/
export function makeMagnifyIcon(): Node {
const r = CONTROL_ICON_SIZE * CONTROL_ICON_MAGNIFIER_RADIUS_FRACTION;
const cx = r + 1;
const cy = r + 1;
const gray = TrackLabColors.iconGrayProperty;
return new Node({
children: [
new Circle(r, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THICK,
fill: null,
x: cx,
y: cy,
}),
new Line(
cx + r * CONTROL_ICON_ORIGIN_FRACTION,
cy + r * CONTROL_ICON_ORIGIN_FRACTION,
CONTROL_ICON_SIZE - 1,
CONTROL_ICON_SIZE - 1,
{
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_MAGNIFIER,
},
),
],
});
}

/**
* Crosshair with a small centre dot — tracking / auto-track target.
*/
export function makeTrackingIcon(): Node {
const cx = CONTROL_ICON_SIZE * CONTROL_ICON_CENTER_FRACTION;
const cy = CONTROL_ICON_SIZE * CONTROL_ICON_CENTER_FRACTION;
const r = CONTROL_ICON_SIZE * CONTROL_ICON_TRACKING_RADIUS_FRACTION;
const gap = CONTROL_ICON_SIZE * CONTROL_ICON_TRACKING_GAP_FRACTION;
const gray = TrackLabColors.iconGrayProperty;
return new Node({
children: [
new Circle(r, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THICK,
fill: null,
x: cx,
y: cy,
}),
new Circle(CONTROL_ICON_CENTER_DOT_RADIUS, { fill: gray, x: cx, y: cy }),
new Line(cx, cy - r - gap, cx, cy - gap, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THIN,
}),
new Line(cx, cy + gap, cx, cy + r + gap, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THIN,
}),
new Line(cx - r - gap, cy, cx - gap, cy, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THIN,
}),
new Line(cx + gap, cy, cx + r + gap, cy, {
stroke: gray,
lineWidth: CONTROL_ICON_LINE_WIDTH_THIN,
}),
],
});
}
5 changes: 3 additions & 2 deletions src/track-lab/graph/PanGestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { ChartRectangle, ChartTransform } from "scenerystack/bamboo";
import { Range, type Vector2 } from "scenerystack/dot";
import { DragListener, KeyboardDragListener } from "scenerystack/scenery";
import { GRAPH_PAN_SHIFT_DRAG_SPEED, OVERLAY_DRAG_SPEED } from "../../TrackLabConstants.js";
import TrackLabNamespace from "../../TrackLabNamespace.js";
import type GraphDataManager from "./GraphDataManager.js";
import type { ChartConfig } from "./GraphInteractionHandler.js";
Expand Down Expand Up @@ -110,8 +111,8 @@ export default class PanGestureHandler {
this.chartRectangle.tagName = "div";
this.chartRectangle.addInputListener(
new KeyboardDragListener({
dragSpeed: 200,
shiftDragSpeed: 60,
dragSpeed: OVERLAY_DRAG_SPEED,
shiftDragSpeed: GRAPH_PAN_SHIFT_DRAG_SPEED,
drag: (_event, listener) => {
const xRange = this.chartTransform.modelXRange;
const yRange = this.chartTransform.modelYRange;
Expand Down
57 changes: 38 additions & 19 deletions src/track-lab/graph/ResizeGestureHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ import type { Vector2 } from "scenerystack/dot";
import { type Node, Rectangle, RichDragListener } from "scenerystack/scenery";
import { Tandem } from "scenerystack/tandem";
import TrackLabColors from "../../TrackLabColors.js";
import { OVERLAY_DRAG_SPEED, OVERLAY_SHIFT_DRAG_SPEED } from "../../TrackLabConstants.js";
import {
OVERLAY_DRAG_SPEED,
OVERLAY_SHIFT_DRAG_SPEED,
RESIZE_HANDLE_CORNER_RADIUS,
RESIZE_HANDLE_LINE_WIDTH,
RESIZE_HANDLE_MOUSE_DILATION,
RESIZE_HANDLE_OFFSET,
RESIZE_HANDLE_SIZE,
RESIZE_HANDLE_TOUCH_DILATION,
} from "../../TrackLabConstants.js";
import TrackLabNamespace from "../../TrackLabNamespace.js";
import type { GraphDimensions } from "./GraphInteractionHandler.js";

const HANDLE_SIZE = 12;
const HANDLE_OFFSET = -6; // centers the 12px handle on each corner
const TOUCH_AREA_DILATION = 6;
const MOUSE_AREA_DILATION = 4;
// Minimum graph dimensions enforced while resizing (px).
const MIN_WIDTH = 200;
const MIN_HEIGHT = 150;

Expand Down Expand Up @@ -79,18 +85,26 @@ export default class ResizeGestureHandler {
];

corners.forEach((corner, index) => {
const handle = new Rectangle(corner.x + HANDLE_OFFSET, corner.y + HANDLE_OFFSET, HANDLE_SIZE, HANDLE_SIZE, 2, 2, {
fill: TrackLabColors.controlPanelFillProperty,
stroke: TrackLabColors.controlPanelStrokeProperty,
lineWidth: 2,
cursor: corner.cursor,
tagName: "div",
focusable: true,
accessibleName: cornerA11yNames[index as 0 | 1 | 2 | 3],
});
const handle = new Rectangle(
corner.x + RESIZE_HANDLE_OFFSET,
corner.y + RESIZE_HANDLE_OFFSET,
RESIZE_HANDLE_SIZE,
RESIZE_HANDLE_SIZE,
RESIZE_HANDLE_CORNER_RADIUS,
RESIZE_HANDLE_CORNER_RADIUS,
{
fill: TrackLabColors.controlPanelFillProperty,
stroke: TrackLabColors.controlPanelStrokeProperty,
lineWidth: RESIZE_HANDLE_LINE_WIDTH,
cursor: corner.cursor,
tagName: "div",
focusable: true,
accessibleName: cornerA11yNames[index as 0 | 1 | 2 | 3],
},
);

handle.touchArea = handle.localBounds.dilated(TOUCH_AREA_DILATION);
handle.mouseArea = handle.localBounds.dilated(MOUSE_AREA_DILATION);
handle.touchArea = handle.localBounds.dilated(RESIZE_HANDLE_TOUCH_DILATION);
handle.mouseArea = handle.localBounds.dilated(RESIZE_HANDLE_MOUSE_DILATION);

this.handles.push(handle);
this.attachDragListener(handle, index);
Expand All @@ -114,9 +128,14 @@ export default class ResizeGestureHandler {
this.handles.forEach((handle, index) => {
const corner = corners[index];
if (corner) {
handle.setRect(corner.x + HANDLE_OFFSET, corner.y + HANDLE_OFFSET, HANDLE_SIZE, HANDLE_SIZE);
handle.touchArea = handle.localBounds.dilated(TOUCH_AREA_DILATION);
handle.mouseArea = handle.localBounds.dilated(MOUSE_AREA_DILATION);
handle.setRect(
corner.x + RESIZE_HANDLE_OFFSET,
corner.y + RESIZE_HANDLE_OFFSET,
RESIZE_HANDLE_SIZE,
RESIZE_HANDLE_SIZE,
);
handle.touchArea = handle.localBounds.dilated(RESIZE_HANDLE_TOUCH_DILATION);
handle.mouseArea = handle.localBounds.dilated(RESIZE_HANDLE_MOUSE_DILATION);
}
});
}
Expand Down
Loading
Loading