diff --git a/CLAUDE.md b/CLAUDE.md
index 0f98404..d9d6ba3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -90,6 +90,20 @@ npm run fix # fix lint + format issues together
- **Frame rate**: `SimModel.frameRateProperty` (default 30 fps) drives `frameDurationProperty`. The user can change frame rate via `PlaybackControlsNode`; frame stepping and time display use this value.
- **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.
+## Constants and colors
+
+- **Colors**: All UI colors live in `TrackLabColors.ts` as `ProfileColorProperty` instances for automatic dark/light theme switching. When adding a new color, create it there — never hardcode `rgb(…)` or hex strings in view files.
+- **Layout constants**: Shared numeric constants (panel margins, drag speeds, touch dilation) live in `TrackLabConstants.ts`. File-local constants that are used only within a single view file can remain local, but any constant duplicated across two or more files should be hoisted to `TrackLabConstants.ts`.
+- **Overlay constants**: Draggable overlays (measuring tape, angle tool, calibration tool, coordinate system) share `OVERLAY_DRAG_SPEED`, `OVERLAY_SHIFT_DRAG_SPEED`, `OVERLAY_TOUCH_DILATION`, and label panel styling constants (`LABEL_PANEL_*`) from `TrackLabConstants.ts`.
+- **Control panel constants**: `CONTROL_ICON_SIZE`, `CONTROL_PANEL_ROWS_SPACING`, `CONTROL_PANEL_X_MARGIN`, `CONTROL_PANEL_Y_MARGIN` are shared across `ControlPanel.ts`, `MeasurementToolsPanel.ts`, and `InfoDialogNode.ts`.
+
+## Accessibility
+
+- **Localized a11y strings**: All accessibility text lives in `StringManager.getA11y()` backed by the `a11y` section in `strings_en.json` / `strings_fr.json`. Never hardcode English strings for `accessibleName` or `aria-label`.
+- **Interactive elements**: Every interactive SceneryStack node (button, checkbox, draggable handle) must have an `accessibleName` sourced from the a11y string properties.
+- **Canvas overlays**: Non-interactive overlay containers that wrap a Canvas or DOM element should have `tagName: "div"` and an `accessibleName` so screen readers can identify them.
+- **HTML tables**: Use `
` (visually hidden) and `aria-label` on `` elements for data tables.
+
## Testing
There is currently **no test suite**, and none should be added at this stage. The codebase is evolving rapidly — APIs, model structure, and UI components change frequently enough that maintaining tests would cost more than they save right now. Do not install a test framework or create test files.
diff --git a/package-lock.json b/package-lock.json
index 6f25fd6..00387fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -73,7 +73,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -3247,7 +3246,6 @@
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3285,7 +3283,6 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3510,7 +3507,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6153,7 +6149,6 @@
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true,
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -6209,7 +6204,6 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
@@ -6470,7 +6464,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -6848,7 +6841,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
diff --git a/scripts/bouncingBallToSVG.ts b/scripts/bouncingBallToSVG.ts
index 7ca29b0..7615f51 100644
--- a/scripts/bouncingBallToSVG.ts
+++ b/scripts/bouncingBallToSVG.ts
@@ -2,7 +2,7 @@
* bouncingBallToSVG.ts
*
* Generates an SVG of a bouncing ball for use as an icon.
- *
+ *
* The bouncing ball is a simple parabolic motion with a restitution coefficient.
* The ball is drawn as a series of circles, one for each snapshot.
* The floor is drawn as a line.
@@ -108,7 +108,7 @@ function buildSvg(snapshots: Point[]): string {
(p) =>
` `
+ `opacity="${BALL_OPACITY}"/>`,
)
.join("\n");
diff --git a/src/TrackLabColors.ts b/src/TrackLabColors.ts
index ffb5c65..a508d83 100644
--- a/src/TrackLabColors.ts
+++ b/src/TrackLabColors.ts
@@ -254,6 +254,31 @@ const TrackLabColors = {
new Color(0, 150, 180),
),
+ // Measuring tape overlay
+ measuringTapeColorProperty: profileColor("measuringTapeColor", new Color(240, 185, 55), new Color(220, 170, 40)),
+ measuringTapeShadowProperty: profileColor("measuringTapeShadow", new Color(0, 0, 0, 0.45), new Color(0, 0, 0, 0.45)),
+
+ // Angle tool overlay
+ angleToolColorProperty: profileColor("angleToolColor", new Color(170, 100, 255), new Color(150, 80, 230)),
+ angleToolShadowProperty: profileColor("angleToolShadow", new Color(0, 0, 0, 0.45), new Color(0, 0, 0, 0.45)),
+
+ // Shared overlay handle outline (used by measuring tape and angle tool endpoints)
+ overlayHandleOutlineProperty: profileColor(
+ "overlayHandleOutline",
+ new Color(0, 0, 0, 0.65),
+ new Color(0, 0, 0, 0.65),
+ ),
+
+ // Shared overlay icon shadow (used by measurement tool panel icons)
+ iconShadowProperty: profileColor("iconShadow", new Color(0, 0, 0, 0.5), new Color(0, 0, 0, 0.5)),
+
+ // Calibration tool warning (endpoints too close)
+ calibrationWarningColorProperty: profileColor(
+ "calibrationWarningColor",
+ new Color(255, 60, 60),
+ new Color(255, 60, 60),
+ ),
+
// Track list panel
trashIconProperty: profileColor("trashIcon", new Color(255, 102, 102), new Color(220, 80, 80)),
trashButtonBaseProperty: profileColor("trashButtonBase", new Color(60, 20, 20, 0.5), new Color(80, 30, 30, 0.6)),
diff --git a/src/TrackLabConstants.ts b/src/TrackLabConstants.ts
index 8cce2b7..316fba3 100644
--- a/src/TrackLabConstants.ts
+++ b/src/TrackLabConstants.ts
@@ -76,3 +76,26 @@ export const WEBCAM_PREVIEW_HEIGHT = 324; // height of the preview and review vi
// Opacity applied to coordinate system and calibration tool overlays while
// the user is actively digitizing, signalling that those tools are locked out.
export const DIGITIZING_DIM_OPACITY = 0.35;
+
+// ── Shared overlay drag speeds ────────────────────────────────────────────────
+// Keyboard drag speeds in pixels/second used by all draggable overlays
+// (measuring tape, angle tool, calibration tool, coordinate system).
+export const OVERLAY_DRAG_SPEED = 200; // normal keyboard drag
+export const OVERLAY_SHIFT_DRAG_SPEED = 40; // shift-key fine adjustment
+
+// ── 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;
+export const LABEL_PANEL_X_MARGIN = 6;
+export const LABEL_PANEL_Y_MARGIN = 3;
+export const LABEL_PANEL_SCALE = 0.8;
+
+// ── Shared overlay endpoint touch dilation ────────────────────────────────────
+export const OVERLAY_TOUCH_DILATION = 12;
+
+// ── Control panel icon and layout ─────────────────────────────────────────────
+// Shared by ControlPanel, MeasurementToolsPanel, and InfoDialogNode.
+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;
diff --git a/src/i18n/StringManager.ts b/src/i18n/StringManager.ts
index 7249383..c80a2be 100644
--- a/src/i18n/StringManager.ts
+++ b/src/i18n/StringManager.ts
@@ -413,6 +413,26 @@ export class StringManager {
removeTrackStringProperty: ReadOnlyProperty;
dataTableStringProperty: ReadOnlyProperty;
exportCSVStringProperty: ReadOnlyProperty;
+ measuringTapeBaseStringProperty: ReadOnlyProperty;
+ measuringTapeTipStringProperty: ReadOnlyProperty;
+ angleToolVertexStringProperty: ReadOnlyProperty;
+ angleArm1StringProperty: ReadOnlyProperty;
+ angleArm2StringProperty: ReadOnlyProperty;
+ toggleAxesStringProperty: ReadOnlyProperty;
+ toggleCalibrationStringProperty: ReadOnlyProperty;
+ toggleMagnifierStringProperty: ReadOnlyProperty;
+ toggleAutoTrackingStringProperty: ReadOnlyProperty;
+ toggleMeasuringTapeStringProperty: ReadOnlyProperty;
+ toggleAngleToolStringProperty: ReadOnlyProperty;
+ graphRescaleStringProperty: ReadOnlyProperty;
+ graphZoomInStringProperty: ReadOnlyProperty;
+ graphZoomOutStringProperty: ReadOnlyProperty;
+ graphPanLeftStringProperty: ReadOnlyProperty;
+ graphPanRightStringProperty: ReadOnlyProperty;
+ graphPanUpStringProperty: ReadOnlyProperty;
+ graphPanDownStringProperty: ReadOnlyProperty;
+ kinematicsGraphStringProperty: ReadOnlyProperty;
+ selectTrackForGraphStringProperty: ReadOnlyProperty;
} {
return {
videoPlayerStringProperty: this.stringProperties.a11y.videoPlayerStringProperty,
@@ -423,6 +443,26 @@ export class StringManager {
removeTrackStringProperty: this.stringProperties.a11y.removeTrackStringProperty,
dataTableStringProperty: this.stringProperties.a11y.dataTableStringProperty,
exportCSVStringProperty: this.stringProperties.a11y.exportCSVStringProperty,
+ measuringTapeBaseStringProperty: this.stringProperties.a11y.measuringTapeBaseStringProperty,
+ measuringTapeTipStringProperty: this.stringProperties.a11y.measuringTapeTipStringProperty,
+ angleToolVertexStringProperty: this.stringProperties.a11y.angleToolVertexStringProperty,
+ angleArm1StringProperty: this.stringProperties.a11y.angleArm1StringProperty,
+ angleArm2StringProperty: this.stringProperties.a11y.angleArm2StringProperty,
+ toggleAxesStringProperty: this.stringProperties.a11y.toggleAxesStringProperty,
+ toggleCalibrationStringProperty: this.stringProperties.a11y.toggleCalibrationStringProperty,
+ toggleMagnifierStringProperty: this.stringProperties.a11y.toggleMagnifierStringProperty,
+ toggleAutoTrackingStringProperty: this.stringProperties.a11y.toggleAutoTrackingStringProperty,
+ toggleMeasuringTapeStringProperty: this.stringProperties.a11y.toggleMeasuringTapeStringProperty,
+ toggleAngleToolStringProperty: this.stringProperties.a11y.toggleAngleToolStringProperty,
+ graphRescaleStringProperty: this.stringProperties.a11y.graphRescaleStringProperty,
+ graphZoomInStringProperty: this.stringProperties.a11y.graphZoomInStringProperty,
+ graphZoomOutStringProperty: this.stringProperties.a11y.graphZoomOutStringProperty,
+ graphPanLeftStringProperty: this.stringProperties.a11y.graphPanLeftStringProperty,
+ graphPanRightStringProperty: this.stringProperties.a11y.graphPanRightStringProperty,
+ graphPanUpStringProperty: this.stringProperties.a11y.graphPanUpStringProperty,
+ graphPanDownStringProperty: this.stringProperties.a11y.graphPanDownStringProperty,
+ kinematicsGraphStringProperty: this.stringProperties.a11y.kinematicsGraphStringProperty,
+ selectTrackForGraphStringProperty: this.stringProperties.a11y.selectTrackForGraphStringProperty,
};
}
diff --git a/src/i18n/strings_en.json b/src/i18n/strings_en.json
index d431a97..49d85d0 100644
--- a/src/i18n/strings_en.json
+++ b/src/i18n/strings_en.json
@@ -157,12 +157,32 @@
},
"a11y": {
"videoPlayer": "Video player",
- "videoScrubber": "Video timeline — drag to seek",
+ "videoScrubber": "Video timeline \u2014 drag to seek",
"rewindToStart": "Rewind to start",
- "digitizingArea": "Video digitizing area — click to record the position of the active track",
+ "digitizingArea": "Video digitizing area \u2014 click to record the position of the active track",
"digitizeTrack": "Digitize track {{symbol}}",
"removeTrack": "Remove track {{symbol}}",
"dataTable": "Track data",
- "exportCSV": "Export track data as CSV file"
+ "exportCSV": "Export track data as CSV file",
+ "measuringTapeBase": "Measuring tape base endpoint",
+ "measuringTapeTip": "Measuring tape tip endpoint",
+ "angleToolVertex": "Angle tool vertex",
+ "angleArm1": "Angle tool arm 1 endpoint",
+ "angleArm2": "Angle tool arm 2 endpoint",
+ "toggleAxes": "Toggle coordinate axes",
+ "toggleCalibration": "Toggle calibration tool",
+ "toggleMagnifier": "Toggle digitizing magnifier",
+ "toggleAutoTracking": "Toggle auto-tracking",
+ "toggleMeasuringTape": "Toggle measuring tape",
+ "toggleAngleTool": "Toggle angle tool",
+ "graphRescale": "Rescale graph to fit data",
+ "graphZoomIn": "Zoom in on graph",
+ "graphZoomOut": "Zoom out on graph",
+ "graphPanLeft": "Pan graph left",
+ "graphPanRight": "Pan graph right",
+ "graphPanUp": "Pan graph up",
+ "graphPanDown": "Pan graph down",
+ "kinematicsGraph": "Kinematics graph",
+ "selectTrackForGraph": "Select track to display on graph"
}
}
diff --git a/src/i18n/strings_fr.json b/src/i18n/strings_fr.json
index dd97b59..2396b83 100644
--- a/src/i18n/strings_fr.json
+++ b/src/i18n/strings_fr.json
@@ -163,6 +163,26 @@
"digitizeTrack": "Num\u00e9riser la piste {{symbol}}",
"removeTrack": "Supprimer la piste {{symbol}}",
"dataTable": "Donn\u00e9es de piste",
- "exportCSV": "Exporter les donn\u00e9es de piste en fichier CSV"
+ "exportCSV": "Exporter les donn\u00e9es de piste en fichier CSV",
+ "measuringTapeBase": "Extr\u00e9mit\u00e9 de base du ruban de mesure",
+ "measuringTapeTip": "Extr\u00e9mit\u00e9 du ruban de mesure",
+ "angleToolVertex": "Sommet du rapporteur",
+ "angleArm1": "Extr\u00e9mit\u00e9 du bras 1 du rapporteur",
+ "angleArm2": "Extr\u00e9mit\u00e9 du bras 2 du rapporteur",
+ "toggleAxes": "Basculer les axes de coordonn\u00e9es",
+ "toggleCalibration": "Basculer l'outil de calibration",
+ "toggleMagnifier": "Basculer la loupe de num\u00e9risation",
+ "toggleAutoTracking": "Basculer le suivi automatique",
+ "toggleMeasuringTape": "Basculer le ruban de mesure",
+ "toggleAngleTool": "Basculer le rapporteur",
+ "graphRescale": "Redimensionner le graphique pour ajuster les donn\u00e9es",
+ "graphZoomIn": "Zoom avant sur le graphique",
+ "graphZoomOut": "Zoom arri\u00e8re sur le graphique",
+ "graphPanLeft": "D\u00e9placer le graphique vers la gauche",
+ "graphPanRight": "D\u00e9placer le graphique vers la droite",
+ "graphPanUp": "D\u00e9placer le graphique vers le haut",
+ "graphPanDown": "D\u00e9placer le graphique vers le bas",
+ "kinematicsGraph": "Graphique cin\u00e9matique",
+ "selectTrackForGraph": "S\u00e9lectionner la piste \u00e0 afficher sur le graphique"
}
}
diff --git a/src/screen-name/graph/ConfigurableGraph.ts b/src/screen-name/graph/ConfigurableGraph.ts
index d5ecd4f..3252382 100644
--- a/src/screen-name/graph/ConfigurableGraph.ts
+++ b/src/screen-name/graph/ConfigurableGraph.ts
@@ -58,6 +58,7 @@ import { Shape } from "scenerystack/kite";
import { Orientation } from "scenerystack/phet-core";
import { FireListener, HBox, Node, Rectangle, Text } from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
+import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
import trackLab from "../../TrackLabNamespace.js";
import GraphControlsPanel from "./GraphControlsPanel.js";
@@ -326,8 +327,10 @@ export default class ConfigurableGraph extends Node {
const buttonPadding = BUTTON_PADDING;
const buttonSpacing = BUTTON_SPACING;
+ const a11yStrings = StringManager.getInstance().getA11y();
+
// Helper function to create a button
- const createButton = (label: string, onClick: () => void): Node => {
+ const createButton = (label: string, onClick: () => void, accessibleName?: TReadOnlyProperty): Node => {
const buttonText = new Text(label, {
font: BUTTON_FONT,
fill: TrackLabColors.controlPanelStrokeProperty,
@@ -341,6 +344,8 @@ export default class ConfigurableGraph extends Node {
const button = new Node({
children: [buttonBackground, buttonText],
+ tagName: "button",
+ ...(accessibleName && { accessibleName }),
});
// Center the text in the button
@@ -367,37 +372,65 @@ export default class ConfigurableGraph extends Node {
};
// Create rescale button
- const rescaleButton = createButton("↻", () => {
- // Reset manual zoom flag and rescale to fit data
- this.dataManager.setManuallyZoomed(false);
- this.dataManager.updateAxisRanges();
- });
+ const rescaleButton = createButton(
+ "↻",
+ () => {
+ // Reset manual zoom flag and rescale to fit data
+ this.dataManager.setManuallyZoomed(false);
+ this.dataManager.updateAxisRanges();
+ },
+ a11yStrings.graphRescaleStringProperty,
+ );
- // Create zoom buttons (will be wired up after interactionHandler is created)
- const zoomInButton = createButton("+", () => {
- this.interactionHandler.zoomIn();
- });
+ // Create zoom buttons
+ const zoomInButton = createButton(
+ "+",
+ () => {
+ this.interactionHandler.zoomIn();
+ },
+ a11yStrings.graphZoomInStringProperty,
+ );
- const zoomOutButton = createButton("−", () => {
- this.interactionHandler.zoomOut();
- });
+ const zoomOutButton = createButton(
+ "−",
+ () => {
+ this.interactionHandler.zoomOut();
+ },
+ a11yStrings.graphZoomOutStringProperty,
+ );
- // Create pan buttons (will be wired up after interactionHandler is created)
- const panLeftButton = createButton("←", () => {
- this.interactionHandler.pan("left");
- });
+ // Create pan buttons
+ const panLeftButton = createButton(
+ "←",
+ () => {
+ this.interactionHandler.pan("left");
+ },
+ a11yStrings.graphPanLeftStringProperty,
+ );
- const panRightButton = createButton("→", () => {
- this.interactionHandler.pan("right");
- });
+ const panRightButton = createButton(
+ "→",
+ () => {
+ this.interactionHandler.pan("right");
+ },
+ a11yStrings.graphPanRightStringProperty,
+ );
- const panUpButton = createButton("↑", () => {
- this.interactionHandler.pan("up");
- });
+ const panUpButton = createButton(
+ "↑",
+ () => {
+ this.interactionHandler.pan("up");
+ },
+ a11yStrings.graphPanUpStringProperty,
+ );
- const panDownButton = createButton("↓", () => {
- this.interactionHandler.pan("down");
- });
+ const panDownButton = createButton(
+ "↓",
+ () => {
+ this.interactionHandler.pan("down");
+ },
+ a11yStrings.graphPanDownStringProperty,
+ );
// Create HBox to hold all buttons
const controlButtonsPanel = new HBox({
diff --git a/src/screen-name/view/AngleToolNode.ts b/src/screen-name/view/AngleToolNode.ts
index cdb8d93..01d0318 100644
--- a/src/screen-name/view/AngleToolNode.ts
+++ b/src/screen-name/view/AngleToolNode.ts
@@ -16,12 +16,20 @@ import { Circle, Line, Node, Path, RichDragListener, Text } from "scenerystack/s
import { 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 {
+ LABEL_PANEL_CORNER_RADIUS,
+ LABEL_PANEL_SCALE,
+ LABEL_PANEL_X_MARGIN,
+ LABEL_PANEL_Y_MARGIN,
+ OVERLAY_DRAG_SPEED,
+ OVERLAY_SHIFT_DRAG_SPEED,
+ OVERLAY_TOUCH_DILATION,
+} from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
// ── Visual constants ──────────────────────────────────────────────────────────
-const ARM_COLOR = "rgb(170, 100, 255)";
-const ARM_SHADOW_COLOR = "rgba(0, 0, 0, 0.45)";
const ARM_LINE_WIDTH = 2.5;
const ARM_SHADOW_WIDTH = 5;
const ARM_DASH: number[] = [8, 4];
@@ -31,17 +39,16 @@ const ARC_LINE_WIDTH = 2;
const VERTEX_RADIUS = 7;
const ENDPOINT_RADIUS = 5;
-const TOUCH_DILATION = 12;
+const VERTEX_OUTLINE_WIDTH = 1.5;
+const ARM_ENDPOINT_LINE_WIDTH = 2;
const FONT = new PhetFont({ size: 12, weight: "bold" });
const LABEL_OFFSET = ARC_RADIUS + 20; // distance from vertex to label centre
-const DRAG_SPEED = 200;
-const SHIFT_DRAG_SPEED = 40;
-
// Minimum arm length below which the arc and label are hidden to avoid
// degenerate geometry (zero-length arm → atan2 is undefined).
const MIN_ARM_LENGTH = 5;
+const ANGLE_DECIMAL_PLACES = 1;
/**
* Three-handle angle tool. Drag the vertex to reposition the whole tool's
@@ -53,13 +60,15 @@ export class AngleToolNode extends Node {
public constructor(visibleProperty: TReadOnlyProperty, model: SimModel) {
super();
+ const a11yStrings = StringManager.getInstance().getA11y();
+
// ── Arm shadow lines ──────────────────────────────────────────────────
const shadow1 = new Line(0, 0, 0, 0, {
- stroke: ARM_SHADOW_COLOR,
+ stroke: TrackLabColors.angleToolShadowProperty,
lineWidth: ARM_SHADOW_WIDTH,
});
const shadow2 = new Line(0, 0, 0, 0, {
- stroke: ARM_SHADOW_COLOR,
+ stroke: TrackLabColors.angleToolShadowProperty,
lineWidth: ARM_SHADOW_WIDTH,
});
this.addChild(shadow1);
@@ -67,12 +76,12 @@ export class AngleToolNode extends Node {
// ── Arm lines ─────────────────────────────────────────────────────────
const arm1Line = new Line(0, 0, 0, 0, {
- stroke: ARM_COLOR,
+ stroke: TrackLabColors.angleToolColorProperty,
lineWidth: ARM_LINE_WIDTH,
lineDash: ARM_DASH,
});
const arm2Line = new Line(0, 0, 0, 0, {
- stroke: ARM_COLOR,
+ stroke: TrackLabColors.angleToolColorProperty,
lineWidth: ARM_LINE_WIDTH,
lineDash: ARM_DASH,
});
@@ -81,60 +90,60 @@ export class AngleToolNode extends Node {
// ── Arc at vertex ─────────────────────────────────────────────────────
const arcPath = new Path(null, {
- stroke: ARM_COLOR,
+ stroke: TrackLabColors.angleToolColorProperty,
lineWidth: ARC_LINE_WIDTH,
});
this.addChild(arcPath);
// ── Vertex handle ─────────────────────────────────────────────────────
const vertexNode = new Circle(VERTEX_RADIUS, {
- fill: ARM_COLOR,
- stroke: "rgba(0, 0, 0, 0.65)",
- lineWidth: 1.5,
+ fill: TrackLabColors.angleToolColorProperty,
+ stroke: TrackLabColors.overlayHandleOutlineProperty,
+ lineWidth: VERTEX_OUTLINE_WIDTH,
cursor: "grab",
tagName: "div",
focusable: true,
- accessibleName: "Angle tool vertex",
+ accessibleName: a11yStrings.angleToolVertexStringProperty,
});
- const vertexTouchArea = Shape.circle(0, 0, VERTEX_RADIUS + TOUCH_DILATION);
+ const vertexTouchArea = Shape.circle(0, 0, VERTEX_RADIUS + OVERLAY_TOUCH_DILATION);
vertexNode.mouseArea = vertexTouchArea;
vertexNode.touchArea = vertexTouchArea;
this.addChild(vertexNode);
// ── Arm endpoint handles ──────────────────────────────────────────────
- const makeArmEndpoint = (accessibleName: string) => {
+ const makeArmEndpoint = (accessibleName: TReadOnlyProperty) => {
const node = new Circle(ENDPOINT_RADIUS, {
fill: "transparent",
- stroke: ARM_COLOR,
- lineWidth: 2,
+ stroke: TrackLabColors.angleToolColorProperty,
+ lineWidth: ARM_ENDPOINT_LINE_WIDTH,
cursor: "crosshair",
tagName: "div",
focusable: true,
accessibleName,
});
- const touchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + TOUCH_DILATION);
+ const touchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + OVERLAY_TOUCH_DILATION);
node.mouseArea = touchArea;
node.touchArea = touchArea;
return node;
};
- const arm1Node = makeArmEndpoint("Angle arm 1");
- const arm2Node = makeArmEndpoint("Angle arm 2");
+ const arm1Node = makeArmEndpoint(a11yStrings.angleArm1StringProperty);
+ const arm2Node = makeArmEndpoint(a11yStrings.angleArm2StringProperty);
this.addChild(arm1Node);
this.addChild(arm2Node);
// ── Angle label ───────────────────────────────────────────────────────
- const angleText = new Text("---", {
+ const angleText = new Text("", {
font: FONT,
fill: TrackLabColors.textOnDarkProperty,
});
const labelPanel = new Panel(angleText, {
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeProperty,
- cornerRadius: 4,
- xMargin: 6,
- yMargin: 3,
+ cornerRadius: LABEL_PANEL_CORNER_RADIUS,
+ xMargin: LABEL_PANEL_X_MARGIN,
+ yMargin: LABEL_PANEL_Y_MARGIN,
});
- labelPanel.setScaleMagnitude(0.8);
+ labelPanel.setScaleMagnitude(LABEL_PANEL_SCALE);
this.addChild(labelPanel);
// ── Geometry multilink ────────────────────────────────────────────────
@@ -183,7 +192,7 @@ export class AngleToolNode extends Node {
// Angle value
const angleDeg = (Math.abs(diff) * 180) / Math.PI;
- angleText.string = `${angleDeg.toFixed(1)}\u00b0`;
+ angleText.string = `${angleDeg.toFixed(ANGLE_DECIMAL_PLACES)}\u00b0`;
// Label along the bisector, outside the arc
const bisectorAngle = a1 + diff / 2;
@@ -196,21 +205,21 @@ export class AngleToolNode extends Node {
vertexNode.addInputListener(
new RichDragListener({
positionProperty: model.angleVertexProperty,
- keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED },
+ keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED },
tandem: Tandem.OPT_OUT,
}),
);
arm1Node.addInputListener(
new RichDragListener({
positionProperty: model.angleArm1Property,
- keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED },
+ keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED },
tandem: Tandem.OPT_OUT,
}),
);
arm2Node.addInputListener(
new RichDragListener({
positionProperty: model.angleArm2Property,
- keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED },
+ keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED },
tandem: Tandem.OPT_OUT,
}),
);
diff --git a/src/screen-name/view/CalibrationToolNode.ts b/src/screen-name/view/CalibrationToolNode.ts
index a4d7a32..1941cf7 100644
--- a/src/screen-name/view/CalibrationToolNode.ts
+++ b/src/screen-name/view/CalibrationToolNode.ts
@@ -5,7 +5,6 @@
* and specify its value in desired units to establish the model-to-pixel scale.
*/
-import { Color } from "scenerystack";
import type { TReadOnlyProperty } from "scenerystack/axon";
import { DerivedProperty, Multilink } from "scenerystack/axon";
import { Shape } from "scenerystack/kite";
@@ -16,7 +15,13 @@ import { ButtonNode, ComboBox, type ComboBoxItem, Panel, TextPushButton } from "
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
-import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, DIGITIZING_DIM_OPACITY } from "../../TrackLabConstants.js";
+import {
+ BUTTON_X_MARGIN,
+ BUTTON_Y_MARGIN,
+ DIGITIZING_DIM_OPACITY,
+ OVERLAY_DRAG_SPEED,
+ OVERLAY_SHIFT_DRAG_SPEED,
+} from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
import { CALIBRATION_UNITS } from "../model/SimModel.js";
@@ -37,11 +42,9 @@ 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
// Pixel distance below which endpoints are considered overlapping and a warning is shown.
const OVERLAP_WARNING_DISTANCE = 10;
-const ENDPOINT_WARNING_COLOR = new Color(255, 60, 60);
+const CALIBRATION_DECIMAL_PLACES = 2;
/**
* Two-endpoint calibration ruler overlay for setting the real-world scale.
@@ -136,7 +139,7 @@ export class CalibrationToolNode extends Node {
// Button showing current value + unit; clicking it opens the keypad.
const buttonLabelProperty = new DerivedProperty(
[model.calibDistanceProperty, model.calibUnitProperty],
- (dist, unit) => `${dist.toFixed(2)} ${unit}`,
+ (dist, unit) => `${dist.toFixed(CALIBRATION_DECIMAL_PLACES)} ${unit}`,
);
const distanceButton = new TextPushButton(buttonLabelProperty, {
@@ -199,7 +202,7 @@ export class CalibrationToolNode extends Node {
// Shown when endpoints are too close together to produce a valid calibration.
const overlapWarning = new Text(calibrationStrings.pointsTooCloseStringProperty, {
font: WARNING_FONT,
- fill: ENDPOINT_WARNING_COLOR,
+ fill: TrackLabColors.calibrationWarningColorProperty,
visible: false,
});
this.addChild(overlapWarning);
@@ -227,7 +230,9 @@ export class CalibrationToolNode extends Node {
// Show warning and highlight endpoints when too close to be useful.
const tooClose = p1.distance(p2) < OVERLAP_WARNING_DISTANCE;
- const endpointFill = tooClose ? ENDPOINT_WARNING_COLOR : TrackLabColors.calibrationFillProperty.value;
+ const endpointFill = tooClose
+ ? TrackLabColors.calibrationWarningColorProperty.value
+ : TrackLabColors.calibrationFillProperty.value;
endpoint1.fill = endpointFill;
endpoint2.fill = endpointFill;
overlapWarning.visible = tooClose;
@@ -246,8 +251,8 @@ export class CalibrationToolNode extends Node {
new RichDragListener({
positionProperty: model.calibPoint1Property,
keyboardDragListenerOptions: {
- dragSpeed: ENDPOINT_DRAG_SPEED,
- shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED,
+ dragSpeed: OVERLAY_DRAG_SPEED,
+ shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED,
},
tandem: Tandem.OPT_OUT,
}),
@@ -256,8 +261,8 @@ export class CalibrationToolNode extends Node {
new RichDragListener({
positionProperty: model.calibPoint2Property,
keyboardDragListenerOptions: {
- dragSpeed: ENDPOINT_DRAG_SPEED,
- shiftDragSpeed: ENDPOINT_SHIFT_DRAG_SPEED,
+ dragSpeed: OVERLAY_DRAG_SPEED,
+ shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED,
},
tandem: Tandem.OPT_OUT,
}),
diff --git a/src/screen-name/view/ControlPanel.ts b/src/screen-name/view/ControlPanel.ts
index e4c5501..e3298ed 100644
--- a/src/screen-name/view/ControlPanel.ts
+++ b/src/screen-name/view/ControlPanel.ts
@@ -8,12 +8,18 @@
import { Circle, Line, Node, VBox } from "scenerystack/scenery";
import { ArrowNode } from "scenerystack/scenery-phet";
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 { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
+import {
+ CONTROL_ICON_SIZE,
+ CONTROL_PANEL_ROWS_SPACING,
+ CONTROL_PANEL_X_MARGIN,
+ CONTROL_PANEL_Y_MARGIN,
+ PANEL_CORNER_RADIUS,
+} from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
-const ICON_SIZE = 20; // bounding box each icon targets
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;
@@ -22,36 +28,55 @@ 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];
-const PANEL_ROWS_SPACING = 12;
-const PANEL_X_MARGIN = 12;
-const PANEL_Y_MARGIN = 12;
+
+// ── 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, ICON_SIZE * 0.7, ICON_SIZE * 0.85, ICON_SIZE * 0.7, {
- 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, ICON_SIZE * 0.7, 0, ICON_SIZE * 0.05, {
- fill: TrackLabColors.axisYColorProperty,
- stroke: null,
- headWidth: ICON_ARROW_HEAD_SIZE,
- headHeight: ICON_ARROW_HEAD_SIZE,
- tailWidth: ICON_ARROW_TAIL_WIDTH,
- });
+ 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 = ICON_SIZE * 0.5;
- const cy = ICON_SIZE * 0.5;
- const half = ICON_SIZE * 0.4;
+ 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: [
@@ -68,7 +93,7 @@ function calibrationIcon(): Node {
/** Circle with a diagonal handle — magnifying glass silhouette. */
function magnifyIcon(): Node {
- const r = ICON_SIZE * 0.32;
+ const r = CONTROL_ICON_SIZE * ICON_MAGNIFIER_RADIUS_FRACTION;
const cx = r + 1;
const cy = r + 1;
const gray = TrackLabColors.iconGrayProperty;
@@ -81,20 +106,26 @@ function magnifyIcon(): Node {
x: cx,
y: cy,
}),
- new Line(cx + r * 0.7, cy + r * 0.7, ICON_SIZE - 1, ICON_SIZE - 1, {
- stroke: gray,
- lineWidth: ICON_LINE_WIDTH_MAGNIFIER,
- }),
+ 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 = ICON_SIZE * 0.5;
- const cy = ICON_SIZE * 0.5;
- const r = ICON_SIZE * 0.35;
- const gap = ICON_SIZE * 0.15;
+ 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: [
@@ -128,10 +159,11 @@ function trackingIcon(): Node {
// ── Helper ────────────────────────────────────────────────────────────────
-function makeRow(icon: Node, property: SimModel["axesVisibleProperty"]): Checkbox {
+function makeRow(icon: Node, property: SimModel["axesVisibleProperty"], accessibleName: string): Checkbox {
return new Checkbox(property, icon, {
checkboxColor: TrackLabColors.checkboxColorProperty,
checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty,
+ accessibleName,
});
}
@@ -148,19 +180,25 @@ export class ControlPanel extends Panel {
* @param trackLabPreferences - Determines whether the auto-tracking checkbox is shown.
*/
public constructor(model: SimModel, trackLabPreferences: TrackLabPreferencesModel) {
- const autoTrackingCheckbox = makeRow(trackingIcon(), model.autoTrackingProperty);
+ const a11yStrings = StringManager.getInstance().getA11y();
+
+ const autoTrackingCheckbox = makeRow(
+ trackingIcon(),
+ model.autoTrackingProperty,
+ a11yStrings.toggleAutoTrackingStringProperty.value,
+ );
// The auto-tracking checkbox is only visible if the preference allows it.
// When the preference is disabled, the checkbox is completely hidden from the panel.
autoTrackingCheckbox.visibleProperty = trackLabPreferences.enableAutoTrackingProperty;
const rows = new VBox({
children: [
- makeRow(axesIcon(), model.axesVisibleProperty),
- makeRow(calibrationIcon(), model.calibrationVisibleProperty),
- makeRow(magnifyIcon(), model.magnifyVideoProperty),
+ makeRow(axesIcon(), model.axesVisibleProperty, a11yStrings.toggleAxesStringProperty.value),
+ makeRow(calibrationIcon(), model.calibrationVisibleProperty, a11yStrings.toggleCalibrationStringProperty.value),
+ makeRow(magnifyIcon(), model.magnifyVideoProperty, a11yStrings.toggleMagnifierStringProperty.value),
autoTrackingCheckbox,
],
- spacing: PANEL_ROWS_SPACING,
+ spacing: CONTROL_PANEL_ROWS_SPACING,
align: "left",
});
@@ -168,8 +206,8 @@ export class ControlPanel extends Panel {
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeProperty,
cornerRadius: PANEL_CORNER_RADIUS,
- xMargin: PANEL_X_MARGIN,
- yMargin: PANEL_Y_MARGIN,
+ xMargin: CONTROL_PANEL_X_MARGIN,
+ yMargin: CONTROL_PANEL_Y_MARGIN,
});
}
}
diff --git a/src/screen-name/view/InfoDialogNode.ts b/src/screen-name/view/InfoDialogNode.ts
index dd2da70..c05d8ed 100644
--- a/src/screen-name/view/InfoDialogNode.ts
+++ b/src/screen-name/view/InfoDialogNode.ts
@@ -13,7 +13,7 @@ import { Panel } from "scenerystack/sun";
import { Tandem } from "scenerystack/tandem";
import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
-import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
+import { CONTROL_ICON_SIZE, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
import trackLab from "../../TrackLabNamespace.js";
// ── Layout constants ──────────────────────────────────────────────────────────
@@ -26,8 +26,27 @@ const STEP_BODY_FONT = new PhetFont(13);
const STEPS_SPACING = 12; // vertical gap between steps
const STEP_INNER_SPACING = 2; // gap between step title and body text
const CLOSE_BUTTON_ICON_LENGTH = 10;
-const ICON_SIZE = 20; // bounding box for step icons
const ICON_SPACING = 8; // gap between icon and text
+const TITLE_MAX_WIDTH_SPACING = 8; // gap between title text and close button
+
+// ── 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;
// ── Icon helpers ──────────────────────────────────────────────────────────────
@@ -42,45 +61,57 @@ function videoIcon(): Node {
.lineTo(18, 16)
.lineTo(2, 16)
.close();
- const folder = new Path(folderShape, { stroke: gray, lineWidth: 1.5, fill: null });
+ const folder = new Path(folderShape, { stroke: gray, lineWidth: 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, ICON_SIZE * 0.7, ICON_SIZE * 0.85, ICON_SIZE * 0.7, {
- fill: TrackLabColors.axisXColorProperty,
- stroke: null,
- headWidth: 5,
- headHeight: 5,
- tailWidth: 1.5,
- });
- const yArrow = new ArrowNode(0, ICON_SIZE * 0.7, 0, ICON_SIZE * 0.05, {
- fill: TrackLabColors.axisYColorProperty,
- stroke: null,
- headWidth: 5,
- headHeight: 5,
- tailWidth: 1.5,
- });
+ 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 = ICON_SIZE * 0.5;
- const cy = ICON_SIZE * 0.5;
- const half = ICON_SIZE * 0.4;
+ 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: 1.5,
- lineDash: [3, 2],
+ lineWidth: ICON_LINE_WIDTH_THICK,
+ lineDash: ICON_LINE_DASH,
}),
- new Circle(3, { fill: calColor, x: cx - half, y: cy }),
- new Circle(3, { fill: calColor, x: cx + half, y: cy }),
+ new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx - half, y: cy }),
+ new Circle(ICON_DOT_RADIUS, { fill: calColor, x: cx + half, y: cy }),
],
});
}
@@ -88,8 +119,8 @@ function calibrationIcon(): Node {
/** Plus sign for adding tracks. */
function addTrackIcon(): Node {
const gray = TrackLabColors.iconGrayProperty;
- const center = ICON_SIZE * 0.5;
- const size = ICON_SIZE * 0.6;
+ const center = CONTROL_ICON_SIZE * ICON_CENTER_FRACTION;
+ const size = CONTROL_ICON_SIZE * 0.6;
return new Node({
children: [
new Rectangle(center - 1, center - size / 2, 2, size, { fill: gray }),
@@ -100,7 +131,7 @@ function addTrackIcon(): Node {
/** Magnifying glass for digitizing. */
function magnifyIcon(): Node {
- const r = ICON_SIZE * 0.32;
+ const r = CONTROL_ICON_SIZE * ICON_MAGNIFIER_RADIUS_FRACTION;
const cx = r + 1;
const cy = r + 1;
const gray = TrackLabColors.iconGrayProperty;
@@ -108,36 +139,42 @@ function magnifyIcon(): Node {
children: [
new Circle(r, {
stroke: gray,
- lineWidth: 1.5,
+ lineWidth: ICON_LINE_WIDTH_THICK,
fill: null,
x: cx,
y: cy,
}),
- new Line(cx + r * 0.7, cy + r * 0.7, ICON_SIZE - 1, ICON_SIZE - 1, {
- stroke: gray,
- lineWidth: 2,
- }),
+ 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 centre dot for auto-tracking. */
function trackingIcon(): Node {
- const cx = ICON_SIZE * 0.5;
- const cy = ICON_SIZE * 0.5;
- const r = ICON_SIZE * 0.35;
- const gap = ICON_SIZE * 0.15;
+ 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: 1.5,
+ lineWidth: ICON_LINE_WIDTH_THICK,
fill: null,
x: cx,
y: cy,
}),
- new Circle(2, { fill: gray, 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 }),
@@ -153,13 +190,13 @@ function makeStep(icon: Node, titleProp: ReadOnlyProperty, bodyProp: Rea
const titleText = new Text(titleProp, {
font: STEP_TITLE_FONT,
fill: TrackLabColors.textOnDarkProperty,
- maxWidth: CONTENT_WIDTH - ICON_SIZE - ICON_SPACING,
+ maxWidth: CONTENT_WIDTH - CONTROL_ICON_SIZE - ICON_SPACING,
});
const bodyText = new RichText(bodyProp, {
font: STEP_BODY_FONT,
fill: TrackLabColors.textMutedProperty,
- lineWrap: CONTENT_WIDTH - ICON_SIZE - ICON_SPACING,
+ lineWrap: CONTENT_WIDTH - CONTROL_ICON_SIZE - ICON_SPACING,
});
const textBox = new VBox({
@@ -208,7 +245,7 @@ export class InfoDialogNode extends Node {
const headerNode = new Node({ children: [titleText, closeButton] });
closeButton.right = CONTENT_WIDTH;
closeButton.centerY = titleText.centerY;
- titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8;
+ titleText.maxWidth = CONTENT_WIDTH - closeButton.width - TITLE_MAX_WIDTH_SPACING;
// ── Steps ────────────────────────────────────────────────────────────────
const content = new VBox({
diff --git a/src/screen-name/view/KinematicsGraphNode.ts b/src/screen-name/view/KinematicsGraphNode.ts
index 3557102..5b9bfda 100644
--- a/src/screen-name/view/KinematicsGraphNode.ts
+++ b/src/screen-name/view/KinematicsGraphNode.ts
@@ -10,6 +10,7 @@ import { Property } from "scenerystack/axon";
import { Node, Text, VBox } from "scenerystack/scenery";
import { PhetFont } from "scenerystack/scenery-phet";
import { Checkbox } from "scenerystack/sun";
+import { StringManager } from "../../i18n/StringManager.js";
import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js";
import TrackLabColors from "../../TrackLabColors.js";
import ConfigurableGraph from "../graph/ConfigurableGraph.js";
@@ -36,7 +37,12 @@ export class KinematicsGraphNode extends Node {
private readonly disposeKinematicsGraph: () => void;
public constructor(model: SimModel, listParent: Node, preferencesModel: TrackLabPreferencesModel) {
- super({ visible: false });
+ const a11yStrings = StringManager.getInstance().getA11y();
+ super({
+ visible: false,
+ tagName: "div",
+ accessibleName: a11yStrings.kinematicsGraphStringProperty,
+ });
this.model = model;
this.selectedTracksProperty = new Property>(new Set());
@@ -227,11 +233,13 @@ export class KinematicsGraphNode extends Node {
fill: track.color,
});
- // Create checkbox
+ // Create checkbox with accessibility label
+ const kinematicsGraphStrings = StringManager.getInstance().getKinematicsGraph();
const checkbox = new Checkbox(checkboxProperty, label, {
checkboxColor: TrackLabColors.checkboxColorProperty,
checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty,
spacing: 4,
+ accessibleName: kinematicsGraphStrings.trackItemStringProperty.value.replace("{{symbol}}", track.symbol),
});
// Store for later disposal
@@ -263,8 +271,9 @@ export class KinematicsGraphNode extends Node {
const checkboxContainer = this.trackCheckboxPanel.children[0];
if (checkboxContainer) {
// Position relative to graph's current dimensions
- checkboxContainer.right = this.graph.getGraphWidth() - 8;
- checkboxContainer.top = 8;
+ const CHECKBOX_INSET = 8;
+ checkboxContainer.right = this.graph.getGraphWidth() - CHECKBOX_INSET;
+ checkboxContainer.top = CHECKBOX_INSET;
}
}
diff --git a/src/screen-name/view/MeasurementToolsPanel.ts b/src/screen-name/view/MeasurementToolsPanel.ts
index 406b267..f1e18ef 100644
--- a/src/screen-name/view/MeasurementToolsPanel.ts
+++ b/src/screen-name/view/MeasurementToolsPanel.ts
@@ -13,51 +13,77 @@
import { Shape } from "scenerystack/kite";
import { Circle, Line, Node, Path, VBox } from "scenerystack/scenery";
import { Checkbox, Panel } from "scenerystack/sun";
+import { StringManager } from "../../i18n/StringManager.js";
import TrackLabColors from "../../TrackLabColors.js";
-import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js";
+import {
+ CONTROL_ICON_SIZE,
+ CONTROL_PANEL_ROWS_SPACING,
+ CONTROL_PANEL_X_MARGIN,
+ CONTROL_PANEL_Y_MARGIN,
+ PANEL_CORNER_RADIUS,
+} from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
-const ICON_SIZE = 20;
-const PANEL_ROWS_SPACING = 12;
-const PANEL_X_MARGIN = 12;
-const PANEL_Y_MARGIN = 12;
+// ── Icon geometry constants ──────────────────────────────────────────────────
+const ICON_EDGE_INSET = 1;
+const ICON_TAPE_BODY_LINE_WIDTH = 2.5;
+const ICON_REEL_RADIUS = 4;
+const ICON_REEL_OUTLINE_WIDTH = 1;
+const ICON_REEL_DOT_RADIUS = 1.5;
+const ICON_TIP_RADIUS = 2.5;
+const ICON_TICK_LENGTH = 4;
+const ICON_TICK_HALF_LENGTH = 2.5;
+const ICON_TICK_LINE_WIDTH = 1.5;
+const ICON_ARM_LINE_WIDTH = 1.5;
+const ICON_ARC_RADIUS = 7;
// ── Icons ─────────────────────────────────────────────────────────────────────
/** Measuring tape: a horizontal line with a reel circle at one end and a tick
* mark at each end plus the midpoint to evoke a tape measure. */
function measuringTapeIcon(): Node {
- const cy = ICON_SIZE / 2;
- const color = "rgb(240, 185, 55)";
- const x0 = 1;
- const x1 = ICON_SIZE - 1;
- const xMid = ICON_SIZE / 2;
+ const cy = CONTROL_ICON_SIZE / 2;
+ const x0 = ICON_EDGE_INSET;
+ const x1 = CONTROL_ICON_SIZE - ICON_EDGE_INSET;
+ const xMid = CONTROL_ICON_SIZE / 2;
return new Node({
children: [
// Tape body line
- new Line(x0, cy, x1, cy, { stroke: color, lineWidth: 2.5 }),
+ new Line(x0, cy, x1, cy, {
+ stroke: TrackLabColors.measuringTapeColorProperty,
+ lineWidth: ICON_TAPE_BODY_LINE_WIDTH,
+ }),
// Reel circle (left end)
- new Circle(4, {
- fill: color,
- stroke: "rgba(0,0,0,0.5)",
- lineWidth: 1,
+ new Circle(ICON_REEL_RADIUS, {
+ fill: TrackLabColors.measuringTapeColorProperty,
+ stroke: TrackLabColors.iconShadowProperty,
+ lineWidth: ICON_REEL_OUTLINE_WIDTH,
x: x0,
y: cy,
}),
// Inner dot of reel
- new Circle(1.5, { fill: "rgba(0,0,0,0.5)", x: x0, y: cy }),
+ new Circle(ICON_REEL_DOT_RADIUS, { fill: TrackLabColors.iconShadowProperty, x: x0, y: cy }),
// Right tip circle
- new Circle(2.5, {
- fill: color,
- stroke: "rgba(0,0,0,0.5)",
- lineWidth: 1,
+ new Circle(ICON_TIP_RADIUS, {
+ fill: TrackLabColors.measuringTapeColorProperty,
+ stroke: TrackLabColors.iconShadowProperty,
+ lineWidth: ICON_REEL_OUTLINE_WIDTH,
x: x1,
y: cy,
}),
// Tick marks at left, mid, right
- new Line(x0, cy - 4, x0, cy + 4, { stroke: color, lineWidth: 1.5 }),
- new Line(xMid, cy - 2.5, xMid, cy + 2.5, { stroke: color, lineWidth: 1.5 }),
- new Line(x1, cy - 4, x1, cy + 4, { stroke: color, lineWidth: 1.5 }),
+ new Line(x0, cy - ICON_TICK_LENGTH, x0, cy + ICON_TICK_LENGTH, {
+ stroke: TrackLabColors.measuringTapeColorProperty,
+ lineWidth: ICON_TICK_LINE_WIDTH,
+ }),
+ new Line(xMid, cy - ICON_TICK_HALF_LENGTH, xMid, cy + ICON_TICK_HALF_LENGTH, {
+ stroke: TrackLabColors.measuringTapeColorProperty,
+ lineWidth: ICON_TICK_LINE_WIDTH,
+ }),
+ new Line(x1, cy - ICON_TICK_LENGTH, x1, cy + ICON_TICK_LENGTH, {
+ stroke: TrackLabColors.measuringTapeColorProperty,
+ lineWidth: ICON_TICK_LINE_WIDTH,
+ }),
],
});
}
@@ -65,33 +91,32 @@ function measuringTapeIcon(): Node {
/** Angle tool: two lines meeting at a bottom-left vertex with a small arc
* showing the angle between them. */
function angleToolIcon(): Node {
- const color = "rgb(170, 100, 255)";
// Vertex at bottom-left, arm1 goes up-right, arm2 goes right
const vx = 2;
- const vy = ICON_SIZE - 2;
- const a1x = ICON_SIZE - 2;
+ const vy = CONTROL_ICON_SIZE - 2;
+ const a1x = CONTROL_ICON_SIZE - 2;
const a1y = 2;
- const a2x = ICON_SIZE - 2;
- const a2y = ICON_SIZE - 2;
- const arcRadius = 7;
+ const a2x = CONTROL_ICON_SIZE - 2;
+ const a2y = CONTROL_ICON_SIZE - 2;
const angle1 = Math.atan2(a1y - vy, a1x - vx);
const angle2 = Math.atan2(a2y - vy, a2x - vx);
- const arcShape = new Shape().arc(vx, vy, arcRadius, angle1, angle2, false);
+ const arcShape = new Shape().arc(vx, vy, ICON_ARC_RADIUS, angle1, angle2, false);
return new Node({
children: [
- new Line(vx, vy, a1x, a1y, { stroke: color, lineWidth: 1.5 }),
- new Line(vx, vy, a2x, a2y, { stroke: color, lineWidth: 1.5 }),
- new Path(arcShape, { stroke: color, lineWidth: 1.5 }),
+ new Line(vx, vy, a1x, a1y, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }),
+ new Line(vx, vy, a2x, a2y, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }),
+ new Path(arcShape, { stroke: TrackLabColors.angleToolColorProperty, lineWidth: ICON_ARM_LINE_WIDTH }),
],
});
}
// ── Helper ─────────────────────────────────────────────────────────────────────
-function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"]): Checkbox {
+function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"], accessibleName: string): Checkbox {
return new Checkbox(property, icon, {
checkboxColor: TrackLabColors.checkboxColorProperty,
checkboxColorBackground: TrackLabColors.checkboxColorBackgroundProperty,
+ accessibleName,
});
}
@@ -103,12 +128,18 @@ function makeRow(icon: Node, property: SimModel["measuringTapeVisibleProperty"])
*/
export class MeasurementToolsPanel extends Panel {
public constructor(model: SimModel) {
+ const a11yStrings = StringManager.getInstance().getA11y();
+
const rows = new VBox({
children: [
- makeRow(measuringTapeIcon(), model.measuringTapeVisibleProperty),
- makeRow(angleToolIcon(), model.angleToolVisibleProperty),
+ makeRow(
+ measuringTapeIcon(),
+ model.measuringTapeVisibleProperty,
+ a11yStrings.toggleMeasuringTapeStringProperty.value,
+ ),
+ makeRow(angleToolIcon(), model.angleToolVisibleProperty, a11yStrings.toggleAngleToolStringProperty.value),
],
- spacing: PANEL_ROWS_SPACING,
+ spacing: CONTROL_PANEL_ROWS_SPACING,
align: "left",
});
@@ -116,8 +147,8 @@ export class MeasurementToolsPanel extends Panel {
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeProperty,
cornerRadius: PANEL_CORNER_RADIUS,
- xMargin: PANEL_X_MARGIN,
- yMargin: PANEL_Y_MARGIN,
+ xMargin: CONTROL_PANEL_X_MARGIN,
+ yMargin: CONTROL_PANEL_Y_MARGIN,
});
}
}
diff --git a/src/screen-name/view/MeasuringTapeNode.ts b/src/screen-name/view/MeasuringTapeNode.ts
index 837b3b6..e02a054 100644
--- a/src/screen-name/view/MeasuringTapeNode.ts
+++ b/src/screen-name/view/MeasuringTapeNode.ts
@@ -15,24 +15,29 @@ import { Circle, Line, Node, RichDragListener, Text } from "scenerystack/scenery
import { 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 {
+ LABEL_PANEL_CORNER_RADIUS,
+ LABEL_PANEL_SCALE,
+ LABEL_PANEL_X_MARGIN,
+ LABEL_PANEL_Y_MARGIN,
+ OVERLAY_DRAG_SPEED,
+ OVERLAY_SHIFT_DRAG_SPEED,
+ OVERLAY_TOUCH_DILATION,
+} from "../../TrackLabConstants.js";
import type { SimModel } from "../model/SimModel.js";
// ── Visual constants ──────────────────────────────────────────────────────────
-const TAPE_COLOR = "rgb(240, 185, 55)";
-const TAPE_SHADOW_COLOR = "rgba(0, 0, 0, 0.45)";
const TAPE_LINE_WIDTH = 4;
const TAPE_SHADOW_WIDTH = 7;
const TAPE_DASH: number[] = [10, 5];
-
const ENDPOINT_RADIUS = 7;
-const TOUCH_DILATION = 12;
-
+const ENDPOINT_DOT_RADIUS = 2.5;
+const ENDPOINT_OUTLINE_WIDTH = 1.5;
const FONT = new PhetFont({ size: 12, weight: "bold" });
const LABEL_Y_OFFSET = 16; // pixels above the midpoint
-
-const DRAG_SPEED = 200;
-const SHIFT_DRAG_SPEED = 40;
+const DISTANCE_DECIMAL_PLACES = 2;
/**
* Two-endpoint measuring tape overlay. Both endpoints are independently draggable.
@@ -44,28 +49,32 @@ export class MeasuringTapeNode extends Node {
public constructor(visibleProperty: TReadOnlyProperty, model: SimModel) {
super();
+ const a11yStrings = StringManager.getInstance().getA11y();
+
// ── Shadow line (contrast on any background) ──────────────────────────
const shadowLine = new Line(0, 0, 0, 0, {
- stroke: TAPE_SHADOW_COLOR,
+ stroke: TrackLabColors.measuringTapeShadowProperty,
lineWidth: TAPE_SHADOW_WIDTH,
});
this.addChild(shadowLine);
// ── Main tape line ────────────────────────────────────────────────────
const tapeLine = new Line(0, 0, 0, 0, {
- stroke: TAPE_COLOR,
+ stroke: TrackLabColors.measuringTapeColorProperty,
lineWidth: TAPE_LINE_WIDTH,
lineDash: TAPE_DASH,
});
this.addChild(tapeLine);
- const makeEndpoint = (accessibleName: string) => {
+ const makeEndpoint = (accessibleName: TReadOnlyProperty) => {
const endpointCircle = new Circle(ENDPOINT_RADIUS, {
- fill: TAPE_COLOR,
- stroke: "rgba(0, 0, 0, 0.65)",
- lineWidth: 1.5,
+ fill: TrackLabColors.measuringTapeColorProperty,
+ stroke: TrackLabColors.overlayHandleOutlineProperty,
+ lineWidth: ENDPOINT_OUTLINE_WIDTH,
+ });
+ const endpointDot = new Circle(ENDPOINT_DOT_RADIUS, {
+ fill: TrackLabColors.overlayHandleOutlineProperty,
});
- const endpointDot = new Circle(2.5, { fill: "rgba(0, 0, 0, 0.65)" });
const endpointNode = new Node({
children: [endpointCircle, endpointDot],
cursor: "grab",
@@ -73,31 +82,31 @@ export class MeasuringTapeNode extends Node {
focusable: true,
accessibleName,
});
- const endpointTouchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + TOUCH_DILATION);
+ const endpointTouchArea = Shape.circle(0, 0, ENDPOINT_RADIUS + OVERLAY_TOUCH_DILATION);
endpointNode.mouseArea = endpointTouchArea;
endpointNode.touchArea = endpointTouchArea;
return endpointNode;
};
// ── Symmetric endpoints ────────────────────────────────────────────────
- const endpoint1Node = makeEndpoint("Measuring tape base");
- const endpoint2Node = makeEndpoint("Measuring tape tip");
+ const endpoint1Node = makeEndpoint(a11yStrings.measuringTapeBaseStringProperty);
+ const endpoint2Node = makeEndpoint(a11yStrings.measuringTapeTipStringProperty);
this.addChild(endpoint1Node);
this.addChild(endpoint2Node);
// ── Distance label ────────────────────────────────────────────────────
- const distanceText = new Text("---", {
+ const distanceText = new Text("", {
font: FONT,
fill: TrackLabColors.textOnDarkProperty,
});
const labelPanel = new Panel(distanceText, {
fill: TrackLabColors.panelFillProperty,
stroke: TrackLabColors.panelStrokeProperty,
- cornerRadius: 4,
- xMargin: 6,
- yMargin: 3,
+ cornerRadius: LABEL_PANEL_CORNER_RADIUS,
+ xMargin: LABEL_PANEL_X_MARGIN,
+ yMargin: LABEL_PANEL_Y_MARGIN,
});
- labelPanel.setScaleMagnitude(0.8);
+ labelPanel.setScaleMagnitude(LABEL_PANEL_SCALE);
this.addChild(labelPanel);
// ── Geometry + label multilinks ───────────────────────────────────────
@@ -116,7 +125,7 @@ export class MeasuringTapeNode extends Node {
(p1, p2, mvt, unit) => {
const m1 = mvt.inversePosition2(p1);
const m2 = mvt.inversePosition2(p2);
- distanceText.string = `${m1.distance(m2).toFixed(2)} ${unit}`;
+ distanceText.string = `${m1.distance(m2).toFixed(DISTANCE_DECIMAL_PLACES)} ${unit}`;
},
);
@@ -124,14 +133,14 @@ export class MeasuringTapeNode extends Node {
endpoint1Node.addInputListener(
new RichDragListener({
positionProperty: model.tapPoint1Property,
- keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED },
+ keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED },
tandem: Tandem.OPT_OUT,
}),
);
endpoint2Node.addInputListener(
new RichDragListener({
positionProperty: model.tapPoint2Property,
- keyboardDragListenerOptions: { dragSpeed: DRAG_SPEED, shiftDragSpeed: SHIFT_DRAG_SPEED },
+ keyboardDragListenerOptions: { dragSpeed: OVERLAY_DRAG_SPEED, shiftDragSpeed: OVERLAY_SHIFT_DRAG_SPEED },
tandem: Tandem.OPT_OUT,
}),
);
|