Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extraKnownMarketplaces": {
"openphysics": {
"source": {
"source": "github",
"repo": "OpenPhysics/Baton"
}
}
},
"enabledPlugins": {
"scenerystack@openphysics": true
}
}
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,16 @@ Comparator), it uses a SceneryStack scene-graph `scale()` — model coordinate v

## Accessibility

The sim ships with PDOM names and `ResetAllButton` instrumented with tandem. Full a11y wiring
(screen summary, `pdomOrder`, keyboard help, `accessibleName` on interactive nodes) is in progress.
Conventions: [../Baton/ACCESSIBILITY.md](../Baton/ACCESSIBILITY.md).
Follows the shared [OpenPhysics accessibility convention](https://github.com/OpenPhysics/Baton/blob/main/ACCESSIBILITY.md).
Each screen has a `<Screen>ScreenSummaryContent.ts` in its `view/` folder (live
`currentDetailsContent` derived from the screen model), registered by the `*Screen.ts` wrapper via
the view's `screenSummaryContent` option. Each `*ScreenView.ts` sets an explicit traversal order
through a wrapper `Node`'s `pdomOrder` (interactive nodes first, Reset All last), and every
interactive node carries an `accessibleName`. A11y strings live under per-screen keys in the `a11y`
block of each locale JSON (`a11y.registration`, `a11y.blinkComparator`, `a11y.photometry`,
`a11y.analyzer`), exposed via `StringManager.get<Screen>A11yStrings()`. `ApertureNode` is
keyboard-operable via a `KeyboardDragListener` (arrow keys; Shift for fine steps); the shared
`VSPKeyboardHelpContent` documents per-screen keys.

## Decompiling the Flash sources

Expand Down
6 changes: 6 additions & 0 deletions src/VSPColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ const VSPColors = {
projector: new Color(163, 31, 32, 0.35),
}),

/** Translucent rubber-band zoom-selection window on the PDM plot. */
pdmZoomSelectionFillProperty: new ProfileColorProperty(VSPNamespace, "pdmZoomSelectionFill", {
default: new Color(120, 170, 255, 0.22),
projector: new Color(60, 110, 200, 0.18),
}),

