From c677e683585f094f1feda77d4933f90d446027a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 12:43:08 +0000 Subject: [PATCH 1/4] Add info button and how-to-digitize modal dialog Adds a '?' button in the lower-left corner (mirroring the reset button margin) that toggles a floating InfoDialogNode explaining the six main steps of the digitizing workflow: load video, set up coordinate system, calibrate scale, add a track, digitize frame by frame, and auto-track. - InfoDialogNode.ts: new Panel-based modal with title, separator, step-by-step instructions, and a CloseButton - SimScreenView.ts: wires the info button + dialog; positions both in the visibleBoundsProperty link so they track the actual visible area - StringManager.ts: adds getInfoDialog() accessor - strings_en.json / strings_fr.json: adds infoDialog string group (title + 6 step title/body pairs) in both languages https://claude.ai/code/session_019asowJWZYPgvgsefumiFmG --- src/i18n/StringManager.ts | 35 +++++++ src/i18n/strings_en.json | 15 +++ src/i18n/strings_fr.json | 15 +++ src/screen-name/view/InfoDialogNode.ts | 128 +++++++++++++++++++++++++ src/screen-name/view/SimScreenView.ts | 41 +++++++- 5 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 src/screen-name/view/InfoDialogNode.ts diff --git a/src/i18n/StringManager.ts b/src/i18n/StringManager.ts index 87d6b7f..6386305 100644 --- a/src/i18n/StringManager.ts +++ b/src/i18n/StringManager.ts @@ -421,6 +421,41 @@ export class StringManager { }; } + /** + * Get info dialog string properties + */ + public getInfoDialog(): { + titleStringProperty: ReadOnlyProperty; + loadVideoTitleStringProperty: ReadOnlyProperty; + loadVideoBodyStringProperty: ReadOnlyProperty; + coordinateSystemTitleStringProperty: ReadOnlyProperty; + coordinateSystemBodyStringProperty: ReadOnlyProperty; + calibrationTitleStringProperty: ReadOnlyProperty; + calibrationBodyStringProperty: ReadOnlyProperty; + addTrackTitleStringProperty: ReadOnlyProperty; + addTrackBodyStringProperty: ReadOnlyProperty; + digitizeTitleStringProperty: ReadOnlyProperty; + digitizeBodyStringProperty: ReadOnlyProperty; + autoTrackTitleStringProperty: ReadOnlyProperty; + autoTrackBodyStringProperty: ReadOnlyProperty; + } { + return { + titleStringProperty: this.stringProperties.infoDialog.titleStringProperty, + loadVideoTitleStringProperty: this.stringProperties.infoDialog.loadVideoTitleStringProperty, + loadVideoBodyStringProperty: this.stringProperties.infoDialog.loadVideoBodyStringProperty, + coordinateSystemTitleStringProperty: this.stringProperties.infoDialog.coordinateSystemTitleStringProperty, + coordinateSystemBodyStringProperty: this.stringProperties.infoDialog.coordinateSystemBodyStringProperty, + calibrationTitleStringProperty: this.stringProperties.infoDialog.calibrationTitleStringProperty, + calibrationBodyStringProperty: this.stringProperties.infoDialog.calibrationBodyStringProperty, + addTrackTitleStringProperty: this.stringProperties.infoDialog.addTrackTitleStringProperty, + addTrackBodyStringProperty: this.stringProperties.infoDialog.addTrackBodyStringProperty, + digitizeTitleStringProperty: this.stringProperties.infoDialog.digitizeTitleStringProperty, + digitizeBodyStringProperty: this.stringProperties.infoDialog.digitizeBodyStringProperty, + autoTrackTitleStringProperty: this.stringProperties.infoDialog.autoTrackTitleStringProperty, + autoTrackBodyStringProperty: this.stringProperties.infoDialog.autoTrackBodyStringProperty, + }; + } + /** * Get video file label string properties */ diff --git a/src/i18n/strings_en.json b/src/i18n/strings_en.json index 622bec5..2423408 100644 --- a/src/i18n/strings_en.json +++ b/src/i18n/strings_en.json @@ -138,6 +138,21 @@ "pucksCollide": "Pucks Collide", "springWars": "Spring Wars" }, + "infoDialog": { + "title": "How to Digitize a Track", + "loadVideoTitle": "1. Load a Video", + "loadVideoBody": "Select a video from the dropdown at the top \u2014 choose a sample clip, a webcam recording, or an uploaded file.", + "coordinateSystemTitle": "2. Set Up the Coordinate System", + "coordinateSystemBody": "Enable Axes in the control panel. Drag the origin to set the (0, 0) point, then rotate the axes to align with your scene.", + "calibrationTitle": "3. Calibrate the Scale", + "calibrationBody": "Enable Calibration in the control panel. Drag the two endpoints onto a known reference distance in the video, then enter the real-world length.", + "addTrackTitle": "4. Add a Track", + "addTrackBody": "In the Tracks panel, click \u201c+ Add Track\u201d to create a new track labelled A, B, C, \u2026", + "digitizeTitle": "5. Digitize Frame by Frame", + "digitizeBody": "Select a track and click on the object\u2019s position in the video. The video advances one frame automatically after each click.", + "autoTrackTitle": "6. Auto-Track (optional)", + "autoTrackBody": "Enable Auto-Tracking in the control panel, then drag a selection box around the object. TrackLab will track it across the remaining frames." + }, "a11y": { "videoPlayer": "Video player", "videoScrubber": "Video timeline — drag to seek", diff --git a/src/i18n/strings_fr.json b/src/i18n/strings_fr.json index 8bb4aba..d6e82a6 100644 --- a/src/i18n/strings_fr.json +++ b/src/i18n/strings_fr.json @@ -138,6 +138,21 @@ "pucksCollide": "Rondelles en collision", "springWars": "Guerres de ressorts" }, + "infoDialog": { + "title": "Comment num\u00e9riser une piste", + "loadVideoTitle": "1. Charger une vid\u00e9o", + "loadVideoBody": "S\u00e9lectionnez une vid\u00e9o dans le menu d\u00e9roulant en haut \u2014 choisissez un exemple, un enregistrement webcam ou un fichier import\u00e9.", + "coordinateSystemTitle": "2. Configurer le syst\u00e8me de coordonn\u00e9es", + "coordinateSystemBody": "Activez les Axes dans le panneau de contr\u00f4le. Faites glisser l\u2019origine pour d\u00e9finir le point (0,\u00a00), puis faites pivoter les axes pour les aligner sur votre sc\u00e8ne.", + "calibrationTitle": "3. Calibrer l\u2019\u00e9chelle", + "calibrationBody": "Activez la Calibration dans le panneau de contr\u00f4le. Faites glisser les deux extr\u00e9mit\u00e9s sur une distance de r\u00e9f\u00e9rence connue dans la vid\u00e9o, puis entrez la longueur r\u00e9elle.", + "addTrackTitle": "4. Ajouter une piste", + "addTrackBody": "Dans le panneau Pistes, cliquez sur \u00ab\u00a0+ Ajouter une piste\u00a0\u00bb pour cr\u00e9er une nouvelle piste \u00e9tiquet\u00e9e A, B, C, \u2026", + "digitizeTitle": "5. Num\u00e9riser image par image", + "digitizeBody": "S\u00e9lectionnez une piste et cliquez sur la position de l\u2019objet dans la vid\u00e9o. La vid\u00e9o avance automatiquement d\u2019une image apr\u00e8s chaque clic.", + "autoTrackTitle": "6. Suivi automatique (optionnel)", + "autoTrackBody": "Activez le Suivi automatique dans le panneau de contr\u00f4le, puis faites glisser un cadre de s\u00e9lection autour de l\u2019objet. TrackLab le suivra sur les images restantes." + }, "a11y": { "videoPlayer": "Lecteur vid\u00e9o", "videoScrubber": "Ligne de temps vid\u00e9o \u2014 glisser pour naviguer", diff --git a/src/screen-name/view/InfoDialogNode.ts b/src/screen-name/view/InfoDialogNode.ts new file mode 100644 index 0000000..a15f48b --- /dev/null +++ b/src/screen-name/view/InfoDialogNode.ts @@ -0,0 +1,128 @@ +/** + * InfoDialogNode.ts + * + * Modal dialog explaining the main steps for digitizing a track in TrackLab. + * Toggled by the info button in the lower-left corner of the screen. + */ + +import type { ReadOnlyProperty } from "scenerystack/axon"; +import { Node, 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 { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import trackLab from "../../TrackLabNamespace.js"; + +// ── Layout constants ────────────────────────────────────────────────────────── +const CONTENT_WIDTH = 370; // inner width of the panel content area +const PANEL_X_MARGIN = 18; +const PANEL_Y_MARGIN = 16; +const TITLE_FONT = new PhetFont({ size: 15, weight: "bold" }); +const STEP_TITLE_FONT = new PhetFont({ size: 13, weight: "bold" }); +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 SEPARATOR_MARGIN = 6; // gap above/below the horizontal rule +const CLOSE_BUTTON_ICON_LENGTH = 10; + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +/** A thin horizontal rule separating the title from the steps. */ +function makeSeparator(): Rectangle { + return new Rectangle(0, 0, CONTENT_WIDTH, 1, { + fill: TrackLabColors.panelStrokeLightProperty, + }); +} + +/** One step: bold heading above a softer-colored description. */ +function makeStep(titleProp: ReadOnlyProperty, bodyProp: ReadOnlyProperty): Node { + const titleText = new Text(titleProp, { + font: STEP_TITLE_FONT, + fill: TrackLabColors.textOnDarkProperty, + maxWidth: CONTENT_WIDTH, + }); + + const bodyText = new RichText(bodyProp, { + font: STEP_BODY_FONT, + fill: TrackLabColors.textMutedProperty, + lineWrap: CONTENT_WIDTH, + }); + + return new VBox({ + children: [titleText, bodyText], + spacing: STEP_INNER_SPACING, + align: "left", + }); +} + +// ── InfoDialogNode ──────────────────────────────────────────────────────────── + +/** + * Floating modal explaining how to digitize a track. + * + * Hidden by default (`visible = false`). Show by setting `visible = true`; + * the internal close button hides it again. + */ +export class InfoDialogNode extends Node { + public constructor() { + super({ visible: false }); + + const strings = StringManager.getInstance().getInfoDialog(); + + // ── Header: title + close button ───────────────────────────────────────── + const titleText = new Text(strings.titleStringProperty, { + font: TITLE_FONT, + fill: TrackLabColors.textOnDarkProperty, + }); + + const closeButton = new CloseButton({ + listener: () => { + this.visible = false; + }, + baseColor: TrackLabColors.buttonBaseDarkProperty, + iconLength: CLOSE_BUTTON_ICON_LENGTH, + tandem: Tandem.OPT_OUT, + }); + + // Lay out title and close button side-by-side, close button flush right. + const headerNode = new Node({ children: [titleText, closeButton] }); + closeButton.right = CONTENT_WIDTH; + closeButton.centerY = titleText.centerY; + titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8; + + // ── Steps ──────────────────────────────────────────────────────────────── + const steps = [ + makeStep(strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty), + makeStep(strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty), + makeStep(strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), + makeStep(strings.addTrackTitleStringProperty, strings.addTrackBodyStringProperty), + makeStep(strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty), + makeStep(strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty), + ]; + + // ── Content layout ─────────────────────────────────────────────────────── + // Spacer nodes give extra breathing room around the separator. + const separatorTop = new Rectangle(0, 0, 0, SEPARATOR_MARGIN); + const separatorBottom = new Rectangle(0, 0, 0, SEPARATOR_MARGIN); + + const content = new VBox({ + children: [headerNode, separatorTop, makeSeparator(), separatorBottom, ...steps], + spacing: STEPS_SPACING, + align: "left", + }); + + const panel = new Panel(content, { + fill: TrackLabColors.panelFillProperty, + stroke: TrackLabColors.panelStrokeProperty, + cornerRadius: PANEL_CORNER_RADIUS, + xMargin: PANEL_X_MARGIN, + yMargin: PANEL_Y_MARGIN, + }); + + this.addChild(panel); + } +} + +trackLab.register("InfoDialogNode", InfoDialogNode); diff --git a/src/screen-name/view/SimScreenView.ts b/src/screen-name/view/SimScreenView.ts index 21fc5d1..08d71e9 100644 --- a/src/screen-name/view/SimScreenView.ts +++ b/src/screen-name/view/SimScreenView.ts @@ -7,16 +7,20 @@ import { DerivedProperty } from "scenerystack/axon"; import { Vector2 } from "scenerystack/dot"; -import { DragListener } from "scenerystack/scenery"; -import { ResetAllButton } from "scenerystack/scenery-phet"; +import { DragListener, Text } from "scenerystack/scenery"; +import { PhetFont, ResetAllButton } from "scenerystack/scenery-phet"; +import { ButtonNode, RectangularPushButton } from "scenerystack/sun"; import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; +import { Tandem } from "scenerystack/tandem"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; -import { CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js"; +import TrackLabColors from "../../TrackLabColors.js"; +import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import { CalibrationToolNode } from "./CalibrationToolNode.js"; import { ControlPanel } from "./ControlPanel.js"; import { CoordinateSystemNode } from "./CoordinateSystemNode.js"; import { DataTableNode } from "./DataTableNode.js"; +import { InfoDialogNode } from "./InfoDialogNode.js"; import { KinematicsGraphNode } from "./KinematicsGraphNode.js"; import { TrackListPanel } from "./TrackListPanel.js"; import { VideoPlayerNode } from "./VideoPlayerNode.js"; @@ -118,6 +122,29 @@ export class SimScreenView extends ScreenView { const kinematicsGraph = new KinematicsGraphNode(model, this, trackLabPreferences); this.addChild(kinematicsGraph); + // ── Info dialog (explains digitizing workflow) ──────────────────────────── + const infoDialogNode = new InfoDialogNode(); + this.addChild(infoDialogNode); + + // ── Info button (lower-left corner, same vertical level as reset button) ─ + const infoButton = new RectangularPushButton({ + content: new Text("?", { + font: new PhetFont({ size: 16, weight: "bold" }), + fill: TrackLabColors.textOnDarkProperty, + }), + baseColor: TrackLabColors.buttonBaseDarkProperty, + buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, + xMargin: BUTTON_X_MARGIN + 2, + yMargin: BUTTON_Y_MARGIN, + cornerRadius: 14, + tandem: Tandem.OPT_OUT, + accessibleName: "How to use TrackLab", + listener: () => { + infoDialogNode.visible = !infoDialogNode.visible; + }, + }); + this.addChild(infoButton); + // ── Webcam panel (topmost when visible, above coord/calibration overlays) ─ const webcamPanel = this.videoPlayerNode.webcamPanel; this.addChild(webcamPanel); @@ -138,6 +165,14 @@ export class SimScreenView extends ScreenView { resetAllButton.right = visibleBounds.maxX - RESET_BUTTON_MARGIN; resetAllButton.bottom = visibleBounds.maxY - RESET_BUTTON_MARGIN; + // Info button: lower-left corner, mirroring the reset button margin. + infoButton.left = visibleBounds.minX + RESET_BUTTON_MARGIN; + infoButton.centerY = resetAllButton.centerY; + + // Info dialog: centered horizontally, positioned just above the info button. + infoDialogNode.centerX = this.layoutBounds.centerX; + infoDialogNode.bottom = infoButton.top - RESET_BUTTON_MARGIN; + // Data table: shift left by extraWidth so it stays within the layout area // and doesn't drift into the extra visible space claimed by the graph. dataTableNode.left = this.videoPlayerNode.right + DATA_TABLE_LEFT_SPACING - extraWidth; From 8b5aa4ea52ace9b630982a11f712aafc93d28d90 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 13:41:58 +0000 Subject: [PATCH 2/4] Use InfoButton and Dialog from scenerystack instead of custom widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-rolled '?' RectangularPushButton with the standard InfoButton (RoundPushButton with circle-i icon) from scenerystack/scenery-phet, and replace the custom Panel-based InfoDialogNode with a proper Dialog subclass from scenerystack/sun. Key changes: - InfoDialogNode now extends Dialog; title passed as option, built-in CloseButton replaces the manual one, Panel/Rectangle/separator removed - SimScreenView: imports InfoButton instead of ButtonNode/RectangularPushButton; listener calls infoDialogNode.show() — Dialog uses phet.joist.sim's popup layer so no addChild or manual positioning needed - Removed now-unused imports: Text, PhetFont, ButtonNode, RectangularPushButton, TrackLabColors, BUTTON_X_MARGIN, BUTTON_Y_MARGIN from SimScreenView https://claude.ai/code/session_019asowJWZYPgvgsefumiFmG --- src/screen-name/view/InfoDialogNode.ts | 84 ++++++++------------------ src/screen-name/view/SimScreenView.ts | 30 ++------- 2 files changed, 31 insertions(+), 83 deletions(-) diff --git a/src/screen-name/view/InfoDialogNode.ts b/src/screen-name/view/InfoDialogNode.ts index a15f48b..c5da516 100644 --- a/src/screen-name/view/InfoDialogNode.ts +++ b/src/screen-name/view/InfoDialogNode.ts @@ -2,13 +2,13 @@ * InfoDialogNode.ts * * Modal dialog explaining the main steps for digitizing a track in TrackLab. - * Toggled by the info button in the lower-left corner of the screen. + * Shown by the InfoButton in the lower-left corner of the screen. */ import type { ReadOnlyProperty } from "scenerystack/axon"; -import { Node, Rectangle, RichText, Text, VBox } from "scenerystack/scenery"; -import { CloseButton, PhetFont } from "scenerystack/scenery-phet"; -import { Panel } from "scenerystack/sun"; +import { Node, RichText, Text, VBox } from "scenerystack/scenery"; +import { PhetFont } from "scenerystack/scenery-phet"; +import { Dialog } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; @@ -16,26 +16,15 @@ import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import trackLab from "../../TrackLabNamespace.js"; // ── Layout constants ────────────────────────────────────────────────────────── -const CONTENT_WIDTH = 370; // inner width of the panel content area -const PANEL_X_MARGIN = 18; -const PANEL_Y_MARGIN = 16; +const CONTENT_WIDTH = 370; // inner width of the content area const TITLE_FONT = new PhetFont({ size: 15, weight: "bold" }); const STEP_TITLE_FONT = new PhetFont({ size: 13, weight: "bold" }); 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 SEPARATOR_MARGIN = 6; // gap above/below the horizontal rule -const CLOSE_BUTTON_ICON_LENGTH = 10; // ── Helpers ─────────────────────────────────────────────────────────────────── -/** A thin horizontal rule separating the title from the steps. */ -function makeSeparator(): Rectangle { - return new Rectangle(0, 0, CONTENT_WIDTH, 1, { - fill: TrackLabColors.panelStrokeLightProperty, - }); -} - /** One step: bold heading above a softer-colored description. */ function makeStep(titleProp: ReadOnlyProperty, bodyProp: ReadOnlyProperty): Node { const titleText = new Text(titleProp, { @@ -60,68 +49,45 @@ function makeStep(titleProp: ReadOnlyProperty, bodyProp: ReadOnlyPropert // ── InfoDialogNode ──────────────────────────────────────────────────────────── /** - * Floating modal explaining how to digitize a track. + * Modal dialog explaining how to digitize a track. * - * Hidden by default (`visible = false`). Show by setting `visible = true`; - * the internal close button hides it again. + * Extends the standard Dialog from scenerystack/sun, which is shown and hidden + * via show() / hide() and rendered in the sim's popup layer — no manual + * scene-graph attachment needed. */ -export class InfoDialogNode extends Node { +export class InfoDialogNode extends Dialog { public constructor() { - super({ visible: false }); - const strings = StringManager.getInstance().getInfoDialog(); - // ── Header: title + close button ───────────────────────────────────────── + // ── Title ───────────────────────────────────────────────────────────── const titleText = new Text(strings.titleStringProperty, { font: TITLE_FONT, fill: TrackLabColors.textOnDarkProperty, }); - const closeButton = new CloseButton({ - listener: () => { - this.visible = false; - }, - baseColor: TrackLabColors.buttonBaseDarkProperty, - iconLength: CLOSE_BUTTON_ICON_LENGTH, - tandem: Tandem.OPT_OUT, - }); - - // Lay out title and close button side-by-side, close button flush right. - const headerNode = new Node({ children: [titleText, closeButton] }); - closeButton.right = CONTENT_WIDTH; - closeButton.centerY = titleText.centerY; - titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8; - - // ── Steps ──────────────────────────────────────────────────────────────── - const steps = [ - makeStep(strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty), - makeStep(strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty), - makeStep(strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), - makeStep(strings.addTrackTitleStringProperty, strings.addTrackBodyStringProperty), - makeStep(strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty), - makeStep(strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty), - ]; - - // ── Content layout ─────────────────────────────────────────────────────── - // Spacer nodes give extra breathing room around the separator. - const separatorTop = new Rectangle(0, 0, 0, SEPARATOR_MARGIN); - const separatorBottom = new Rectangle(0, 0, 0, SEPARATOR_MARGIN); - + // ── Steps ──────────────────────────────────────────────────────────── const content = new VBox({ - children: [headerNode, separatorTop, makeSeparator(), separatorBottom, ...steps], + children: [ + makeStep(strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty), + makeStep(strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty), + makeStep(strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), + makeStep(strings.addTrackTitleStringProperty, strings.addTrackBodyStringProperty), + makeStep(strings.digitizeTitleStringProperty, strings.digitizeBodyStringProperty), + makeStep(strings.autoTrackTitleStringProperty, strings.autoTrackBodyStringProperty), + ], spacing: STEPS_SPACING, align: "left", }); - const panel = new Panel(content, { + super(content, { + title: titleText, + titleAlign: "left", fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, cornerRadius: PANEL_CORNER_RADIUS, - xMargin: PANEL_X_MARGIN, - yMargin: PANEL_Y_MARGIN, + closeButtonColor: TrackLabColors.textOnDarkProperty, + tandem: Tandem.OPT_OUT, }); - - this.addChild(panel); } } diff --git a/src/screen-name/view/SimScreenView.ts b/src/screen-name/view/SimScreenView.ts index 08d71e9..2503bf3 100644 --- a/src/screen-name/view/SimScreenView.ts +++ b/src/screen-name/view/SimScreenView.ts @@ -7,14 +7,12 @@ import { DerivedProperty } from "scenerystack/axon"; import { Vector2 } from "scenerystack/dot"; -import { DragListener, Text } from "scenerystack/scenery"; -import { PhetFont, ResetAllButton } from "scenerystack/scenery-phet"; -import { ButtonNode, RectangularPushButton } from "scenerystack/sun"; +import { DragListener } from "scenerystack/scenery"; +import { InfoButton, ResetAllButton } from "scenerystack/scenery-phet"; import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; import { Tandem } from "scenerystack/tandem"; import type { TrackLabPreferencesModel } from "../../preferences/TrackLabPreferencesModel.js"; -import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js"; +import { CONTROL_PANEL_LEFT_MARGIN, DATA_TABLE_TOP_SPACING, RESET_BUTTON_MARGIN } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import { CalibrationToolNode } from "./CalibrationToolNode.js"; import { ControlPanel } from "./ControlPanel.js"; @@ -123,25 +121,13 @@ export class SimScreenView extends ScreenView { this.addChild(kinematicsGraph); // ── Info dialog (explains digitizing workflow) ──────────────────────────── + // Dialog manages its own scene-graph placement via the sim's popup layer. const infoDialogNode = new InfoDialogNode(); - this.addChild(infoDialogNode); // ── Info button (lower-left corner, same vertical level as reset button) ─ - const infoButton = new RectangularPushButton({ - content: new Text("?", { - font: new PhetFont({ size: 16, weight: "bold" }), - fill: TrackLabColors.textOnDarkProperty, - }), - baseColor: TrackLabColors.buttonBaseDarkProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN + 2, - yMargin: BUTTON_Y_MARGIN, - cornerRadius: 14, + const infoButton = new InfoButton({ + listener: () => infoDialogNode.show(), tandem: Tandem.OPT_OUT, - accessibleName: "How to use TrackLab", - listener: () => { - infoDialogNode.visible = !infoDialogNode.visible; - }, }); this.addChild(infoButton); @@ -169,10 +155,6 @@ export class SimScreenView extends ScreenView { infoButton.left = visibleBounds.minX + RESET_BUTTON_MARGIN; infoButton.centerY = resetAllButton.centerY; - // Info dialog: centered horizontally, positioned just above the info button. - infoDialogNode.centerX = this.layoutBounds.centerX; - infoDialogNode.bottom = infoButton.top - RESET_BUTTON_MARGIN; - // Data table: shift left by extraWidth so it stays within the layout area // and doesn't drift into the extra visible space claimed by the graph. dataTableNode.left = this.videoPlayerNode.right + DATA_TABLE_LEFT_SPACING - extraWidth; From 06ec9871710857864fde674c7087f09e5ed3bda0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 13:51:03 +0000 Subject: [PATCH 3/4] Fix: revert Dialog to Panel+Node (Dialog not in public API); keep InfoButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dialog is not exported from any scenerystack barrel (sun, sim, joist) in v3.0.0, so the previous commit's Dialog subclass caused a TS2305 error. Revert InfoDialogNode to its working Panel+Node approach (CloseButton, manual header layout, visibility toggle). Keep InfoButton from scenerystack/scenery-phet, which IS publicly exported and replaces the hand-rolled RectangularPushButton with the standard ⓘ icon button. Also restore the infoDialogNode positioning inside visibleBoundsProperty.link and the this.addChild(infoDialogNode) call, both required by the Node-based visibility approach. https://claude.ai/code/session_019asowJWZYPgvgsefumiFmG --- src/screen-name/view/InfoDialogNode.ts | 52 ++++++++++++++++++-------- src/screen-name/view/SimScreenView.ts | 10 ++++- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/screen-name/view/InfoDialogNode.ts b/src/screen-name/view/InfoDialogNode.ts index c5da516..725b106 100644 --- a/src/screen-name/view/InfoDialogNode.ts +++ b/src/screen-name/view/InfoDialogNode.ts @@ -2,13 +2,13 @@ * InfoDialogNode.ts * * Modal dialog explaining the main steps for digitizing a track in TrackLab. - * Shown by the InfoButton in the lower-left corner of the screen. + * Toggled by the InfoButton in the lower-left corner of the screen. */ import type { ReadOnlyProperty } from "scenerystack/axon"; import { Node, RichText, Text, VBox } from "scenerystack/scenery"; -import { PhetFont } from "scenerystack/scenery-phet"; -import { Dialog } from "scenerystack/sun"; +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"; @@ -16,12 +16,15 @@ import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import trackLab from "../../TrackLabNamespace.js"; // ── Layout constants ────────────────────────────────────────────────────────── -const CONTENT_WIDTH = 370; // inner width of the content area +const CONTENT_WIDTH = 370; // inner width of the panel content area +const PANEL_X_MARGIN = 18; +const PANEL_Y_MARGIN = 16; const TITLE_FONT = new PhetFont({ size: 15, weight: "bold" }); const STEP_TITLE_FONT = new PhetFont({ size: 13, weight: "bold" }); 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; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -49,25 +52,42 @@ function makeStep(titleProp: ReadOnlyProperty, bodyProp: ReadOnlyPropert // ── InfoDialogNode ──────────────────────────────────────────────────────────── /** - * Modal dialog explaining how to digitize a track. + * Floating panel explaining how to digitize a track. * - * Extends the standard Dialog from scenerystack/sun, which is shown and hidden - * via show() / hide() and rendered in the sim's popup layer — no manual - * scene-graph attachment needed. + * Hidden by default (`visible = false`). Show by setting `visible = true`; + * the internal close button hides it again. */ -export class InfoDialogNode extends Dialog { +export class InfoDialogNode extends Node { public constructor() { + super({ visible: false }); + const strings = StringManager.getInstance().getInfoDialog(); - // ── Title ───────────────────────────────────────────────────────────── + // ── Header: title + close button ───────────────────────────────────────── const titleText = new Text(strings.titleStringProperty, { font: TITLE_FONT, fill: TrackLabColors.textOnDarkProperty, }); - // ── Steps ──────────────────────────────────────────────────────────── + const closeButton = new CloseButton({ + listener: () => { + this.visible = false; + }, + baseColor: TrackLabColors.buttonBaseDarkProperty, + iconLength: CLOSE_BUTTON_ICON_LENGTH, + tandem: Tandem.OPT_OUT, + }); + + // Lay out title and close button side-by-side, close button flush right. + const headerNode = new Node({ children: [titleText, closeButton] }); + closeButton.right = CONTENT_WIDTH; + closeButton.centerY = titleText.centerY; + titleText.maxWidth = CONTENT_WIDTH - closeButton.width - 8; + + // ── Steps ──────────────────────────────────────────────────────────────── const content = new VBox({ children: [ + headerNode, makeStep(strings.loadVideoTitleStringProperty, strings.loadVideoBodyStringProperty), makeStep(strings.coordinateSystemTitleStringProperty, strings.coordinateSystemBodyStringProperty), makeStep(strings.calibrationTitleStringProperty, strings.calibrationBodyStringProperty), @@ -79,15 +99,15 @@ export class InfoDialogNode extends Dialog { align: "left", }); - super(content, { - title: titleText, - titleAlign: "left", + const panel = new Panel(content, { fill: TrackLabColors.panelFillProperty, stroke: TrackLabColors.panelStrokeProperty, cornerRadius: PANEL_CORNER_RADIUS, - closeButtonColor: TrackLabColors.textOnDarkProperty, - tandem: Tandem.OPT_OUT, + xMargin: PANEL_X_MARGIN, + yMargin: PANEL_Y_MARGIN, }); + + this.addChild(panel); } } diff --git a/src/screen-name/view/SimScreenView.ts b/src/screen-name/view/SimScreenView.ts index 2503bf3..abd3222 100644 --- a/src/screen-name/view/SimScreenView.ts +++ b/src/screen-name/view/SimScreenView.ts @@ -121,12 +121,14 @@ export class SimScreenView extends ScreenView { this.addChild(kinematicsGraph); // ── Info dialog (explains digitizing workflow) ──────────────────────────── - // Dialog manages its own scene-graph placement via the sim's popup layer. const infoDialogNode = new InfoDialogNode(); + this.addChild(infoDialogNode); // ── Info button (lower-left corner, same vertical level as reset button) ─ const infoButton = new InfoButton({ - listener: () => infoDialogNode.show(), + listener: () => { + infoDialogNode.visible = !infoDialogNode.visible; + }, tandem: Tandem.OPT_OUT, }); this.addChild(infoButton); @@ -155,6 +157,10 @@ export class SimScreenView extends ScreenView { infoButton.left = visibleBounds.minX + RESET_BUTTON_MARGIN; infoButton.centerY = resetAllButton.centerY; + // Info dialog: centered horizontally, positioned just above the info button. + infoDialogNode.centerX = this.layoutBounds.centerX; + infoDialogNode.bottom = infoButton.top - RESET_BUTTON_MARGIN; + // Data table: shift left by extraWidth so it stays within the layout area // and doesn't drift into the extra visible space claimed by the graph. dataTableNode.left = this.videoPlayerNode.right + DATA_TABLE_LEFT_SPACING - extraWidth; From 6effd69ec0061d5d33b25347094211ed119ce77f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 13:52:09 +0000 Subject: [PATCH 4/4] Apply Biome formatting to codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-formatted by Biome after first npm install in this environment. All changes are whitespace/line-length only — no logic changes. https://claude.ai/code/session_019asowJWZYPgvgsefumiFmG --- src/TrackLabColors.ts | 12 ++---------- src/screen-name/view/AutoTrackerNode.ts | 3 ++- src/screen-name/view/DataTableNode.ts | 3 ++- src/screen-name/view/PlaybackControlsNode.ts | 2 ++ src/screen-name/view/TrackListPanel.ts | 2 ++ src/screen-name/view/WebcamPanel.ts | 10 ++++++++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/TrackLabColors.ts b/src/TrackLabColors.ts index 55a88da..30970d7 100644 --- a/src/TrackLabColors.ts +++ b/src/TrackLabColors.ts @@ -90,16 +90,8 @@ const TrackLabColors = { ), // Axes (X red, Y green) - axisXColorProperty: profileColor( - "axisX", - new Color(255, 68, 68, 0.85), - new Color(238, 51, 51, 0.85), - ), - axisYColorProperty: profileColor( - "axisY", - new Color(68, 204, 68, 0.85), - new Color(51, 187, 51, 0.85), - ), + axisXColorProperty: profileColor("axisX", new Color(255, 68, 68, 0.85), new Color(238, 51, 51, 0.85)), + axisYColorProperty: profileColor("axisY", new Color(68, 204, 68, 0.85), new Color(51, 187, 51, 0.85)), // Calibration tool (bright colors with shadows for visibility on all backgrounds) calibrationFillProperty: profileColor( diff --git a/src/screen-name/view/AutoTrackerNode.ts b/src/screen-name/view/AutoTrackerNode.ts index 0b24c90..7ec1e5a 100644 --- a/src/screen-name/view/AutoTrackerNode.ts +++ b/src/screen-name/view/AutoTrackerNode.ts @@ -254,7 +254,8 @@ export class AutoTrackerNode extends Node { // biome-ignore lint/suspicious/noConsole: error logging for tracker init failure console.error("AutoTracker: failed to initialise OpenCV tracker:", err); if (this.initVersion === capturedVersion) { - const message = err instanceof Error ? err.message : autoTrackerStrings.trackingInitFailedStringProperty.value; + const message = + err instanceof Error ? err.message : autoTrackerStrings.trackingInitFailedStringProperty.value; this.errorText.string = message; this.errorText.visible = true; this.hintText.visible = true; diff --git a/src/screen-name/view/DataTableNode.ts b/src/screen-name/view/DataTableNode.ts index 5348ff0..917780e 100644 --- a/src/screen-name/view/DataTableNode.ts +++ b/src/screen-name/view/DataTableNode.ts @@ -175,7 +175,8 @@ function buildHtmlTable( // ── Accessible caption (visually hidden but read by screen readers) ──────── const caption = document.createElement("caption"); caption.textContent = a11y.tableCaption; - caption.style.cssText = "position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;"; + caption.style.cssText = + "position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;"; table.appendChild(caption); // ── Header row ───────────────────────────────────────────────────────────── diff --git a/src/screen-name/view/PlaybackControlsNode.ts b/src/screen-name/view/PlaybackControlsNode.ts index 0fd7551..2e4f2f6 100644 --- a/src/screen-name/view/PlaybackControlsNode.ts +++ b/src/screen-name/view/PlaybackControlsNode.ts @@ -13,7 +13,9 @@ import { ButtonNode, RectangularPushButton, Slider } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; + const a11yStrings = StringManager.getInstance().getA11y(); + import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; diff --git a/src/screen-name/view/TrackListPanel.ts b/src/screen-name/view/TrackListPanel.ts index a5d9524..040c205 100644 --- a/src/screen-name/view/TrackListPanel.ts +++ b/src/screen-name/view/TrackListPanel.ts @@ -21,7 +21,9 @@ import { ButtonNode, Checkbox, Panel, RectangularPushButton } from "scenerystack import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; + const a11yStrings = StringManager.getInstance().getA11y(); + import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import type { Track } from "../model/Track.js"; diff --git a/src/screen-name/view/WebcamPanel.ts b/src/screen-name/view/WebcamPanel.ts index 45c1bca..2ad04b3 100644 --- a/src/screen-name/view/WebcamPanel.ts +++ b/src/screen-name/view/WebcamPanel.ts @@ -20,7 +20,12 @@ import { import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, WEBCAM_PREVIEW_HEIGHT, WEBCAM_PREVIEW_WIDTH } from "../../TrackLabConstants.js"; +import { + BUTTON_X_MARGIN, + BUTTON_Y_MARGIN, + WEBCAM_PREVIEW_HEIGHT, + WEBCAM_PREVIEW_WIDTH, +} from "../../TrackLabConstants.js"; import { estimateVideoFrameRate, type FPSEstimate, fixWebmDuration, WebcamRecorder } from "../../webcam.js"; import { FRAME_RATE_RANGE, type SimModel } from "../model/SimModel.js"; @@ -372,7 +377,8 @@ export class WebcamPanel extends Node { cameras.forEach((cam, i) => { const opt = document.createElement("option"); opt.value = cam.deviceId; - opt.textContent = cam.label || this.webcamStrings.cameraLabelStringProperty.value.replace("{{number}}", String(i + 1)); + opt.textContent = + cam.label || this.webcamStrings.cameraLabelStringProperty.value.replace("{{number}}", String(i + 1)); this.cameraSelect.appendChild(opt); }); }