From ad7a780356a927535bb73bd67946fbca91a170d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Feb 2026 09:35:44 +0000 Subject: [PATCH 1/2] Add factory method for RectangularPushButton with consistent sizing and touch areas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces createTrackLabButton() in TrackLabButton.ts — a single factory that gives every button the same flat appearance, dark base colour, x/y margins, and enlarged touch/mouse target areas without requiring callers to repeat boilerplate. Key changes: - TrackLabConstants.ts: add BUTTON_MIN_CONTENT_SIZE (18 px), TOUCH_AREA_DILATION (5 px) and MOUSE_AREA_DILATION (2 px) constants - TrackLabButton.ts (new): factory function + icon helpers - sizeContent() wraps every icon in a Node with HStrut/VStrut so all icon buttons share an identical 18×18 content floor; larger content (text labels) simply overflows naturally - makeDownloadIcon() replaces the plain ⬇ unicode glyph with a proper Path-based arrow-into-tray icon for cleaner rendering and better recognisability - makeUploadIcon() extracted from VideoSourceControlNode for reuse - All seven call sites updated to use createTrackLabButton(): VideoSourceControlNode (download + upload), PlaybackControlsNode (rewind), TrackListPanel (add-track + trash), DataTableNode (CSV export), WebcamPanel (record start + stop + use-video) - CameraButton in VideoSourceControlNode gets manual touch/mouse dilation to match the factory defaults (CameraButton is not a RectangularPushButton) https://claude.ai/code/session_01WUtXMUhjfvG784s5q1DXag --- src/TrackLabButton.ts | 153 ++++++++++++++++++ src/TrackLabConstants.ts | 12 ++ src/screen-name/view/DataTableNode.ts | 62 +++---- src/screen-name/view/PlaybackControlsNode.ts | 29 ++-- src/screen-name/view/TrackListPanel.ts | 29 ++-- .../view/VideoSourceControlNode.ts | 46 ++---- src/screen-name/view/WebcamPanel.ts | 30 +--- 7 files changed, 231 insertions(+), 130 deletions(-) create mode 100644 src/TrackLabButton.ts diff --git a/src/TrackLabButton.ts b/src/TrackLabButton.ts new file mode 100644 index 0000000..620ef3b --- /dev/null +++ b/src/TrackLabButton.ts @@ -0,0 +1,153 @@ +/** + * TrackLabButton.ts + * + * Factory function and icon helpers for all RectangularPushButton instances in + * TrackLab. A single factory ensures every button shares the same flat + * appearance, base colour, margins, and touch/mouse target sizes, while still + * allowing per-call overrides for special cases (e.g. record/stop/success + * colours, enabled properties). + * + * Usage: + * import { createTrackLabButton, makeDownloadIcon } from '../../TrackLabButton.js'; + * + * const btn = createTrackLabButton(makeDownloadIcon(), { + * accessibleName: strings.downloadVideoStringProperty, + * listener: () => { ... }, + * }); + * + * // Override base colour for a special action: + * const recordBtn = createTrackLabButton(recordIcon, { + * baseColor: TrackLabColors.buttonRecordProperty, + * listener: () => startRecording(), + * }); + */ + +import { Shape } from "scenerystack/kite"; +import { HStrut, Node, Path, VStrut } from "scenerystack/scenery"; +import { ButtonNode, RectangularPushButton } from "scenerystack/sun"; +import { Tandem } from "scenerystack/tandem"; +import TrackLabColors from "./TrackLabColors.js"; +import { + BUTTON_MIN_CONTENT_SIZE, + BUTTON_X_MARGIN, + BUTTON_Y_MARGIN, + MOUSE_AREA_DILATION, + TOUCH_AREA_DILATION, +} from "./TrackLabConstants.js"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +type PushButtonOptions = ConstructorParameters[0]; + +/** + * Options accepted by createTrackLabButton. + * `content` is excluded because it is supplied as the first argument. + */ +export type TrackLabButtonOptions = Omit; + +// ── Content sizing ──────────────────────────────────────────────────────────── + +/** + * Wrap an icon node with HStrut/VStrut so the content area is at least + * BUTTON_MIN_CONTENT_SIZE × BUTTON_MIN_CONTENT_SIZE. The icon is centred + * within this minimum area so that small glyphs sit squarely in the middle + * of the button rather than in a corner. + * + * If `icon` is already larger than the minimum (e.g. a wide text label), the + * struts have no visible effect and the button sizes naturally to the content. + */ +function sizeContent(icon: Node): Node { + icon.centerX = BUTTON_MIN_CONTENT_SIZE / 2; + icon.centerY = BUTTON_MIN_CONTENT_SIZE / 2; + return new Node({ + children: [new HStrut(BUTTON_MIN_CONTENT_SIZE), new VStrut(BUTTON_MIN_CONTENT_SIZE), icon], + }); +} + +// ── Factory ─────────────────────────────────────────────────────────────────── + +/** + * Create a standard TrackLab rectangular push button. + * + * Defaults applied (all overridable via `options`): + * - Flat appearance strategy + * - Dark base colour (`buttonBaseDarkProperty`) + * - Consistent x/y margins from TrackLabConstants + * - Enlarged touch area (TOUCH_AREA_DILATION on each side) + * - Enlarged mouse area (MOUSE_AREA_DILATION on each side) + * - Tandem opted out + * + * The content is always wrapped with HStrut/VStrut to guarantee a minimum + * icon area so that all icon buttons share identical dimensions. + */ +export function createTrackLabButton(content: Node, options?: TrackLabButtonOptions): RectangularPushButton { + return new RectangularPushButton({ + // ── Defaults ──────────────────────────────────────────────────────────── + baseColor: TrackLabColors.buttonBaseDarkProperty, + buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, + xMargin: BUTTON_X_MARGIN, + yMargin: BUTTON_Y_MARGIN, + touchAreaXDilation: TOUCH_AREA_DILATION, + touchAreaYDilation: TOUCH_AREA_DILATION, + mouseAreaXDilation: MOUSE_AREA_DILATION, + mouseAreaYDilation: MOUSE_AREA_DILATION, + tandem: Tandem.OPT_OUT, + // ── Caller overrides ──────────────────────────────────────────────────── + ...options, + // ── Content: always the min-sized wrapper ──────────────────────────────── + content: sizeContent(content), + }); +} + +// ── Icon helpers ────────────────────────────────────────────────────────────── + +/** + * Download icon: downward arrow with a tray bar. + * + * Replaces the plain ⬇ unicode glyph with a proper symbolic Path icon that + * scales cleanly at all sizes and renders crisply regardless of font hinting. + * + * | shaft | + * \ arrow / + * \_head__/ + * [=tray bar=] + */ +export function makeDownloadIcon(): Node { + const totalW = 12; // total icon width + const shaftW = 4; // width of the vertical arrow shaft + const shaftH = 5; // height of the shaft above the arrowhead + const headH = 4; // height of the arrowhead triangle + const gap = 1; // gap between arrowhead tip and tray bar + const barH = 2; // height of the tray bar + + const shape = new Shape(); + + // Vertical shaft (centered horizontally) + shape.rect((totalW - shaftW) / 2, 0, shaftW, shaftH); + + // Arrowhead triangle (pointing down) + shape.moveTo(0, shaftH); + shape.lineTo(totalW / 2, shaftH + headH); + shape.lineTo(totalW, shaftH); + shape.close(); + + // Tray bar at bottom + shape.rect(0, shaftH + headH + gap, totalW, barH); + + return new Path(shape, { fill: TrackLabColors.textOnDarkProperty }); +} + +/** + * Upload icon: folder shape indicating "open a file". + */ +export function makeUploadIcon(): Node { + const folderShape = new Shape() + .moveTo(0, 3) + .lineTo(4, 3) + .lineTo(5.5, 0) + .lineTo(14, 0) + .lineTo(14, 10) + .lineTo(0, 10) + .close(); + return new Path(folderShape, { fill: TrackLabColors.textOnDarkProperty }); +} diff --git a/src/TrackLabConstants.ts b/src/TrackLabConstants.ts index 0e02524..248f141 100644 --- a/src/TrackLabConstants.ts +++ b/src/TrackLabConstants.ts @@ -56,6 +56,18 @@ export const MIN_CALIB_DISTANCE = 1e-9; // minimum real-world calibration distan export const BUTTON_X_MARGIN = 8; export const BUTTON_Y_MARGIN = 6; +// Minimum icon content area (width × height) guaranteed by HStrut/VStrut inside +// the factory-created content wrapper. This ensures every icon-only button is +// the same size even when the icon glyph is smaller than this floor. +export const BUTTON_MIN_CONTENT_SIZE = 18; + +// Extra touch-target padding beyond the rendered button bounds. Helps users on +// touch screens reliably tap small buttons without pixel-perfect precision. +export const TOUCH_AREA_DILATION = 5; + +// Extra mouse-pointer hit area beyond the rendered button bounds. +export const MOUSE_AREA_DILATION = 2; + // ── Webcam panel ────────────────────────────────────────────────────────────── export const WEBCAM_PREVIEW_WIDTH = 480; // width of the preview and review video elements export const WEBCAM_PREVIEW_HEIGHT = 270; // height of the preview and review video elements diff --git a/src/screen-name/view/DataTableNode.ts b/src/screen-name/view/DataTableNode.ts index 5348ff0..84c3977 100644 --- a/src/screen-name/view/DataTableNode.ts +++ b/src/screen-name/view/DataTableNode.ts @@ -14,10 +14,11 @@ import type { TReadOnlyProperty } from "scenerystack/axon"; import { DOM, HBox, type Node, Text, VBox } from "scenerystack/scenery"; import { PhetFont } from "scenerystack/scenery-phet"; -import { ButtonNode, Panel, RectangularPushButton } from "scenerystack/sun"; +import { Panel } from "scenerystack/sun"; import { StringManager } from "../../i18n/StringManager.js"; +import { createTrackLabButton, makeDownloadIcon } from "../../TrackLabButton.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; +import { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import type { Track } from "../model/Track.js"; @@ -37,7 +38,6 @@ const MAX_TABLE_HEIGHT = 400; // Maximum height before scrolling (increased from const TITLE_FONT = new PhetFont({ size: 12, weight: "bold" }); const TABLE_FONT_SIZE = 11; // HTML table font size in px const EXPORT_BUTTON_FONT_SIZE = 9; -const DOWNLOAD_ICON_FONT_SIZE = 11; // font size for the ⬇ icon glyph // ── Precision ───────────────────────────────────────────────────────────────── // Both values are kept equal so exported CSV data matches what users see on screen. @@ -351,17 +351,6 @@ function buildSingleDataRow( return tr; } -/** - * Download icon (simple arrow pointing down). - */ -function makeDownloadIcon(): Node { - // Simple text-based icon - return new Text("⬇", { - font: new PhetFont({ size: DOWNLOAD_ICON_FONT_SIZE }), - fill: TrackLabColors.textOnDarkProperty, - }); -} - // ── Component ──────────────────────────────────────────────────────────────── export class DataTableNode extends Panel { @@ -412,8 +401,8 @@ export class DataTableNode extends Panel { const tableDomNode = new DOM(tableWrapper, { allowInput: true }); // ── Export button ──────────────────────────────────────────────────────── - const exportButton = new RectangularPushButton({ - content: new HBox({ + const exportButton = createTrackLabButton( + new HBox({ children: [ makeDownloadIcon(), new Text(dataTableStrings.csvStringProperty, { @@ -426,28 +415,27 @@ export class DataTableNode extends Panel { ], spacing: EXPORT_BUTTON_ICON_SPACING, }), - accessibleName: a11yStrings.exportCSVStringProperty, - baseColor: TrackLabColors.exportButtonProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - listener: () => { - const tracks = model.tracksProperty.value; - const unit = unitProperty.value; - const csv = generateCsv(tracks, unit, getLabels()); - - // Create download — no DOM insertion needed in modern browsers. - const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `export${this.exportCounter}.csv`; - link.click(); - URL.revokeObjectURL(url); - - this.exportCounter++; + { + accessibleName: a11yStrings.exportCSVStringProperty, + baseColor: TrackLabColors.exportButtonProperty, + listener: () => { + const tracks = model.tracksProperty.value; + const unit = unitProperty.value; + const csv = generateCsv(tracks, unit, getLabels()); + + // Create download — no DOM insertion needed in modern browsers. + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `export${this.exportCounter}.csv`; + link.click(); + URL.revokeObjectURL(url); + + this.exportCounter++; + }, }, - }); + ); // ── Title row ──────────────────────────────────────────────────────────── const titleLabel = new Text(dataTableStrings.titleStringProperty, { diff --git a/src/screen-name/view/PlaybackControlsNode.ts b/src/screen-name/view/PlaybackControlsNode.ts index 0fd7551..3988e0a 100644 --- a/src/screen-name/view/PlaybackControlsNode.ts +++ b/src/screen-name/view/PlaybackControlsNode.ts @@ -9,12 +9,12 @@ import { DerivedProperty, EnumerationProperty } from "scenerystack/axon"; import { Dimension2, Range } from "scenerystack/dot"; import { HBox, Text, VBox } from "scenerystack/scenery"; import { PhetFont, TimeControlNode, TimeSpeed } from "scenerystack/scenery-phet"; -import { ButtonNode, RectangularPushButton, Slider } from "scenerystack/sun"; +import { Slider } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; +import { createTrackLabButton } from "../../TrackLabButton.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"; const LABEL_FONT = new PhetFont(14); @@ -163,24 +163,21 @@ export class PlaybackControlsNode extends HBox { }); // ── Rewind-to-zero button ────────────────────────────────────────────── - const rewindButton = new RectangularPushButton({ - content: new Text("\u23EE", { + const rewindButton = createTrackLabButton( + new Text("\u23EE", { font: new PhetFont(REWIND_BUTTON_ICON_SIZE), fill: TrackLabColors.textOnDarkProperty, }), - baseColor: TrackLabColors.buttonBaseDarkProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - listener: () => { - model.isPlayingProperty.value = false; - model.currentTimeProperty.value = 0; - videoElement.currentTime = 0; + { + enabledProperty: model.videoLoadedProperty, + accessibleName: a11yStrings.rewindToStartStringProperty, + listener: () => { + model.isPlayingProperty.value = false; + model.currentTimeProperty.value = 0; + videoElement.currentTime = 0; + }, }, - enabledProperty: model.videoLoadedProperty, - tandem: Tandem.OPT_OUT, - accessibleName: a11yStrings.rewindToStartStringProperty, - }); + ); this.children = [timeControlNode, scrubber, rewindButton, infoDisplay]; diff --git a/src/screen-name/view/TrackListPanel.ts b/src/screen-name/view/TrackListPanel.ts index a5d9524..e692669 100644 --- a/src/screen-name/view/TrackListPanel.ts +++ b/src/screen-name/view/TrackListPanel.ts @@ -17,12 +17,13 @@ import { Color } from "scenerystack"; import { BooleanProperty, DerivedProperty, type TReadOnlyProperty } from "scenerystack/axon"; import { Circle, Line, Node, Rectangle, Text, VBox } from "scenerystack/scenery"; import { PhetFont } from "scenerystack/scenery-phet"; -import { ButtonNode, Checkbox, Panel, RectangularPushButton } from "scenerystack/sun"; +import { Checkbox, Panel } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; +import { createTrackLabButton } from "../../TrackLabButton.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 { PANEL_CORNER_RADIUS } from "../../TrackLabConstants.js"; import type { SimModel } from "../model/SimModel.js"; import type { Track } from "../model/Track.js"; @@ -147,14 +148,9 @@ class TrackRowNode extends Node { checkbox.centerY = ROW_CY; // ── Trash button (right side) ───────────────────────────────────────── - const trashButton = new RectangularPushButton({ - content: makeTrashIcon(), + const trashButton = createTrackLabButton(makeTrashIcon(), { baseColor: TrackLabColors.trashButtonBaseProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, listener: () => model.removeTrack(track.id), - tandem: Tandem.OPT_OUT, accessibleName: a11yStrings.removeTrackStringProperty.value.replace("{{symbol}}", track.symbol), }); trashButton.centerY = ROW_CY; @@ -204,19 +200,16 @@ export class TrackListPanel extends Panel { (loaded, canAdd) => loaded && canAdd, ); - const addButton = new RectangularPushButton({ - content: new Text(trackListStrings.addTrackStringProperty, { + const addButton = createTrackLabButton( + new Text(trackListStrings.addTrackStringProperty, { font: LABEL_FONT, fill: TrackLabColors.textOnDarkProperty, }), - baseColor: TrackLabColors.buttonBaseDarkProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - enabledProperty: addButtonEnabledProperty, - listener: () => model.addTrack(), - tandem: Tandem.OPT_OUT, - }); + { + enabledProperty: addButtonEnabledProperty, + listener: () => model.addTrack(), + }, + ); // ── Track list (rebuilt whenever tracks change) ─────────────────────── const trackListVBox = new VBox({ diff --git a/src/screen-name/view/VideoSourceControlNode.ts b/src/screen-name/view/VideoSourceControlNode.ts index 388caf0..3990de3 100644 --- a/src/screen-name/view/VideoSourceControlNode.ts +++ b/src/screen-name/view/VideoSourceControlNode.ts @@ -7,14 +7,14 @@ import type { TReadOnlyProperty } from "scenerystack/axon"; import { Property } from "scenerystack/axon"; -import { Shape } from "scenerystack/kite"; -import { HBox, type Node, Path, Text } from "scenerystack/scenery"; +import { HBox, type Node, Text } from "scenerystack/scenery"; import { CameraButton, PhetFont } from "scenerystack/scenery-phet"; -import { ButtonNode, ComboBox, type ComboBoxItem, RectangularPushButton } from "scenerystack/sun"; +import { ButtonNode, ComboBox, type ComboBoxItem } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; +import { createTrackLabButton, makeDownloadIcon, makeUploadIcon } from "../../TrackLabButton.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN } from "../../TrackLabConstants.js"; +import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, MOUSE_AREA_DILATION, TOUCH_AREA_DILATION } from "../../TrackLabConstants.js"; import { DEFAULT_FRAME_RATE, type SimModel, type UploadedVideo, type WebcamRecording } from "../model/SimModel.js"; import { WebcamPanel } from "./WebcamPanel.js"; @@ -22,7 +22,6 @@ const LABEL_FONT = new PhetFont(14); const HEADER_FONT = new PhetFont({ size: 12, style: "italic" }); const CONTROLS_SPACING = 12; const HEADER_VALUE_PREFIX = "__header:"; -const DOWNLOAD_ICON_FONT_SIZE = 11; // font size for the download arrow icon glyph // Bundled video files with known frame rates (labels resolved from StringManager) type VideoFile = { @@ -293,17 +292,7 @@ export class VideoSourceControlNode extends HBox { model.uploadedVideosProperty.lazyLink(() => rebuildComboBox()); // ── Download button (visible for user-provided videos) ──────────────── - const downloadIcon = new Text("\u2B07", { - font: new PhetFont({ size: DOWNLOAD_ICON_FONT_SIZE }), - fill: TrackLabColors.textOnDarkProperty, - }); - const downloadButton = new RectangularPushButton({ - content: downloadIcon, - baseColor: TrackLabColors.buttonBaseDarkProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - tandem: Tandem.OPT_OUT, + const downloadButton = createTrackLabButton(makeDownloadIcon(), { accessibleName: videoSourceStrings.downloadVideoStringProperty, listener: () => { const blob = model.currentWebcamBlobProperty.value; @@ -357,26 +346,7 @@ export class VideoSourceControlNode extends HBox { fileInput.value = ""; }); - // Folder icon shape for the upload button - const folderShape = new Shape() - .moveTo(0, 3) - .lineTo(4, 3) - .lineTo(5.5, 0) - .lineTo(14, 0) - .lineTo(14, 10) - .lineTo(0, 10) - .close(); - const uploadIcon = new Path(folderShape, { - fill: TrackLabColors.textOnDarkProperty, - }); - - const uploadButton = new RectangularPushButton({ - content: uploadIcon, - baseColor: TrackLabColors.buttonBaseDarkProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - tandem: Tandem.OPT_OUT, + const uploadButton = createTrackLabButton(makeUploadIcon(), { accessibleName: videoSourceStrings.openVideoFileStringProperty, listener: () => { model.isPlayingProperty.value = false; @@ -409,6 +379,10 @@ export class VideoSourceControlNode extends HBox { buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, xMargin: BUTTON_X_MARGIN, yMargin: BUTTON_Y_MARGIN, + touchAreaXDilation: TOUCH_AREA_DILATION, + touchAreaYDilation: TOUCH_AREA_DILATION, + mouseAreaXDilation: MOUSE_AREA_DILATION, + mouseAreaYDilation: MOUSE_AREA_DILATION, iconFill: TrackLabColors.textOnDarkProperty, tandem: Tandem.OPT_OUT, accessibleName: videoSourceStrings.recordWebcamStringProperty, diff --git a/src/screen-name/view/WebcamPanel.ts b/src/screen-name/view/WebcamPanel.ts index 45c1bca..cd19bcf 100644 --- a/src/screen-name/view/WebcamPanel.ts +++ b/src/screen-name/view/WebcamPanel.ts @@ -15,12 +15,12 @@ import { checkSolidShape, NumberSpinner, Panel, - RectangularPushButton, } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; +import { createTrackLabButton } from "../../TrackLabButton.js"; import TrackLabColors from "../../TrackLabColors.js"; -import { BUTTON_X_MARGIN, BUTTON_Y_MARGIN, WEBCAM_PREVIEW_HEIGHT, WEBCAM_PREVIEW_WIDTH } from "../../TrackLabConstants.js"; +import { 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"; @@ -151,13 +151,8 @@ export class WebcamPanel extends Node { const recordIcon = new Path(Shape.circle(0, 0, RECORD_ICON_RADIUS), { fill: TrackLabColors.textOnDarkProperty, }); - const startButton = new RectangularPushButton({ - content: recordIcon, + const startButton = createTrackLabButton(recordIcon, { baseColor: TrackLabColors.buttonRecordProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - tandem: Tandem.OPT_OUT, accessibleName: this.webcamStrings.startRecordingStringProperty, listener: () => this.startRecording(), }); @@ -165,14 +160,8 @@ export class WebcamPanel extends Node { const stopIcon = new Path(new StopIconShape(STOP_ICON_SIZE), { fill: TrackLabColors.textOnDarkProperty, }); - stopIcon.translation = stopIcon.bounds.center.negated(); - const stopButton = new RectangularPushButton({ - content: stopIcon, + const stopButton = createTrackLabButton(stopIcon, { baseColor: TrackLabColors.buttonStopProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - tandem: Tandem.OPT_OUT, accessibleName: this.webcamStrings.stopRecordingStringProperty, listener: () => this.stopRecording(), }); @@ -191,13 +180,8 @@ export class WebcamPanel extends Node { scale: CHECK_ICON_SCALE, fill: TrackLabColors.textOnDarkProperty, }); - const useVideoButton = new RectangularPushButton({ - content: useVideoIcon, + const useVideoButton = createTrackLabButton(useVideoIcon, { baseColor: TrackLabColors.buttonSuccessProperty, - buttonAppearanceStrategy: ButtonNode.FlatAppearanceStrategy, - xMargin: BUTTON_X_MARGIN, - yMargin: BUTTON_Y_MARGIN, - tandem: Tandem.OPT_OUT, accessibleName: this.webcamStrings.useVideoStringProperty, listener: () => this.useVideo(options.onVideoReady), }); @@ -299,8 +283,8 @@ export class WebcamPanel extends Node { this._stopButton = stopButton; } - private readonly _startButton: RectangularPushButton; - private readonly _stopButton: RectangularPushButton; + private readonly _startButton: ReturnType; + private readonly _stopButton: ReturnType; private readonly webcamStrings: ReturnType; // ── Public ─────────────────────────────────────────────────────────────── From b6099fcaeb5eddebb95983720c9a6a5e16c44d1a Mon Sep 17 00:00:00 2001 From: veillettem Date: Mon, 23 Feb 2026 12:02:53 -0500 Subject: [PATCH 2/2] Fix lint, format, and typecheck errors Co-Authored-By: Claude Sonnet 4.5 --- src/screen-name/view/DataTableNode.ts | 2 +- src/screen-name/view/PlaybackControlsNode.ts | 3 +-- src/screen-name/view/WebcamPanel.ts | 8 +------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/screen-name/view/DataTableNode.ts b/src/screen-name/view/DataTableNode.ts index ec61e76..5cd9a40 100644 --- a/src/screen-name/view/DataTableNode.ts +++ b/src/screen-name/view/DataTableNode.ts @@ -12,7 +12,7 @@ */ import type { TReadOnlyProperty } from "scenerystack/axon"; -import { DOM, HBox, type Node, Text, VBox } from "scenerystack/scenery"; +import { DOM, HBox, Text, VBox } from "scenerystack/scenery"; import { PhetFont } from "scenerystack/scenery-phet"; import { Panel } from "scenerystack/sun"; import { StringManager } from "../../i18n/StringManager.js"; diff --git a/src/screen-name/view/PlaybackControlsNode.ts b/src/screen-name/view/PlaybackControlsNode.ts index 707098a..b887f25 100644 --- a/src/screen-name/view/PlaybackControlsNode.ts +++ b/src/screen-name/view/PlaybackControlsNode.ts @@ -79,8 +79,7 @@ export class PlaybackControlsNode extends HBox { timeSpeeds: [TimeSpeed.NORMAL, TimeSpeed.SLOW], enabledProperty: model.videoLoadedProperty, tandem: Tandem.OPT_OUT, - playPauseStep - Options: { + playPauseStepButtonOptions: { includeStepBackwardButton: true, stepBackwardButtonOptions: { listener: onStepBackward, diff --git a/src/screen-name/view/WebcamPanel.ts b/src/screen-name/view/WebcamPanel.ts index 075b738..451fd51 100644 --- a/src/screen-name/view/WebcamPanel.ts +++ b/src/screen-name/view/WebcamPanel.ts @@ -9,13 +9,7 @@ import { Property } from "scenerystack/axon"; import { Shape } from "scenerystack/kite"; import { DOM, HBox, Node, Path, Text, VBox } from "scenerystack/scenery"; import { CloseButton, PhetFont, RefreshButton, StopIconShape } from "scenerystack/scenery-phet"; -import { - ButtonNode, - cameraSolidShape, - checkSolidShape, - NumberSpinner, - Panel, -} from "scenerystack/sun"; +import { ButtonNode, cameraSolidShape, checkSolidShape, NumberSpinner, Panel } from "scenerystack/sun"; import { Tandem } from "scenerystack/tandem"; import { StringManager } from "../../i18n/StringManager.js"; import { createTrackLabButton } from "../../TrackLabButton.js";