diff --git a/CLAUDE.md b/CLAUDE.md index 6592490..1728232 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/doc/model.md b/doc/model.md index 36a5a3f..7b703c6 100644 --- a/doc/model.md +++ b/doc/model.md @@ -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 diff --git a/src/TrackLabConstants.ts b/src/TrackLabConstants.ts index 1e958d4..235a626 100644 --- a/src/TrackLabConstants.ts +++ b/src/TrackLabConstants.ts @@ -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 ───────────────────────────────────────────────────────────── @@ -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 ─────────────────────────────────────────────────────── @@ -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 ───────────────────────────────── @@ -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. @@ -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; @@ -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. diff --git a/src/TrackLabIcons.ts b/src/TrackLabIcons.ts index 412747e..c7acfad 100644 --- a/src/TrackLabIcons.ts +++ b/src/TrackLabIcons.ts @@ -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. @@ -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, + }), + ], + }); +} diff --git a/src/track-lab/graph/PanGestureHandler.ts b/src/track-lab/graph/PanGestureHandler.ts index 4d867a5..a68f473 100644 --- a/src/track-lab/graph/PanGestureHandler.ts +++ b/src/track-lab/graph/PanGestureHandler.ts @@ -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"; @@ -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; diff --git a/src/track-lab/graph/ResizeGestureHandler.ts b/src/track-lab/graph/ResizeGestureHandler.ts index b49af22..300cb4d 100644 --- a/src/track-lab/graph/ResizeGestureHandler.ts +++ b/src/track-lab/graph/ResizeGestureHandler.ts @@ -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; @@ -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); @@ -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); } }); } diff --git a/src/track-lab/model/TrackExporter.ts b/src/track-lab/model/TrackExporter.ts index 4beaa87..f14f93b 100644 --- a/src/track-lab/model/TrackExporter.ts +++ b/src/track-lab/model/TrackExporter.ts @@ -7,10 +7,9 @@ */ import { toFixed } from "scenerystack/dot"; +import { DATA_DECIMAL_PLACES } from "../../TrackLabConstants.js"; import type { Track } from "./Track.js"; -const CSV_DECIMAL_PLACES = 4; - export type DataRow = { frame: number; time: number; @@ -61,11 +60,11 @@ export function generateCsv(tracks: readonly Track[], unit: string, labels: CsvL const lines = [headers.join(",")]; for (const row of dataRows) { - const cells: string[] = [String(row.frame), toFixed(row.time, CSV_DECIMAL_PLACES)]; + const cells: string[] = [String(row.frame), toFixed(row.time, DATA_DECIMAL_PLACES)]; for (const track of tracks) { const val = row.values.get(track.id); if (val) { - cells.push(toFixed(val.x, CSV_DECIMAL_PLACES), toFixed(val.y, CSV_DECIMAL_PLACES)); + cells.push(toFixed(val.x, DATA_DECIMAL_PLACES), toFixed(val.y, DATA_DECIMAL_PLACES)); } else { cells.push("", ""); } diff --git a/src/track-lab/view/ControlPanel.ts b/src/track-lab/view/ControlPanel.ts index 7c3442a..8774bc1 100644 --- a/src/track-lab/view/ControlPanel.ts +++ b/src/track-lab/view/ControlPanel.ts @@ -5,159 +5,21 @@ * (coordinate system, calibration, auto-tracker, digitizing). */ -import { Circle, Line, Node, VBox } from "scenerystack/scenery"; -import { ArrowNode } from "scenerystack/scenery-phet"; +import { type Node, VBox } from "scenerystack/scenery"; import { Checkbox, Panel } from "scenerystack/sun"; import { StringManager } from "../../i18n/StringManager.js"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; import TrackLabColors from "../../TrackLabColors.js"; import { - CONTROL_ICON_SIZE, CONTROL_PANEL_ROWS_SPACING, CONTROL_PANEL_X_MARGIN, CONTROL_PANEL_Y_MARGIN, PANEL_CORNER_RADIUS, } from "../../TrackLabConstants.js"; +import { makeAxesIcon, makeCalibrationIcon, makeMagnifyIcon, makeTrackingIcon } from "../../TrackLabIcons.js"; import TrackLabNamespace from "../../TrackLabNamespace.js"; import type { OverlayToolsModel } from "../model/OverlayToolsModel.js"; -const ICON_ARROW_HEAD_SIZE = 5; // headWidth and headHeight for icon arrows -const ICON_ARROW_TAIL_WIDTH = 1.5; -const ICON_LINE_WIDTH_THICK = 1.5; -const ICON_LINE_WIDTH_THIN = 1; -const ICON_LINE_WIDTH_MAGNIFIER = 2; // handle line in magnifier icon -const ICON_DOT_RADIUS = 3; // calibration endpoint dots -const ICON_CENTER_DOT_RADIUS = 2; // centre dot in tracking icon -const ICON_LINE_DASH: number[] = [3, 2]; - -// ── Icon layout fractions ───────────────────────────────────────────────────── -const ICON_ORIGIN_FRACTION = 0.7; // where axes originate (fraction of CONTROL_ICON_SIZE) -const ICON_X_ARROW_END_FRACTION = 0.85; -const ICON_Y_ARROW_END_FRACTION = 0.05; -const ICON_HALF_FRACTION = 0.4; -const ICON_CENTER_FRACTION = 0.5; -const ICON_MAGNIFIER_RADIUS_FRACTION = 0.32; -const ICON_TRACKING_RADIUS_FRACTION = 0.35; -const ICON_TRACKING_GAP_FRACTION = 0.15; - -// ── Icons ───────────────────────────────────────────────────────────────── - -/** Two small XY arrows. */ -function axesIcon(): Node { - const xArrow = new ArrowNode( - 0, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - CONTROL_ICON_SIZE * ICON_X_ARROW_END_FRACTION, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - { - fill: TrackLabColors.axisXColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }, - ); - const yArrow = new ArrowNode( - 0, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - 0, - CONTROL_ICON_SIZE * ICON_Y_ARROW_END_FRACTION, - { - fill: TrackLabColors.axisYColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }, - ); - return new Node({ children: [xArrow, yArrow] }); -} - -/** Two endpoint dots joined by a dashed line. */ -function calibrationIcon(): Node { - const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const half = CONTROL_ICON_SIZE * ICON_HALF_FRACTION; - const calColor = TrackLabColors.calibrationFillProperty; - return new Node({ - children: [ - new Line(cx - half, cy, cx + half, cy, { - stroke: calColor, - lineWidth: ICON_LINE_WIDTH_THICK, - lineDash: ICON_LINE_DASH, - }), - new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx - half, y: cy }), - new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx + half, y: cy }), - ], - }); -} - -/** Circle with a diagonal handle — magnifying glass silhouette. */ -function magnifyIcon(): Node { - const r = CONTROL_ICON_SIZE * 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: ICON_LINE_WIDTH_THICK, - fill: null, - x: cx, - y: cy, - }), - new Line( - cx + r * ICON_ORIGIN_FRACTION, - cy + r * ICON_ORIGIN_FRACTION, - CONTROL_ICON_SIZE - 1, - CONTROL_ICON_SIZE - 1, - { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_MAGNIFIER, - }, - ), - ], - }); -} - -/** Crosshair with a small centre dot — tracking target. */ -function trackingIcon(): Node { - const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const r = CONTROL_ICON_SIZE * ICON_TRACKING_RADIUS_FRACTION; - const gap = CONTROL_ICON_SIZE * ICON_TRACKING_GAP_FRACTION; - const gray = TrackLabColors.iconGrayProperty; - return new Node({ - children: [ - new Circle(r, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THICK, - fill: null, - x: cx, - y: cy, - }), - new Circle(ICON_CENTER_DOT_RADIUS, { fill: gray, x: cx, y: cy }), - new Line(cx, cy - r - gap, cx, cy - gap, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THIN, - }), - new Line(cx, cy + gap, cx, cy + r + gap, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THIN, - }), - new Line(cx - r - gap, cy, cx - gap, cy, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THIN, - }), - new Line(cx + gap, cy, cx + r + gap, cy, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THIN, - }), - ], - }); -} - // ── Helper ──────────────────────────────────────────────────────────────── function makeRow(icon: Node, property: import("scenerystack/axon").BooleanProperty, accessibleName: string): Checkbox { @@ -186,7 +48,7 @@ export class ControlPanel extends Panel { const a11yStrings = StringManager.getInstance().getA11y(); const autoTrackingCheckbox = makeRow( - trackingIcon(), + makeTrackingIcon(), overlayTools.autoTrackingProperty, a11yStrings.toggleAutoTrackingStringProperty.value, ); @@ -196,13 +58,13 @@ export class ControlPanel extends Panel { const rows = new VBox({ children: [ - makeRow(axesIcon(), overlayTools.axesVisibleProperty, a11yStrings.toggleAxesStringProperty.value), + makeRow(makeAxesIcon(), overlayTools.axesVisibleProperty, a11yStrings.toggleAxesStringProperty.value), makeRow( - calibrationIcon(), + makeCalibrationIcon(), overlayTools.calibrationVisibleProperty, a11yStrings.toggleCalibrationStringProperty.value, ), - makeRow(magnifyIcon(), overlayTools.magnifyVideoProperty, a11yStrings.toggleMagnifierStringProperty.value), + makeRow(makeMagnifyIcon(), overlayTools.magnifyVideoProperty, a11yStrings.toggleMagnifierStringProperty.value), autoTrackingCheckbox, ], spacing: CONTROL_PANEL_ROWS_SPACING, diff --git a/src/track-lab/view/DataTableNode.ts b/src/track-lab/view/DataTableNode.ts index 210d22e..008dcb3 100644 --- a/src/track-lab/view/DataTableNode.ts +++ b/src/track-lab/view/DataTableNode.ts @@ -26,7 +26,17 @@ import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import { createTrackLabButton, makeDownloadIcon } from "../../TrackLabButton.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { OVERLAY_DRAG_SPEED, OVERLAY_SHIFT_DRAG_SPEED, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import { + OVERLAY_DRAG_SPEED, + OVERLAY_SHIFT_DRAG_SPEED, + PANEL_CORNER_RADIUS, + 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 { generateCsv } from "../model/TrackExporter.js"; import type { TrackingModel } from "../model/TrackingModel.js"; @@ -43,11 +53,7 @@ const CONTENT_SPACING = 6; const TITLE_ROW_SPACING = 8; const EXPORT_BUTTON_ICON_SPACING = 3; -// ── Resize handle geometry ──────────────────────────────────────────────────── -const HANDLE_SIZE = 12; -const HANDLE_OFFSET = -6; -const RESIZE_TOUCH_DILATION = 6; -const RESIZE_MOUSE_DILATION = 4; +// ── Resize constraints (handle geometry lives in TrackLabConstants) ─────────── const MIN_TABLE_RESIZE_WIDTH = 200; const MIN_TABLE_RESIZE_HEIGHT = 80; @@ -220,17 +226,25 @@ export class DataTableNode extends Node { ] as const; resizeCorners.forEach(({ cursor, accessibleName }, cornerIndex) => { - const handle = new Rectangle(0, 0, HANDLE_SIZE, HANDLE_SIZE, 2, 2, { - fill: TrackLabColors.controlPanelFillProperty, - stroke: TrackLabColors.controlPanelStrokeProperty, - lineWidth: 2, - cursor, - tagName: "div", - focusable: true, - accessibleName, - }); - handle.touchArea = handle.localBounds.dilated(RESIZE_TOUCH_DILATION); - handle.mouseArea = handle.localBounds.dilated(RESIZE_MOUSE_DILATION); + const handle = new Rectangle( + 0, + 0, + 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, + tagName: "div", + focusable: true, + accessibleName, + }, + ); + handle.touchArea = handle.localBounds.dilated(RESIZE_HANDLE_TOUCH_DILATION); + handle.mouseArea = handle.localBounds.dilated(RESIZE_HANDLE_MOUSE_DILATION); let dragStartState: { maxWidth: number; maxHeight: number; nodeX: number; nodeY: number } | null = null; let dragStartPointerPoint: Vector2 | null = null; @@ -418,9 +432,14 @@ export class DataTableNode extends Node { this.resizeHandles.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(RESIZE_TOUCH_DILATION); - handle.mouseArea = handle.localBounds.dilated(RESIZE_MOUSE_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); } }); } diff --git a/src/track-lab/view/InfoDialogNode.ts b/src/track-lab/view/InfoDialogNode.ts index 95543b0..743fb2f 100644 --- a/src/track-lab/view/InfoDialogNode.ts +++ b/src/track-lab/view/InfoDialogNode.ts @@ -11,13 +11,21 @@ import type { ReadOnlyProperty } from "scenerystack/axon"; import { Shape } from "scenerystack/kite"; -import { Circle, FireListener, HBox, Line, Node, Path, Rectangle, RichText, Text, VBox } from "scenerystack/scenery"; -import { ArrowNode, CloseButton, PhetFont } from "scenerystack/scenery-phet"; +import { FireListener, HBox, Line, Node, Path, Rectangle, RichText, Text, VBox } from "scenerystack/scenery"; +import { CloseButton, PhetFont } from "scenerystack/scenery-phet"; import { Panel } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { CONTROL_ICON_SIZE, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import { + CONTROL_ICON_CENTER_FRACTION, + CONTROL_ICON_HALF_FRACTION, + CONTROL_ICON_LINE_WIDTH_THICK, + CONTROL_ICON_LINE_WIDTH_THIN, + CONTROL_ICON_SIZE, + PANEL_CORNER_RADIUS, +} from "../../TrackLabConstants.js"; +import { makeAxesIcon, makeCalibrationIcon, makeMagnifyIcon, makeTrackingIcon } from "../../TrackLabIcons.js"; import TrackLabNamespace from "../../TrackLabNamespace.js"; // ── Layout constants ────────────────────────────────────────────────────────── @@ -38,26 +46,7 @@ const TAB_UNDERLINE_HEIGHT = 2; // thickness of active-tab indicator const TAB_ACTIVE_OPACITY = 1; const TAB_INACTIVE_OPACITY = 0.4; -// ── Icon shared constants ──────────────────────────────────────────────────── -const ICON_ARROW_HEAD_SIZE = 5; -const ICON_ARROW_TAIL_WIDTH = 1.5; -const ICON_LINE_WIDTH_THICK = 1.5; -const ICON_LINE_WIDTH_MAGNIFIER = 2; -const ICON_DOT_RADIUS = 3; -const ICON_CENTER_DOT_RADIUS = 2; -const ICON_LINE_DASH: number[] = [3, 2]; - -// Icon layout fractions -const ICON_ORIGIN_FRACTION = 0.7; -const ICON_X_ARROW_END_FRACTION = 0.85; -const ICON_Y_ARROW_END_FRACTION = 0.05; -const ICON_HALF_FRACTION = 0.4; -const ICON_CENTER_FRACTION = 0.5; -const ICON_MAGNIFIER_RADIUS_FRACTION = 0.32; -const ICON_TRACKING_RADIUS_FRACTION = 0.35; -const ICON_TRACKING_GAP_FRACTION = 0.15; - -// ── Tab 1 icon helpers ──────────────────────────────────────────────────────── +// ── Dialog-only icon helpers ────────────────────────────────────────────────── /** Video/file icon - folder with video symbol. */ function videoIcon(): Node { @@ -70,65 +59,15 @@ function videoIcon(): Node { .lineTo(18, 16) .lineTo(2, 16) .close(); - const folder = new Path(folderShape, { stroke: gray, lineWidth: ICON_LINE_WIDTH_THICK, fill: null }); + const folder = new Path(folderShape, { stroke: gray, lineWidth: CONTROL_ICON_LINE_WIDTH_THICK, fill: null }); const play = new Path(new Shape().moveTo(8, 7).lineTo(8, 13).lineTo(13, 10).close(), { fill: gray }); return new Node({ children: [folder, play] }); } -/** Two small XY arrows for coordinate system. */ -function axesIcon(): Node { - const xArrow = new ArrowNode( - 0, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - CONTROL_ICON_SIZE * ICON_X_ARROW_END_FRACTION, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - { - fill: TrackLabColors.axisXColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }, - ); - const yArrow = new ArrowNode( - 0, - CONTROL_ICON_SIZE * ICON_ORIGIN_FRACTION, - 0, - CONTROL_ICON_SIZE * ICON_Y_ARROW_END_FRACTION, - { - fill: TrackLabColors.axisYColorProperty, - stroke: null, - headWidth: ICON_ARROW_HEAD_SIZE, - headHeight: ICON_ARROW_HEAD_SIZE, - tailWidth: ICON_ARROW_TAIL_WIDTH, - }, - ); - return new Node({ children: [xArrow, yArrow] }); -} - -/** Two endpoint dots joined by a dashed line for calibration. */ -function calibrationIcon(): Node { - const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const half = CONTROL_ICON_SIZE * ICON_HALF_FRACTION; - const calColor = TrackLabColors.calibrationFillProperty; - return new Node({ - children: [ - new Line(cx - half, cy, cx + half, cy, { - stroke: calColor, - lineWidth: ICON_LINE_WIDTH_THICK, - lineDash: ICON_LINE_DASH, - }), - new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx - half, y: cy }), - new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx + half, y: cy }), - ], - }); -} - /** Plus sign for adding tracks. */ function addTrackIcon(): Node { const gray = TrackLabColors.iconGrayProperty; - const center = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; + const center = CONTROL_ICON_SIZE * CONTROL_ICON_CENTER_FRACTION; const size = CONTROL_ICON_SIZE * 0.6; return new Node({ children: [ @@ -138,75 +77,25 @@ function addTrackIcon(): Node { }); } -/** Magnifying glass for digitizing. */ -function magnifyIcon(): Node { - const r = CONTROL_ICON_SIZE * 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: ICON_LINE_WIDTH_THICK, - fill: null, - x: cx, - y: cy, - }), - new Line( - cx + r * ICON_ORIGIN_FRACTION, - cy + r * ICON_ORIGIN_FRACTION, - CONTROL_ICON_SIZE - 1, - CONTROL_ICON_SIZE - 1, - { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_MAGNIFIER, - }, - ), - ], - }); -} - -// ── Tab 2 icon helpers ──────────────────────────────────────────────────────── - -/** Crosshair with centre dot for auto-tracking. */ -function trackingIcon(): Node { - const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const r = CONTROL_ICON_SIZE * ICON_TRACKING_RADIUS_FRACTION; - const gap = CONTROL_ICON_SIZE * ICON_TRACKING_GAP_FRACTION; - const gray = TrackLabColors.iconGrayProperty; - return new Node({ - children: [ - new Circle(r, { - stroke: gray, - lineWidth: ICON_LINE_WIDTH_THICK, - fill: null, - x: cx, - y: cy, - }), - new Circle(ICON_CENTER_DOT_RADIUS, { fill: gray, x: cx, y: cy }), - new Line(cx, cy - r - gap, cx, cy - gap, { stroke: gray, lineWidth: 1 }), - new Line(cx, cy + gap, cx, cy + r + gap, { stroke: gray, lineWidth: 1 }), - new Line(cx - r - gap, cy, cx - gap, cy, { stroke: gray, lineWidth: 1 }), - new Line(cx + gap, cy, cx + r + gap, cy, { stroke: gray, lineWidth: 1 }), - ], - }); -} - /** Horizontal tape with end ticks — measuring tape. */ function measuringTapeIcon(): Node { - const cx = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const cy = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION; - const half = CONTROL_ICON_SIZE * ICON_HALF_FRACTION; + 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 color = TrackLabColors.measuringTapeColorProperty; const tickH = 4; return new Node({ children: [ - new Line(cx - half, cy, cx + half, cy, { stroke: color, lineWidth: ICON_LINE_WIDTH_THICK }), - new Line(cx - half, cy - tickH / 2, cx - half, cy + tickH / 2, { stroke: color, lineWidth: 1 }), - new Line(cx, cy - tickH / 4, cx, cy + tickH / 4, { stroke: color, lineWidth: 1 }), - new Line(cx + half, cy - tickH / 2, cx + half, cy + tickH / 2, { stroke: color, lineWidth: 1 }), + new Line(cx - half, cy, cx + half, cy, { stroke: color, lineWidth: CONTROL_ICON_LINE_WIDTH_THICK }), + new Line(cx - half, cy - tickH / 2, cx - half, cy + tickH / 2, { + stroke: color, + lineWidth: CONTROL_ICON_LINE_WIDTH_THIN, + }), + new Line(cx, cy - tickH / 4, cx, cy + tickH / 4, { stroke: color, lineWidth: CONTROL_ICON_LINE_WIDTH_THIN }), + new Line(cx + half, cy - tickH / 2, cx + half, cy + tickH / 2, { + stroke: color, + lineWidth: CONTROL_ICON_LINE_WIDTH_THIN, + }), ], }); } @@ -221,17 +110,17 @@ function angleToolIcon(): Node { const arm1 = new Line(vx, vy, vx + armLen, vy, { stroke: color, - lineWidth: ICON_LINE_WIDTH_THICK, + lineWidth: CONTROL_ICON_LINE_WIDTH_THICK, }); const arm2 = new Line(vx, vy, vx + armLen * Math.cos(armAngle), vy - armLen * Math.sin(armAngle), { stroke: color, - lineWidth: ICON_LINE_WIDTH_THICK, + lineWidth: CONTROL_ICON_LINE_WIDTH_THICK, }); // Arc sweeping from the diagonal arm (−armAngle) to horizontal (0), clockwise on screen const arcR = armLen * 0.32; const arc = new Path(new Shape().arc(vx, vy, arcR, -armAngle, 0, false), { stroke: color, - lineWidth: 1, + lineWidth: CONTROL_ICON_LINE_WIDTH_THIN, fill: null, }); @@ -249,12 +138,12 @@ function graphIcon(): Node { const top = m + 1; const w = right - left; - const xAxis = new Line(left, bottom, right, bottom, { stroke: gray, lineWidth: 1 }); - const yAxis = new Line(left, bottom, left, top, { stroke: gray, lineWidth: 1 }); + const xAxis = new Line(left, bottom, right, bottom, { stroke: gray, lineWidth: CONTROL_ICON_LINE_WIDTH_THIN }); + const yAxis = new Line(left, bottom, left, top, { stroke: gray, lineWidth: CONTROL_ICON_LINE_WIDTH_THIN }); const curveShape = new Shape() .moveTo(left + 1, bottom - 2) .cubicCurveTo(left + w * 0.25, bottom - 3, left + w * 0.55, top + 5, right - 1, top + 2); - const curve = new Path(curveShape, { stroke: curveColor, lineWidth: 1.5, fill: null }); + const curve = new Path(curveShape, { stroke: curveColor, lineWidth: CONTROL_ICON_LINE_WIDTH_THICK, fill: null }); return new Node({ children: [xAxis, yAxis, curve] }); } @@ -274,7 +163,7 @@ function tableIcon(): Node { // header fill new Rectangle(m, m, w, headerH, { fill: TrackLabColors.tableHeaderBackgroundProperty, stroke: null }), // outer border (drawn after fill so it appears on top) - new Rectangle(m, m, w, h, { stroke: gray, lineWidth: 1, fill: null }), + new Rectangle(m, m, w, h, { stroke: gray, lineWidth: CONTROL_ICON_LINE_WIDTH_THIN, fill: null }), // horizontal dividers new Line(m, m + headerH, m + w, m + headerH, { stroke: gray, lineWidth: 0.5 }), new Line(m, m + headerH + Math.round((h - headerH) / 2), m + w, m + headerH + Math.round((h - headerH) / 2), { @@ -405,10 +294,14 @@ export class InfoDialogNode extends Node { const tab1Content = new VBox({ children: [ makeStep(videoIcon(), strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty), - makeStep(axesIcon(), strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty), - makeStep(calibrationIcon(), strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), + makeStep( + makeAxesIcon(), + strings.coordinateSystemTitleStringProperty, + strings.coordinateSystemBodyStringProperty, + ), + makeStep(makeCalibrationIcon(), strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), makeStep(addTrackIcon(), strings.addTrackTitleStringProperty, strings.addTrackBodyStringProperty), - makeStep(magnifyIcon(), strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty), + makeStep(makeMagnifyIcon(), strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty), ], spacing: STEPS_SPACING, align: "left", @@ -417,7 +310,7 @@ export class InfoDialogNode extends Node { // ── Tab 2 content (Tools) ───────────────────────────────────────────────── const tab2Content = new VBox({ children: [ - makeStep(trackingIcon(), strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty), + makeStep(makeTrackingIcon(), strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty), makeStep( measuringTapeIcon(), strings.measuringTapeTitleStringProperty, diff --git a/src/track-lab/view/TableRenderer.ts b/src/track-lab/view/TableRenderer.ts index 2830ac3..55ff769 100644 --- a/src/track-lab/view/TableRenderer.ts +++ b/src/track-lab/view/TableRenderer.ts @@ -24,6 +24,7 @@ import { toFixed } from "scenerystack/dot"; import { TRACK_COLORS } from "../../TrackLabColors.js"; +import { DATA_DECIMAL_PLACES } from "../../TrackLabConstants.js"; import type { Track } from "../model/Track.js"; import { buildDataRows, type DataRow } from "../model/TrackExporter.js"; @@ -33,7 +34,6 @@ export const MIN_TABLE_HEIGHT = 100; export const MAX_TABLE_HEIGHT = 220; // ── Precision ───────────────────────────────────────────────────────────────── -const CELL_DECIMAL_PLACES = 4; const MIN_EMPTY_COL_COUNT = 4; // ── CSS dimensions ──────────────────────────────────────────────────────────── @@ -222,12 +222,12 @@ function buildDataRow( }; addCell(String(row.frame)); - addCell(toFixed(row.time, CELL_DECIMAL_PLACES)); + addCell(toFixed(row.time, DATA_DECIMAL_PLACES)); for (const track of tracks) { const val = row.values.get(track.id); if (val) { - addCell(toFixed(val.x, CELL_DECIMAL_PLACES)); - addCell(toFixed(val.y, CELL_DECIMAL_PLACES)); + addCell(toFixed(val.x, DATA_DECIMAL_PLACES)); + addCell(toFixed(val.y, DATA_DECIMAL_PLACES)); } else { addCell("—"); addCell("—"); @@ -389,10 +389,10 @@ export class TableRenderer { const xCell = cells[cellIdx]; const yCell = cells[cellIdx + 1]; if (xCell) { - xCell.textContent = val ? toFixed(val.x, CELL_DECIMAL_PLACES) : "—"; + xCell.textContent = val ? toFixed(val.x, DATA_DECIMAL_PLACES) : "—"; } if (yCell) { - yCell.textContent = val ? toFixed(val.y, CELL_DECIMAL_PLACES) : "—"; + yCell.textContent = val ? toFixed(val.y, DATA_DECIMAL_PLACES) : "—"; } cellIdx += 2; } diff --git a/src/track-lab/view/TrackLabScreenView.ts b/src/track-lab/view/TrackLabScreenView.ts index 54c0088..7369c06 100644 --- a/src/track-lab/view/TrackLabScreenView.ts +++ b/src/track-lab/view/TrackLabScreenView.ts @@ -14,7 +14,14 @@ import { Tandem } from "scenerystack/tandem"; import { FLAT_RESET_ALL_BUTTON_OPTIONS } from "../../common/TrackLabButtonOptions.js"; import { StringManager } from "../../i18n/StringManager.js"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; -import { CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js"; +import { + CONTROL_PANEL_LEFT_MARGIN, + DATA_TABLE_TOP_SPACING, + KINEMATICS_GRAPH_BOTTOM_MARGIN, + RESET_BUTTON_MARGIN, + SCREEN_TOP_MARGIN, + VIDEO_PLAYER_LEFT_SPACING, +} from "../../TrackLabConstants.js"; import TrackLabNamespace from "../../TrackLabNamespace.js"; import type { TrackLabModel } from "../model/TrackLabModel.js"; import { AngleToolNode } from "./AngleToolNode.js"; @@ -30,11 +37,6 @@ import { TrackLabScreenSummaryContent } from "./TrackLabScreenSummaryContent.js" import { TrackListPanel } from "./TrackListPanel.js"; import { VideoPlayerNode } from "./VideoPlayerNode.js"; -// ── Layout constants ────────────────────────────────────────────────────────── -const SCREEN_TOP_MARGIN = 10; // inset from layout top edge for control panel and video -const VIDEO_PLAYER_LEFT_SPACING = 60; // gap between control panel right and video player left -const KINEMATICS_GRAPH_BOTTOM_MARGIN = 50; // gap between kinematics graph bottom and reset button top - /** * Root layout for the simulation screen. *