/** Draggable difference-tool bars on the observations plot. */
deltaBarColorProperty: new ProfileColorProperty(VSPNamespace, "deltaBar", {
default: new Color("#909090"),
Expand Down
11 changes: 4 additions & 7 deletions src/analyzer/AnalyzerScreen.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { type EmptySelfOptions, optionize } from "scenerystack/phet-core";
import type { ScreenOptions } from "scenerystack/sim";
import { Screen, ScreenSummaryContent } from "scenerystack/sim";
import { Screen } from "scenerystack/sim";
import type { Tandem } from "scenerystack/tandem";
import { VSPKeyboardHelpContent } from "../common/view/VSPKeyboardHelpContent.js";
import { StringManager } from "../i18n/StringManager.js";
import type { VSPPreferencesModel } from "../preferences/VSPPreferencesModel.js";
import VSPColors from "../VSPColors.js";
import { AnalyzerModel } from "./model/AnalyzerModel.js";
import { AnalyzerScreenSummaryContent } from "./view/AnalyzerScreenSummaryContent.js";
import { AnalyzerScreenView } from "./view/AnalyzerScreenView.js";

type AnalyzerScreenOptions = ScreenOptions & { tandem: Tandem; preferences: VSPPreferencesModel };

export class AnalyzerScreen extends Screen<AnalyzerModel, AnalyzerScreenView> {
public constructor(options: AnalyzerScreenOptions) {
const summaryStrings = StringManager.getInstance().getA11yStrings().screenSummary.analyzer;
const summaryStrings = StringManager.getInstance().getAnalyzerA11yStrings().screenSummary;
super(
() => new AnalyzerModel(options.tandem.createTandem("model")),
(model) =>
new AnalyzerScreenView(model, options.preferences, {
tandem: options.tandem.createTandem("view"),
screenSummaryContent: new ScreenSummaryContent({
playAreaContent: summaryStrings.playAreaStringProperty,
controlAreaContent: summaryStrings.controlAreaStringProperty,
interactionHintContent: summaryStrings.interactionHintStringProperty,
}),
screenSummaryContent: new AnalyzerScreenSummaryContent(model),
}),
optionize<AnalyzerScreenOptions, EmptySelfOptions, ScreenOptions>()(
{
Expand Down
40 changes: 40 additions & 0 deletions src/analyzer/view/AnalyzerScreenSummaryContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* AnalyzerScreenSummaryContent.ts
*
* The accessible screen summary read by screen readers (SceneryStack's
* Interactive Description). `currentDetailsContent` is a LIVE `DerivedProperty`
* over the model — the number of light-curve measurements and the current trial
* period — so a non-visual user can re-read the state at any time.
*/
import { DerivedProperty } from "scenerystack/axon";
import { ScreenSummaryContent } from "scenerystack/sim";
import { StringManager } from "../../i18n/StringManager.js";
import type { AnalyzerModel } from "../model/AnalyzerModel.js";

export class AnalyzerScreenSummaryContent extends ScreenSummaryContent {
public constructor(model: AnalyzerModel) {
const a11y = StringManager.getInstance().getAnalyzerA11yStrings();

const currentDetails = new DerivedProperty(
[
model.measurementsProperty,
model.trialPeriodProperty,
a11y.currentDetailsWithDataPatternStringProperty,
a11y.currentDetailsNoDataPatternStringProperty,
],
(measurements, trialPeriod, withDataPattern, noDataPattern) =>
measurements.length === 0
? noDataPattern
: withDataPattern
.replace("{{count}}", String(measurements.length))
.replace("{{period}}", trialPeriod.toFixed(4)),
);

super({
playAreaContent: a11y.screenSummary.playAreaStringProperty,
controlAreaContent: a11y.screenSummary.controlAreaStringProperty,
currentDetailsContent: currentDetails,
interactionHintContent: a11y.screenSummary.interactionHintStringProperty,
});
}
}
81 changes: 66 additions & 15 deletions src/analyzer/view/AnalyzerScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class AnalyzerScreenView extends ScreenView {
const tandem = options?.tandem instanceof Tandem ? options.tandem : Tandem.OPT_OUT;
const strings = StringManager.getInstance().getAnalyzerViewStrings();
const unitStrings = StringManager.getInstance().getUnitStrings();
const a11yControls = StringManager.getInstance().getAnalyzerA11yStrings().controls;
const showCrosshairProperty = new BooleanProperty(true);
const showDifferenceToolProperty = new BooleanProperty(false);

Expand Down Expand Up @@ -229,11 +230,12 @@ export class AnalyzerScreenView extends ScreenView {
font: SMALL_FONT,
baseColor: VSPColors.buttonColorProperty,
listener: () => model.clearSelections(),
accessibleName: strings.clearSelectionStringProperty,
});
const crosshairCheckbox = new Checkbox(
showCrosshairProperty,
new Text(strings.showCrosshairsStringProperty, { font: SMALL_FONT }),
{ boxWidth: 14 },
{ boxWidth: 14, accessibleName: strings.showCrosshairsStringProperty },
);

const leftColumn = new VBox({
Expand Down Expand Up @@ -502,22 +504,41 @@ export class AnalyzerScreenView extends ScreenView {
const modeRadioGroup = new AquaRadioButtonGroup<LightCurveMode>(
model.lightCurveModeProperty,
[
{ value: "time", createNode: () => new Text(strings.timeStringProperty, { font: LABEL_FONT }) },
{ value: "phase", createNode: () => new Text(strings.phaseStringProperty, { font: LABEL_FONT }) },
{
value: "time",
createNode: () => new Text(strings.timeStringProperty, { font: LABEL_FONT }),
options: { accessibleName: strings.timeStringProperty },
},
{
value: "phase",
createNode: () => new Text(strings.phaseStringProperty, { font: LABEL_FONT }),
options: { accessibleName: strings.phaseStringProperty },
},
],
{ orientation: "horizontal", spacing: 16, radioButtonOptions: { radius: 7 } },
{
orientation: "horizontal",
spacing: 16,
radioButtonOptions: { radius: 7 },
accessibleName: a11yControls.lightCurveModeStringProperty,
},
);

const phaseOffsetControl = new NumberControl("phase offset (days)", model.phaseOffsetProperty, PHASE_OFFSET_RANGE, {
titleNodeOptions: { font: SMALL_FONT },
numberDisplayOptions: { textOptions: { font: SMALL_FONT } },
sliderOptions: { trackSize: new Dimension2(120, 3) },
layoutFunction: NumberControl.createLayoutFunction1(),
});
const phaseOffsetControl = new NumberControl(
strings.phaseOffsetStringProperty,
model.phaseOffsetProperty,
PHASE_OFFSET_RANGE,
{
titleNodeOptions: { font: SMALL_FONT },
numberDisplayOptions: { textOptions: { font: SMALL_FONT } },
sliderOptions: { trackSize: new Dimension2(120, 3) },
layoutFunction: NumberControl.createLayoutFunction1(),
accessibleName: strings.phaseOffsetStringProperty,
},
);
const differenceToolCheckbox = new Checkbox(
showDifferenceToolProperty,
new Text("show difference tool", { font: LABEL_FONT }),
{ boxWidth: 16 },
new Text(strings.showDifferenceToolStringProperty, { font: LABEL_FONT }),
{ boxWidth: 16, accessibleName: strings.showDifferenceToolStringProperty },
);

// Assemble observations panel (title, [y-label | chart], x-label, radios).
Expand Down Expand Up @@ -577,7 +598,7 @@ export class AnalyzerScreenView extends ScreenView {
const pdmLine = new LinePlot(pdmTransform, [], { stroke: VSPColors.lightCurveColorProperty, lineWidth: 1.5 });
const periodMarker = new Line(0, 0, 0, PDM_H, { stroke: VSPColors.pdmMarkerColorProperty, lineWidth: 2 });
const pdmZoomSelection = new Rectangle(0, 0, 0, PDM_H, {
fill: "rgba(120, 170, 255, 0.22)",
fill: VSPColors.pdmZoomSelectionFillProperty,
stroke: VSPColors.pdmMarkerColorProperty,
lineWidth: 1,
visible: false,
Expand Down Expand Up @@ -700,13 +721,14 @@ export class AnalyzerScreenView extends ScreenView {
},
sliderOptions: { trackSize: new Dimension2(120, 3) },
layoutFunction: NumberControl.createLayoutFunction1(),
accessibleName: strings.trialPeriodAxisStringProperty,
},
);

const bestPeriodReadout = new Text(
new DerivedProperty([model.pdmScanResultsProperty], (scan) => {
new DerivedProperty([model.pdmScanResultsProperty, strings.bestPeriodPatternStringProperty], (scan, pattern) => {
const best = bestPeriod(scan);
return best === null ? "" : `best: ${best.toFixed(4)} d`;
return best === null ? "" : pattern.replace("{{value}}", best.toFixed(4));
}),
{ font: SMALL_FONT, fill: VSPColors.mutedTextColorProperty },
);
Expand All @@ -715,25 +737,30 @@ export class AnalyzerScreenView extends ScreenView {
font: SMALL_FONT,
baseColor: VSPColors.buttonActiveColorProperty,
listener: () => model.zoomInAroundPeriod(),
accessibleName: strings.zoomInAroundPeriodStringProperty,
});
const zoomOutButton = new TextPushButton(strings.zoomOutAroundPeriodStringProperty, {
font: SMALL_FONT,
baseColor: VSPColors.buttonActiveColorProperty,
listener: () => model.zoomOutAroundPeriod(),
accessibleName: strings.zoomOutAroundPeriodStringProperty,
});
const fullButton = new TextPushButton(strings.zoomToFullStringProperty, {
font: SMALL_FONT,
baseColor: VSPColors.buttonColorProperty,
listener: () => model.zoomToFull(),
accessibleName: strings.zoomToFullStringProperty,
});
const undoButton = new TextPushButton(strings.undoLastZoomStringProperty, {
font: SMALL_FONT,
baseColor: VSPColors.buttonColorProperty,
listener: () => model.undoLastZoom(),
accessibleName: strings.undoLastZoomStringProperty,
});
const snapButton = new TextPushButton(strings.snapToMinStringProperty, {
font: SMALL_FONT,
baseColor: VSPColors.buttonSnapColorProperty,
accessibleName: strings.snapToMinStringProperty,
listener: () => {
const best = bestPeriod(model.pdmScanResultsProperty.value);
if (best !== null) {
Expand Down Expand Up @@ -787,5 +814,29 @@ export class AnalyzerScreenView extends ScreenView {
tandem: tandem.createTandem("resetAllButton"),
});
this.addChild(resetAllButton);

// -----------------------------------------------------------------------
// Accessibility: keyboard / reading traversal order. ScreenView throws if
// pdomOrder is set on itself, so a wrapper Node "borrows" the interactive
// nodes — star-field controls, then plot controls, then PDM, Reset All last.
// -----------------------------------------------------------------------
this.addChild(
new Node({
pdomOrder: [
clearButton,
crosshairCheckbox,
modeRadioGroup,
phaseOffsetControl,
differenceToolCheckbox,
periodControl,
zoomInButton,
zoomOutButton,
fullButton,
undoButton,
snapButton,
resetAllButton,
],
}),
);
}
}
11 changes: 4 additions & 7 deletions src/blink-comparator/BlinkComparatorScreen.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { type EmptySelfOptions, optionize } from "scenerystack/phet-core";
import type { ScreenOptions } from "scenerystack/sim";
import { Screen, ScreenSummaryContent } from "scenerystack/sim";
import { Screen } from "scenerystack/sim";
import type { Tandem } from "scenerystack/tandem";
import { VSPKeyboardHelpContent } from "../common/view/VSPKeyboardHelpContent.js";
import { StringManager } from "../i18n/StringManager.js";
import type { VSPPreferencesModel } from "../preferences/VSPPreferencesModel.js";
import VSPColors from "../VSPColors.js";
import { BlinkComparatorModel } from "./model/BlinkComparatorModel.js";
import { BlinkComparatorScreenSummaryContent } from "./view/BlinkComparatorScreenSummaryContent.js";
import { BlinkComparatorScreenView } from "./view/BlinkComparatorScreenView.js";

type BlinkComparatorScreenOptions = ScreenOptions & { tandem: Tandem; preferences: VSPPreferencesModel };

export class BlinkComparatorScreen extends Screen<BlinkComparatorModel, BlinkComparatorScreenView> {
public constructor(options: BlinkComparatorScreenOptions) {
const summaryStrings = StringManager.getInstance().getA11yStrings().screenSummary.blinkComparator;
const summaryStrings = StringManager.getInstance().getBlinkComparatorA11yStrings().screenSummary;
super(
() => new BlinkComparatorModel(options.tandem.createTandem("model")),
(model) =>
new BlinkComparatorScreenView(model, options.preferences, {
tandem: options.tandem.createTandem("view"),
screenSummaryContent: new ScreenSummaryContent({
playAreaContent: summaryStrings.playAreaStringProperty,
controlAreaContent: summaryStrings.controlAreaStringProperty,
interactionHintContent: summaryStrings.interactionHintStringProperty,
}),
screenSummaryContent: new BlinkComparatorScreenSummaryContent(model),
}),
optionize<BlinkComparatorScreenOptions, EmptySelfOptions, ScreenOptions>()(
{
Expand Down
36 changes: 36 additions & 0 deletions src/blink-comparator/view/BlinkComparatorScreenSummaryContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* BlinkComparatorScreenSummaryContent.ts
*
* The accessible screen summary read by screen readers (SceneryStack's
* Interactive Description). `currentDetailsContent` is a LIVE `DerivedProperty`
* over the model — how many observations are queued and whether blinking is
* running — so a non-visual user can re-read the state at any time.
*/
import { DerivedProperty } from "scenerystack/axon";
import { ScreenSummaryContent } from "scenerystack/sim";
import { StringManager } from "../../i18n/StringManager.js";
import type { BlinkComparatorModel } from "../model/BlinkComparatorModel.js";

export class BlinkComparatorScreenSummaryContent extends ScreenSummaryContent {
public constructor(model: BlinkComparatorModel) {
const a11y = StringManager.getInstance().getBlinkComparatorA11yStrings();

const currentDetails = new DerivedProperty(
[
model.blinkQueue.lengthProperty,
model.isBlinkingProperty,
a11y.currentDetailsBlinkingPatternStringProperty,
a11y.currentDetailsPausedPatternStringProperty,
],
(count, isBlinking, blinkingPattern, pausedPattern) =>
(isBlinking ? blinkingPattern : pausedPattern).replace("{{count}}", String(count)),
);

super({
playAreaContent: a11y.screenSummary.playAreaStringProperty,
controlAreaContent: a11y.screenSummary.controlAreaStringProperty,
currentDetailsContent: currentDetails,
interactionHintContent: a11y.screenSummary.interactionHintStringProperty,
});
}
}
Loading