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
35 changes: 35 additions & 0 deletions src/TrackLabConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* TrackLabConstants.ts
*
* 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.
*/

// ── Panel styling ─────────────────────────────────────────────────────────────
// Shared corner radius used by the main side panels.
export const PANEL_CORNER_RADIUS = 8;

// ── Screen layout offsets ─────────────────────────────────────────────────────
// SceneryStack's ScreenView.DEFAULT_LAYOUT_BOUNDS = Bounds2(0, 0, 1024, 618).
export const VIDEO_PLAYER_Y_OFFSET = -20; // video center offset below layout center
export const CONTROL_PANEL_LEFT_MARGIN = 10; // control panel inset from layout left edge
export const TRACK_LIST_LEFT_SPACING = 12; // gap between video right edge and track list
export const DATA_TABLE_TOP_SPACING = 8; // gap between track list bottom and data table
export const RESET_BUTTON_MARGIN = 10; // reset button inset from layout right/bottom edges

// ── Track symbol limits ───────────────────────────────────────────────────────
// Tracks are labelled A–Z using ASCII codes. These bounds are used both when
// creating a new track and when resetting the session.
export const TRACK_SYMBOL_FIRST_CODE = 65; // ASCII 'A'
export const TRACK_SYMBOL_LAST_CODE = 90; // ASCII 'Z'

// ── Model-view transform precision thresholds ─────────────────────────────────
// Guards against degenerate (zero-length) calibration or pixel distances that
// would produce a singular transform matrix.
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

// ── Webcam panel ──────────────────────────────────────────────────────────────
export const WEBCAM_PREVIEW_WIDTH = 480; // width of the preview and review video elements
export const WEBCAM_PREVIEW_HEIGHT = 270; // height of the preview and review video elements
20 changes: 14 additions & 6 deletions src/screen-name/model/SimModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
} from "scenerystack/axon";
import { Matrix3, Range, Transform3, Vector2 } from "scenerystack/dot";
import { TRACK_COLORS } from "../../TrackLabColors.js";
import {
MIN_CALIB_DISTANCE,
MIN_PIXEL_DISTANCE,
TRACK_SYMBOL_FIRST_CODE,
TRACK_SYMBOL_LAST_CODE,
} from "../../TrackLabConstants.js";
import { OpenCVTracker } from "../../tracking/OpenCVTracker.js";
import type { Track, TrackPoint } from "./Track.js";

Expand Down Expand Up @@ -66,7 +72,7 @@ function buildModelViewTransform(
dist: number,
): Transform3 {
const pixelDist = p1.distance(p2);
if (pixelDist < 1e-6 || dist < 1e-9) {
if (pixelDist < MIN_PIXEL_DISTANCE || dist < MIN_CALIB_DISTANCE) {
return new Transform3(Matrix3.IDENTITY);
}
const s = pixelDist / dist; // pixels per model unit
Expand Down Expand Up @@ -150,15 +156,17 @@ export class SimModel {
// after a track is removed. Stable, unique symbols matter for data export
// and user recognition: re-issuing "A" to a new track after the original "A"
// is deleted would be confusing. The practical limit is 26 tracks per session.
private nextSymbolCode = 65; // ASCII code for 'A'
private nextSymbolCode = TRACK_SYMBOL_FIRST_CODE;

public addTrack(): void {
if (this.nextSymbolCode > 90) return; // 'Z' is the last allowed symbol
if (this.nextSymbolCode > TRACK_SYMBOL_LAST_CODE) return; // 'Z' is the last allowed symbol
const symbol = String.fromCharCode(this.nextSymbolCode);
const color =
TRACK_COLORS[(this.nextSymbolCode - 65) % TRACK_COLORS.length];
TRACK_COLORS[
(this.nextSymbolCode - TRACK_SYMBOL_FIRST_CODE) % TRACK_COLORS.length
];
this.nextSymbolCode++;
this.canAddTrackProperty.value = this.nextSymbolCode <= 90;
this.canAddTrackProperty.value = this.nextSymbolCode <= TRACK_SYMBOL_LAST_CODE;

const track: Track = {
id: `track-${symbol}`,
Expand Down Expand Up @@ -216,7 +224,7 @@ export class SimModel {
this.tracksProperty.value = [];
this.activeTrackIdProperty.value = null;
this.canAddTrackProperty.value = true;
this.nextSymbolCode = 65;
this.nextSymbolCode = TRACK_SYMBOL_FIRST_CODE;
this.tracker.dispose();
}

Expand Down
25 changes: 16 additions & 9 deletions src/screen-name/view/AutoTrackerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import { type SimModel, VIDEO_HEIGHT, VIDEO_WIDTH } from "../model/SimModel.js";

const MAX_TRAIL = 150;
const CROSSHAIR_SIZE = 16;
const HINT_FONT_SIZE = 15;
const SELECTION_LINE_WIDTH = 2;
const SELECTION_LINE_DASH: number[] = [6, 3];
const CROSSHAIR_LINE_WIDTH = 2;
const CROSSHAIR_CIRCLE_RADIUS = 6; // small filled circle at crosshair centre
const MIN_REGION_SIZE = 4; // minimum pixel width/height to begin tracking
const TRAIL_DOT_RADIUS = 3; // radius of each past-position dot in the trail

/**
* Transparent SceneryStack overlay that sits directly on top of the video element.
Expand Down Expand Up @@ -72,7 +79,7 @@ export class AutoTrackerNode extends Node {

// ── Hint text ────────────────────────────────────────────────────────
this.hintText = new Text("Drag on video to select object to track", {
font: new PhetFont({ size: 15, weight: "bold" }),
font: new PhetFont({ size: HINT_FONT_SIZE, weight: "bold" }),
fill: TrackLabColors.trackerHintFillProperty,
});
this.hintText.center = new Vector2(VIDEO_WIDTH / 2, VIDEO_HEIGHT / 2);
Expand All @@ -81,8 +88,8 @@ export class AutoTrackerNode extends Node {
// ── Selection rectangle ───────────────────────────────────────────────
this.selectionRect = new Rectangle(0, 0, 0, 0, {
stroke: TrackLabColors.trackerSelectionStrokeProperty,
lineWidth: 2,
lineDash: [6, 3],
lineWidth: SELECTION_LINE_WIDTH,
lineDash: SELECTION_LINE_DASH,
fill: TrackLabColors.trackerSelectionFillProperty,
visible: false,
});
Expand All @@ -98,17 +105,17 @@ export class AutoTrackerNode extends Node {
const crosshairStroke = TrackLabColors.trackerCrosshairStrokeProperty;
this.crosshairH = new Line(-CROSSHAIR_SIZE, 0, CROSSHAIR_SIZE, 0, {
stroke: crosshairStroke,
lineWidth: 2,
lineWidth: CROSSHAIR_LINE_WIDTH,
visible: false,
});
this.crosshairV = new Line(0, -CROSSHAIR_SIZE, 0, CROSSHAIR_SIZE, {
stroke: crosshairStroke,
lineWidth: 2,
lineWidth: CROSSHAIR_LINE_WIDTH,
visible: false,
});
this.crosshairCircle = new Path(Shape.circle(0, 0, 6), {
this.crosshairCircle = new Path(Shape.circle(0, 0, CROSSHAIR_CIRCLE_RADIUS), {
stroke: crosshairStroke,
lineWidth: 2,
lineWidth: CROSSHAIR_LINE_WIDTH,
visible: false,
});
this.addChild(this.trailPath);
Expand Down Expand Up @@ -158,7 +165,7 @@ export class AutoTrackerNode extends Node {
h: Math.abs(p.y - this.selStart.y),
};

if (region.w > 4 && region.h > 4) {
if (region.w > MIN_REGION_SIZE && region.h > MIN_REGION_SIZE) {
// initFromVideo is async (loads WASM on first call); tracking begins
// automatically once `ready` becomes true.
this.model.tracker.initFromVideo(videoElement, region).catch((err) => {
Expand Down Expand Up @@ -231,7 +238,7 @@ export class AutoTrackerNode extends Node {
private updateTrackerVisuals(pt: { x: number; y: number }): void {
const shape = new Shape();
for (const p of this.trail) {
shape.circle(p.x, p.y, 3);
shape.circle(p.x, p.y, TRAIL_DOT_RADIUS);
}
this.trailPath.shape = shape;
this.trailPath.visible = true;
Expand Down
42 changes: 30 additions & 12 deletions src/screen-name/view/CalibrationToolNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ import { CALIBRATION_UNITS } from "../model/SimModel.js";

const FONT = new PhetFont(14);
const ENDPOINT_RADIUS = 8;
const LINE_WIDTH = 2;
const LINE_DASH: number[] = [8, 4];
const ENDPOINT_LINE_WIDTH = 1.5;
const MAX_KEYPAD_DECIMALS = 4;
const MIDPOINT_PANEL_SCALE = 0.5;
const MIDPOINT_PANEL_CORNER_RADIUS = 6;
const MIDPOINT_PANEL_X_MARGIN = 8;
const MIDPOINT_PANEL_Y_MARGIN = 6;
const MIDPOINT_PANEL_SPACING = 8;
const MIDPOINT_Y_OFFSET = 12; // pixels above midpoint where the panel sits
const ENDPOINT_DRAG_SPEED = 200; // pixels/s for normal keyboard drag
const ENDPOINT_SHIFT_DRAG_SPEED = 40; // pixels/s for shift-key keyboard drag

export class CalibrationToolNode extends Node {
public constructor(
Expand All @@ -36,8 +48,8 @@ export class CalibrationToolNode extends Node {
// ── Connecting line ────────────────────────────────────────────────────
const calibrationLine = new Line(0, 0, 0, 0, {
stroke: TrackLabColors.calibrationStrokeProperty,
lineWidth: 2,
lineDash: [8, 4],
lineWidth: LINE_WIDTH,
lineDash: LINE_DASH,
});
this.addChild(calibrationLine);

Expand All @@ -46,7 +58,7 @@ export class CalibrationToolNode extends Node {
new Circle(ENDPOINT_RADIUS, {
fill: TrackLabColors.calibrationFillProperty,
stroke: TrackLabColors.textOnDarkProperty,
lineWidth: 1.5,
lineWidth: ENDPOINT_LINE_WIDTH,
cursor: "crosshair",
tagName: "div",
focusable: true,
Expand All @@ -62,7 +74,7 @@ export class CalibrationToolNode extends Node {
keypadLayout: Keypad.PositiveDecimalLayout,
keypadOptions: {
accumulatorOptions: {
maxDigitsRightOfMantissa: 4,
maxDigitsRightOfMantissa: MAX_KEYPAD_DECIMALS,
},
},
tandem: Tandem.OPT_OUT,
Expand Down Expand Up @@ -117,18 +129,18 @@ export class CalibrationToolNode extends Node {
const midpointPanel = new Panel(
new HBox({
children: [distanceButton, unitComboBox],
spacing: 8,
spacing: MIDPOINT_PANEL_SPACING,
align: "center",
}),
{
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeLightProperty,
cornerRadius: 6,
xMargin: 8,
yMargin: 6,
cornerRadius: MIDPOINT_PANEL_CORNER_RADIUS,
xMargin: MIDPOINT_PANEL_X_MARGIN,
yMargin: MIDPOINT_PANEL_Y_MARGIN,
},
);
midpointPanel.setScaleMagnitude(0.5);
midpointPanel.setScaleMagnitude(MIDPOINT_PANEL_SCALE);
this.addChild(midpointPanel);

// ── Update geometry when endpoints move ───────────────────────────────
Expand All @@ -140,7 +152,7 @@ export class CalibrationToolNode extends Node {
endpoint2.translation = p2;
const mid = p1.blend(p2, 0.5);
midpointPanel.centerX = mid.x;
midpointPanel.bottom = mid.y - 12;
midpointPanel.bottom = mid.y - MIDPOINT_Y_OFFSET;
};
model.calibPoint1Property.link(updateGeometry);
model.calibPoint2Property.link(updateGeometry);
Expand All @@ -149,14 +161,20 @@ export class CalibrationToolNode extends Node {
endpoint1.addInputListener(
new RichDragListener({
positionProperty: model.calibPoint1Property,
keyboardDragListenerOptions: { dragSpeed: 200, shiftDragSpeed: 40 },
keyboardDragListenerOptions: {
dragSpeed: ENDPOINT_DRAG_SPEED,
shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED,
},
tandem: Tandem.OPT_OUT,
}),
);
endpoint2.addInputListener(
new RichDragListener({
positionProperty: model.calibPoint2Property,
keyboardDragListenerOptions: { dragSpeed: 200, shiftDragSpeed: 40 },
keyboardDragListenerOptions: {
dragSpeed: ENDPOINT_DRAG_SPEED,
shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED,
},
tandem: Tandem.OPT_OUT,
}),
);
Expand Down
Loading