From 278a013ee795bbc9ed059ee4f992151adfef8688 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 22:00:11 +0000 Subject: [PATCH] Split screens by counting source; let both show either chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Simulation screen now carries only the mock-up activity slider and the Bluetooth Device screen only the Geiger counter connection controls — each screen is fixed to one source via RadioactivityModel's new fixedSourceType option. A new View panel (Histogram / Count rate over time) lets either screen show either chart, backed by a shared RadioactivityScreenModel and RadioactivityScreenView that both screens now use. Renamed intro/ -> simulation/ and lab/ -> lab/device/ with matching string, icon, and doc updates across all three locales. --- CLAUDE.md | 8 +- doc/implementation-notes.md | 33 +- doc/model.md | 2 +- src/RadioactivityAndStatisticsColors.ts | 2 +- src/RadioactivityAndStatisticsConstants.ts | 4 +- .../RadioactivityAndStatisticsScreenIcons.ts | 19 +- src/common/model/ChartViewType.ts | 21 + src/common/model/RadioactivityModel.ts | 11 +- .../model/RadioactivityScreenModel.ts} | 41 +- src/common/view/AcquisitionPanel.ts | 4 +- src/common/view/ChartViewPanel.ts | 69 +++ src/common/view/DistributionControlsPanel.ts | 4 +- src/common/view/RadioactivityScreenView.ts | 172 +++++++ src/common/view/SourcePanel.ts | 449 +++++++++--------- src/device/DeviceScreen.ts | 50 ++ src/device/model/DeviceModel.ts | 23 + src/device/view/DeviceScreenSummaryContent.ts | 38 ++ src/i18n/StringManager.ts | 55 +-- src/i18n/strings_en.json | 38 +- src/i18n/strings_es.json | 38 +- src/i18n/strings_fr.json | 38 +- src/intro/IntroScreen.ts | 40 -- src/intro/model/IntroModel.ts | 37 -- src/intro/view/IntroScreenView.ts | 124 ----- src/lab/LabScreen.ts | 40 -- src/lab/view/LabScreenSummaryContent.ts | 38 -- src/lab/view/LabScreenView.ts | 123 ----- src/main.ts | 16 +- src/simulation/SimulationScreen.ts | 50 ++ src/simulation/model/SimulationModel.ts | 26 + .../view/SimulationScreenSummaryContent.ts} | 20 +- 31 files changed, 861 insertions(+), 772 deletions(-) create mode 100644 src/common/model/ChartViewType.ts rename src/{lab/model/LabModel.ts => common/model/RadioactivityScreenModel.ts} (52%) create mode 100644 src/common/view/ChartViewPanel.ts create mode 100644 src/common/view/RadioactivityScreenView.ts create mode 100644 src/device/DeviceScreen.ts create mode 100644 src/device/model/DeviceModel.ts create mode 100644 src/device/view/DeviceScreenSummaryContent.ts delete mode 100644 src/intro/IntroScreen.ts delete mode 100644 src/intro/model/IntroModel.ts delete mode 100644 src/intro/view/IntroScreenView.ts delete mode 100644 src/lab/LabScreen.ts delete mode 100644 src/lab/view/LabScreenSummaryContent.ts delete mode 100644 src/lab/view/LabScreenView.ts create mode 100644 src/simulation/SimulationScreen.ts create mode 100644 src/simulation/model/SimulationModel.ts rename src/{intro/view/IntroScreenSummaryContent.ts => simulation/view/SimulationScreenSummaryContent.ts} (60%) diff --git a/CLAUDE.md b/CLAUDE.md index 21818f9..bf68959 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,8 @@ changing the acquisition or hardware layers. | File | Purpose | |---|---| -| `src/common/model/RadioactivityModel.ts` | The shared acquisition model — sources, counting cycle, run, derived statistics. Both screens compose it | +| `src/common/model/RadioactivityModel.ts` | The shared acquisition model — sources, counting cycle, run, derived statistics. Each screen's model locks it to one fixed source | +| `src/common/model/RadioactivityScreenModel.ts` | Composes `RadioactivityModel` with the screen-level display state (chart view, curve visibility) both screens share; `SimulationModel`/`DeviceModel` just fix the source and the default chart | | `src/common/model/CountSource.ts` | The `TCountSource` contract: a monotonic running total. The reason hardware and simulated data share one code path | | `src/common/model/SimulatedCountSource.ts` | Poisson event generator (the default source, and the only one with a known λ) | | `src/common/model/GeigerCountSource.ts` | Hardware source: connection lifecycle, polling loop, register interpretation | @@ -27,8 +28,9 @@ changing the acquisition or hardware layers. | `src/common/model/Statistics.ts` | Welford statistics, log-gamma, Poisson and Gaussian distributions | | `src/common/model/Histogram.ts` | Integer binning, bin-width choice, per-bin expected frequencies | | `src/common/model/GaussianFit.ts` | Levenberg–Marquardt fit with Poisson weighting | -| `src/common/view/HistogramNode.ts` | Lab centrepiece: bars plus the three model curves | -| `src/common/view/CountRateChartNode.ts` | Intro strip chart: rate against time, with the mean | +| `src/common/view/HistogramNode.ts` | The histogram view: bars plus the three model curves | +| `src/common/view/CountRateChartNode.ts` | The count-rate view: rate against time, with the mean | +| `src/common/view/RadioactivityScreenView.ts` | The view shared by both screens; a chart-view switch chooses which of the above two is shown | | `src/RadioactivityAndStatisticsColors.ts` | All `ProfileColorProperty` instances, including the validated chart palette | | `src/RadioactivityAndStatisticsConstants.ts` | Layout, chart sizes, acquisition ranges, timing guards | diff --git a/doc/implementation-notes.md b/doc/implementation-notes.md index cc42917..d15bb6a 100644 --- a/doc/implementation-notes.md +++ b/doc/implementation-notes.md @@ -5,26 +5,35 @@ obvious from the code. ## Shape of the sim -Two screens over one shared acquisition model. +Two screens, told apart only by which counting source they are fixed to, over +one shared acquisition model and one shared view. ``` src/ common/ hardware/ PascoProtocol.ts GeigerCounterDevice.ts webBluetoothSupport.ts - model/ RadioactivityModel.ts CountSource.ts SimulatedCountSource.ts - GeigerCountSource.ts Statistics.ts Histogram.ts GaussianFit.ts - CountSample.ts csvExport.ts ConnectionState.ts - view/ SourcePanel AcquisitionPanel DataTableNode StatisticsPanel - HistogramNode CountRateChartNode CountRateDisplayNode + model/ RadioactivityModel.ts RadioactivityScreenModel.ts CountSource.ts + ChartViewType.ts SimulatedCountSource.ts GeigerCountSource.ts + Statistics.ts Histogram.ts GaussianFit.ts CountSample.ts + csvExport.ts ConnectionState.ts + view/ RadioactivityScreenView.ts SourcePanel AcquisitionPanel + ChartViewPanel DataTableNode StatisticsPanel HistogramNode + CountRateChartNode CountRateDisplayNode DistributionControlsPanel currentDetailsProperty downloadCsv - intro/ model/IntroModel.ts view/IntroScreenView.ts - lab/ model/LabModel.ts view/LabScreenView.ts + simulation/ model/SimulationModel.ts + device/ model/DeviceModel.ts ``` -`IntroModel` and `LabModel` **compose** `RadioactivityModel` rather than extend -it. Composition keeps the shared model free of any one screen's assumptions: -the Lab screen adds curve-visibility state without the Intro screen carrying it, -and neither screen can quietly change acquisition semantics for the other. +`RadioactivityModel` **composes**, rather than is extended by, the two count +sources; `RadioactivityScreenModel` in turn composes `RadioactivityModel` and +adds the state both screens need to display it — which chart is shown, and +which theoretical curves are drawn over the histogram. `SimulationModel` and +`DeviceModel` are thin subclasses that only fix which source +`RadioactivityModel` is locked to (`CountSourceType.SIMULATED` or +`GEIGER_COUNTER`) and which chart the screen opens on; `RadioactivityScreenView` +is the one view class both screens use. Composition keeps the shared +acquisition model free of any one screen's assumptions, and a screen can no +longer quietly change acquisition semantics for the other. ## The count-source abstraction diff --git a/doc/model.md b/doc/model.md index 1a414c7..4432617 100644 --- a/doc/model.md +++ b/doc/model.md @@ -47,7 +47,7 @@ as a run of 10 and a standard error about three times smaller. ## The Gaussian limit For large λ the Poisson distribution approaches a Gaussian with the same mean -and σ = √λ. The Lab screen can draw that Gaussian on top of the histogram. +and σ = √λ. Either screen can draw that Gaussian on top of the histogram. The approximation is not uniformly good, and the sim lets that be seen. At the peak the two agree to a fraction of a percent even at λ = 100; one standard diff --git a/src/RadioactivityAndStatisticsColors.ts b/src/RadioactivityAndStatisticsColors.ts index 8ed2713..0c9a929 100644 --- a/src/RadioactivityAndStatisticsColors.ts +++ b/src/RadioactivityAndStatisticsColors.ts @@ -136,7 +136,7 @@ const RadioactivityAndStatisticsColors = { projector: "#5b6b8c", }), - /** Count-rate trace on the Intro screen's strip chart (a single series). */ + /** Count-rate trace on the count-rate strip chart (a single series). */ countRateTraceColorProperty: new ProfileColorProperty(RadioactivityAndStatisticsNamespace, "countRateTrace", { default: "#3987e5", projector: "#2a78d6", diff --git a/src/RadioactivityAndStatisticsConstants.ts b/src/RadioactivityAndStatisticsConstants.ts index 2a45086..b0cd4b0 100644 --- a/src/RadioactivityAndStatisticsConstants.ts +++ b/src/RadioactivityAndStatisticsConstants.ts @@ -39,10 +39,10 @@ export const CONTROL_PANEL_WIDTH = 225; // ── Charts (screen pixels) ──────────────────────────────────────────────────── -/** Plot area of the Lab screen's histogram. */ +/** Plot area of the histogram, shown on either screen. */ export const HISTOGRAM_CHART_SIZE = { width: 380, height: 440 } as const; -/** Plot area of the Intro screen's count-rate strip chart. */ +/** Plot area of the count-rate strip chart, shown on either screen. */ export const RATE_CHART_SIZE = { width: 420, height: 330 } as const; /** Stroke width of plotted model curves. */ diff --git a/src/common/RadioactivityAndStatisticsScreenIcons.ts b/src/common/RadioactivityAndStatisticsScreenIcons.ts index e5caef1..87d90c4 100644 --- a/src/common/RadioactivityAndStatisticsScreenIcons.ts +++ b/src/common/RadioactivityAndStatisticsScreenIcons.ts @@ -4,10 +4,13 @@ * Programmatic home-screen and navigation-bar icons, drawn on the standard PhET * 548 × 373 canvas using the sim's own colors so they follow the active profile. * - * Each icon is a miniature of what its screen is about: the Intro icon is a - * fluctuating count-rate trace about its mean, the Lab icon is a histogram with - * a bell curve over it. They use the same colours as the real charts, so the - * home screen previews what the screen actually looks like. + * Both screens can show either chart now, so the icons no longer distinguish + * "fluctuation" from "distribution" — instead each is a miniature of its + * screen's fixed source: the Simulation icon is a fluctuating count-rate + * trace, evoking the mock-up source's adjustable activity; the Device icon is + * a histogram with a bell curve, evoking the real counter's collected run. + * They use the same colours as the real charts, so the home screen previews + * what the screen actually looks like. */ import { Shape } from "scenerystack/kite"; import { Circle, Line, Node, Path, Rectangle } from "scenerystack/scenery"; @@ -33,13 +36,13 @@ function iconFrom(content: Node): ScreenIcon { } /** - * Intro: a count rate scattering about its mean. + * Simulation: a count rate scattering about its mean. * * The sample heights are fixed rather than random so the icon is identical on * every launch — an icon that changed shape between sessions would read as a * different screen. */ -export function createIntroIcon(): ScreenIcon { +export function createSimulationIcon(): ScreenIcon { const samples = [0.55, 0.78, 0.34, 0.62, 0.45, 0.86, 0.5, 0.28, 0.7, 0.4]; const left = INSET; const right = W - INSET; @@ -87,8 +90,8 @@ export function createIntroIcon(): ScreenIcon { ); } -/** Lab: a histogram of counts with the Poisson curve drawn over it. */ -export function createLabIcon(): ScreenIcon { +/** Device: a histogram of counts with the Poisson curve drawn over it. */ +export function createDeviceIcon(): ScreenIcon { const bars = [0.12, 0.3, 0.62, 0.92, 0.78, 0.45, 0.2, 0.08]; const left = INSET; const right = W - INSET; diff --git a/src/common/model/ChartViewType.ts b/src/common/model/ChartViewType.ts new file mode 100644 index 0000000..55eafae --- /dev/null +++ b/src/common/model/ChartViewType.ts @@ -0,0 +1,21 @@ +/** + * ChartViewType.ts + * + * Which of the two model charts a screen is currently showing. Both screens + * carry the same acquisition machinery and can display either chart — only the + * counting source is fixed per screen — so the choice between them is a single + * enum rather than two screen-specific booleans. + */ + +/** Which chart is drawn in the centre of the screen. */ +export const ChartViewType = { + /** Distribution of counts per interval, with the theoretical curves. */ + HISTOGRAM: "histogram", + /** Count rate against time, with the running mean. */ + COUNT_RATE: "countRate", +} as const; + +export type ChartViewTypeValue = (typeof ChartViewType)[keyof typeof ChartViewType]; + +/** Ordered for the view-choice radio button group. */ +export const CHART_VIEW_TYPES: readonly ChartViewTypeValue[] = [ChartViewType.HISTOGRAM, ChartViewType.COUNT_RATE]; diff --git a/src/common/model/RadioactivityModel.ts b/src/common/model/RadioactivityModel.ts index 8259cac..3b4df20 100644 --- a/src/common/model/RadioactivityModel.ts +++ b/src/common/model/RadioactivityModel.ts @@ -43,6 +43,15 @@ import { computeStatistics, type SampleStatistics } from "./Statistics.js"; export type RadioactivityModelOptions = { /** Host-side Geiger controls from Preferences → Simulation. */ readonly geigerControls?: GeigerDeviceControls; + + /** + * Locks {@link sourceTypeProperty} to one source for the lifetime of the + * model (reset returns to this value too, since it is the Property's + * initial value). Each screen now has a fixed counting source, so there is + * no UI that could ever change it away from this. Defaults to + * {@link CountSourceType.SIMULATED}. + */ + readonly fixedSourceType?: CountSourceTypeValue; }; export class RadioactivityModel implements TModel { @@ -132,7 +141,7 @@ export class RadioactivityModel implements TModel { this.simulatedSource = new SimulatedCountSource(DEFAULT_ACTIVITY); this.geigerSource = new GeigerCountSource(options?.geigerControls ?? null); - this.sourceTypeProperty = new Property(CountSourceType.SIMULATED); + this.sourceTypeProperty = new Property(options?.fixedSourceType ?? CountSourceType.SIMULATED); this.activeSourceProperty = new DerivedProperty( [this.sourceTypeProperty], (sourceType): TCountSource => diff --git a/src/lab/model/LabModel.ts b/src/common/model/RadioactivityScreenModel.ts similarity index 52% rename from src/lab/model/LabModel.ts rename to src/common/model/RadioactivityScreenModel.ts index 7027361..f17651f 100644 --- a/src/lab/model/LabModel.ts +++ b/src/common/model/RadioactivityScreenModel.ts @@ -1,24 +1,35 @@ /** - * LabModel.ts + * RadioactivityScreenModel.ts * - * Model for the Lab screen: the distribution of the measurements, and how it - * compares with theory. - * - * Adds only what the Intro screen has no use for — which theoretical curves are - * drawn — on top of the shared {@link RadioactivityModel}. Everything about - * collecting data, including the histogram and the Gaussian fit, is shared; - * the Lab screen simply chooses to display it. + * Model shared by both screens: a {@link RadioactivityModel} locked to one + * counting source, plus the display choices that used to live on the Lab + * screen alone — which chart is shown, and which theoretical curves are drawn + * over the histogram. Both screens can show either chart now, so both need + * this state; only the fixed source tells the two screens apart. */ -import { BooleanProperty } from "scenerystack/axon"; +import { BooleanProperty, Property } from "scenerystack/axon"; import type { TModel } from "scenerystack/joist"; -import { RadioactivityModel } from "../../common/model/RadioactivityModel.js"; import type { RadioactivityAndStatisticsPreferencesModel } from "../../preferences/RadioactivityAndStatisticsPreferencesModel.js"; +import type { ChartViewTypeValue } from "./ChartViewType.js"; +import type { CountSourceTypeValue } from "./CountSource.js"; +import { RadioactivityModel } from "./RadioactivityModel.js"; + +export type RadioactivityScreenModelOptions = { + /** The one source this screen ever counts from. */ + readonly fixedSourceType: CountSourceTypeValue; -export class LabModel implements TModel { + /** Which chart the screen opens on. */ + readonly initialChartView: ChartViewTypeValue; +}; + +export class RadioactivityScreenModel implements TModel { /** Sources, counting cycle, collected run, and derived statistics. */ public readonly acquisition: RadioactivityModel; + /** Which chart is currently shown: the histogram, or the count-rate trace. */ + public readonly chartViewProperty: Property; + /** * Whether the Poisson prediction is drawn, using λ = the measured mean. * @@ -38,17 +49,23 @@ export class LabModel implements TModel { /** Whether the least-squares best-fit Gaussian is drawn. */ public readonly gaussianFitVisibleProperty = new BooleanProperty(false); - public constructor(preferences: RadioactivityAndStatisticsPreferencesModel) { + public constructor( + preferences: RadioactivityAndStatisticsPreferencesModel, + options: RadioactivityScreenModelOptions, + ) { this.acquisition = new RadioactivityModel({ + fixedSourceType: options.fixedSourceType, geigerControls: { beepEnabledProperty: preferences.beepEnabledProperty, tubeVoltageProperty: preferences.tubeVoltageProperty, }, }); + this.chartViewProperty = new Property(options.initialChartView); } public reset(): void { this.acquisition.reset(); + this.chartViewProperty.reset(); this.poissonVisibleProperty.reset(); this.gaussianPredictionVisibleProperty.reset(); this.gaussianFitVisibleProperty.reset(); diff --git a/src/common/view/AcquisitionPanel.ts b/src/common/view/AcquisitionPanel.ts index 6122906..4505022 100644 --- a/src/common/view/AcquisitionPanel.ts +++ b/src/common/view/AcquisitionPanel.ts @@ -13,7 +13,7 @@ import { DerivedProperty, PatternStringProperty } from "scenerystack/axon"; import { HBox, Node, Text, VBox } from "scenerystack/scenery"; import { NumberControl, PhetFont } from "scenerystack/scenery-phet"; import { Checkbox, RectangularPushButton } from "scenerystack/sun"; -import type { SharedControlA11yStrings } from "../../i18n/StringManager.js"; +import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js"; import { StringManager } from "../../i18n/StringManager.js"; import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js"; import { @@ -31,7 +31,7 @@ import { downloadCsv } from "./downloadCsv.js"; export class AcquisitionPanel extends RadioactivityAndStatisticsPanel { private readonly disposeAcquisitionPanel: () => void; - public constructor(model: RadioactivityModel, a11y: SharedControlA11yStrings) { + public constructor(model: RadioactivityModel, a11y: ScreenControlA11yStrings) { const stringManager = StringManager.getInstance(); const strings = stringManager.getAcquisitionStrings(); const readoutStrings = stringManager.getReadoutStrings(); diff --git a/src/common/view/ChartViewPanel.ts b/src/common/view/ChartViewPanel.ts new file mode 100644 index 0000000..555990d --- /dev/null +++ b/src/common/view/ChartViewPanel.ts @@ -0,0 +1,69 @@ +/** + * ChartViewPanel.ts + * + * Chooses which of the two model charts is drawn: the histogram, or the count + * rate over time. Every screen now carries both charts, so this is the one + * control that decides which one is on screen. + */ + +import type { Property } from "scenerystack/axon"; +import { Text, VBox } from "scenerystack/scenery"; +import { PhetFont } from "scenerystack/scenery-phet"; +import { AquaRadioButtonGroup } from "scenerystack/sun"; +import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js"; +import { StringManager } from "../../i18n/StringManager.js"; +import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js"; +import { CONTROL_PANEL_WIDTH } from "../../RadioactivityAndStatisticsConstants.js"; +import { ChartViewType, type ChartViewTypeValue } from "../model/ChartViewType.js"; +import { RadioactivityAndStatisticsPanel } from "../RadioactivityAndStatisticsPanel.js"; + +export class ChartViewPanel extends RadioactivityAndStatisticsPanel { + public constructor(chartViewProperty: Property, a11y: ScreenControlA11yStrings) { + const strings = StringManager.getInstance().getChartViewStrings(); + + const title = new Text(strings.titleStringProperty, { + font: new PhetFont({ size: 15, weight: "bold" }), + fill: RadioactivityAndStatisticsColors.textColorProperty, + }); + + const radioGroup = new AquaRadioButtonGroup( + chartViewProperty, + [ + { + value: ChartViewType.HISTOGRAM, + createNode: () => + new Text(strings.histogramStringProperty, { + font: new PhetFont(13), + fill: RadioactivityAndStatisticsColors.textColorProperty, + maxWidth: 170, + }), + }, + { + value: ChartViewType.COUNT_RATE, + createNode: () => + new Text(strings.countRateStringProperty, { + font: new PhetFont(13), + fill: RadioactivityAndStatisticsColors.textColorProperty, + maxWidth: 170, + }), + }, + ], + { + spacing: 6, + radioButtonOptions: { radius: 7 }, + accessibleName: a11y.chartViewRadioGroupStringProperty, + }, + ); + + super( + new VBox({ + align: "left", + spacing: 8, + preferredWidth: CONTROL_PANEL_WIDTH - 24, + stretch: true, + children: [title, radioGroup], + }), + { minWidth: CONTROL_PANEL_WIDTH }, + ); + } +} diff --git a/src/common/view/DistributionControlsPanel.ts b/src/common/view/DistributionControlsPanel.ts index b84ba5b..75e9dcd 100644 --- a/src/common/view/DistributionControlsPanel.ts +++ b/src/common/view/DistributionControlsPanel.ts @@ -17,7 +17,7 @@ import { type BooleanProperty, DerivedProperty, type TReadOnlyProperty } from "s import { Text, VBox } from "scenerystack/scenery"; import { NumberControl, PhetFont } from "scenerystack/scenery-phet"; import { Checkbox } from "scenerystack/sun"; -import type { LabControlA11yStrings } from "../../i18n/StringManager.js"; +import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js"; import { StringManager } from "../../i18n/StringManager.js"; import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js"; import { BIN_WIDTH_RANGE, CONTROL_PANEL_WIDTH } from "../../RadioactivityAndStatisticsConstants.js"; @@ -39,7 +39,7 @@ export type CurveVisibilityControls = { export class DistributionControlsPanel extends RadioactivityAndStatisticsPanel { private readonly disposeDistributionControlsPanel: () => void; - public constructor(model: RadioactivityModel, curves: CurveVisibilityControls, a11y: LabControlA11yStrings) { + public constructor(model: RadioactivityModel, curves: CurveVisibilityControls, a11y: ScreenControlA11yStrings) { const strings = StringManager.getInstance().getHistogramStrings(); const title = new Text(strings.titleStringProperty, { diff --git a/src/common/view/RadioactivityScreenView.ts b/src/common/view/RadioactivityScreenView.ts new file mode 100644 index 0000000..e47040d --- /dev/null +++ b/src/common/view/RadioactivityScreenView.ts @@ -0,0 +1,172 @@ +/** + * RadioactivityScreenView.ts + * + * The view shared by both screens: a fixed-source {@link SourcePanel} plus + * {@link AcquisitionPanel} on the left, a chart-view switch on the right that + * swaps the rest of the right column between the histogram's statistics and + * curve controls or the count-rate chart's live readout and data table, and + * the chosen chart itself in the centre. + * + * ── Layout ──────────────────────────────────────────────────────────────────── + * Only one context block (histogram or count-rate) and one chart are ever + * visible at a time; the other sits alongside it, invisible, inside a plain + * `Node` rather than a layout container — the same pattern {@link SourcePanel} + * used for its own mutually-exclusive source blocks — so the visible one alone + * decides the bounds. + */ + +import type { TReadOnlyProperty } from "scenerystack/axon"; +import { DerivedProperty } from "scenerystack/axon"; +import { Bounds2 } from "scenerystack/dot"; +import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; +import { AlignBox, Node, VBox } from "scenerystack/scenery"; +import { ResetAllButton } from "scenerystack/scenery-phet"; +import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; +import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js"; +import { CENTRE_COLUMN_PADDING, PANEL_SPACING, SCREEN_VIEW_MARGIN } from "../../RadioactivityAndStatisticsConstants.js"; +import { ChartViewType } from "../model/ChartViewType.js"; +import type { CountSourceTypeValue } from "../model/CountSource.js"; +import type { RadioactivityScreenModel } from "../model/RadioactivityScreenModel.js"; +import { FLAT_RESET_ALL_BUTTON_OPTIONS } from "../RadioactivityAndStatisticsButtonOptions.js"; +import { AcquisitionPanel } from "./AcquisitionPanel.js"; +import { ChartViewPanel } from "./ChartViewPanel.js"; +import { CountRateChartNode } from "./CountRateChartNode.js"; +import { CountRateDisplayNode } from "./CountRateDisplayNode.js"; +import { DataTableNode } from "./DataTableNode.js"; +import { DistributionControlsPanel } from "./DistributionControlsPanel.js"; +import { HistogramNode } from "./HistogramNode.js"; +import { SourcePanel } from "./SourcePanel.js"; +import { StatisticsPanel } from "./StatisticsPanel.js"; + +export type RadioactivityScreenViewOptions = ScreenViewOptions; + +export class RadioactivityScreenView extends ScreenView { + public constructor( + model: RadioactivityScreenModel, + fixedSourceType: CountSourceTypeValue, + showDiagnosticsProperty: TReadOnlyProperty, + a11y: ScreenControlA11yStrings, + providedOptions: RadioactivityScreenViewOptions, + ) { + const options = optionize()( + {}, + providedOptions, + ); + super(options); + + const acquisition = model.acquisition; + + // ── Left column: choose a source, then set up and run a measurement ─────── + const sourcePanel = new SourcePanel(acquisition, fixedSourceType, a11y, showDiagnosticsProperty); + const acquisitionPanel = new AcquisitionPanel(acquisition, a11y); + + const leftColumn = new VBox({ + align: "left", + spacing: PANEL_SPACING, + children: [sourcePanel, acquisitionPanel], + left: this.layoutBounds.minX + SCREEN_VIEW_MARGIN, + top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, + }); + + // ── Right column: choose the chart, then see what it implies ────────────── + const chartViewPanel = new ChartViewPanel(model.chartViewProperty, a11y); + + const isHistogramProperty = new DerivedProperty( + [model.chartViewProperty], + (chartView) => chartView === ChartViewType.HISTOGRAM, + ); + const isCountRateProperty = new DerivedProperty([isHistogramProperty], (isHistogram) => !isHistogram); + + const statisticsPanel = new StatisticsPanel(acquisition); + const distributionControlsPanel = new DistributionControlsPanel(acquisition, model, a11y); + const histogramContext = new VBox({ + align: "right", + spacing: PANEL_SPACING, + children: [statisticsPanel, distributionControlsPanel], + visibleProperty: isHistogramProperty, + }); + + const countRateDisplay = new CountRateDisplayNode(acquisition); + const dataTable = new DataTableNode(acquisition); + const countRateContext = new VBox({ + align: "right", + spacing: PANEL_SPACING, + children: [countRateDisplay, dataTable], + visibleProperty: isCountRateProperty, + }); + + const rightColumn = new VBox({ + align: "right", + spacing: PANEL_SPACING, + children: [chartViewPanel, new Node({ children: [histogramContext, countRateContext] })], + right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, + top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, + }); + + // ── Centre: whichever chart is chosen ────────────────────────────────────── + // The chart occupies whatever the two columns leave. An AlignBox centres it + // in that gap and keeps it centred: the chart's bounds change as the axes + // rescale to incoming data, and a one-off centerX would drift out of place. + // maxWidth is the backstop, capping it at the gap it has been given. + const availableBounds = new Bounds2( + leftColumn.right + CENTRE_COLUMN_PADDING, + this.layoutBounds.minY + SCREEN_VIEW_MARGIN, + rightColumn.left - CENTRE_COLUMN_PADDING, + this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, + ); + + const histogram = new HistogramNode(acquisition, model); + histogram.maxWidth = availableBounds.width; + const histogramContainer = new AlignBox(histogram, { + alignBounds: availableBounds, + xAlign: "center", + yAlign: "top", + visibleProperty: isHistogramProperty, + }); + + const rateChart = new CountRateChartNode(acquisition); + rateChart.maxWidth = availableBounds.width; + const rateChartContainer = new AlignBox(rateChart, { + alignBounds: availableBounds, + xAlign: "center", + yAlign: "top", + visibleProperty: isCountRateProperty, + }); + + this.addChild(leftColumn); + this.addChild(new Node({ children: [histogramContainer, rateChartContainer] })); + this.addChild(rightColumn); + + const resetAllButton = new ResetAllButton({ + ...FLAT_RESET_ALL_BUTTON_OPTIONS, + listener: () => { + model.reset(); + this.reset(); + }, + right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, + bottom: this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, + }); + this.addChild(resetAllButton); + + // ── Keyboard / reading order ────────────────────────────────────────────── + // Controls before readouts: a keyboard user wants to reach the things they + // can operate first, and the summary already describes the current state. + this.addChild( + new Node({ + pdomOrder: [ + sourcePanel, + acquisitionPanel, + chartViewPanel, + distributionControlsPanel, + dataTable, + resetAllButton, + ], + }), + ); + } + + /** Resets view-side state. The charts and tables follow the model, so none. */ + public reset(): void { + // Nothing view-only to reset — every visible node derives from the model. + } +} diff --git a/src/common/view/SourcePanel.ts b/src/common/view/SourcePanel.ts index 891e8fd..dc970f4 100644 --- a/src/common/view/SourcePanel.ts +++ b/src/common/view/SourcePanel.ts @@ -1,27 +1,29 @@ /** * SourcePanel.ts * - * Chooses where counts come from, and manages the Bluetooth connection when - * that choice is a real Geiger counter. + * Shows the controls for the one counting source a screen is fixed to — the + * simulated activity slider, or the Bluetooth connection controls — and + * manages the Bluetooth connection when that source is a real Geiger counter. * - * Only the controls belonging to the selected source are shown, so the panel - * never presents a Connect button next to a simulated activity slider. The - * connection status is a coloured dot *and* a text label — the colour alone - * never carries the meaning. + * Each screen now has a fixed source (there is nothing to choose between), so + * this panel only ever builds the controls for the {@link CountSourceType} it + * is given, rather than both sets with one hidden. The connection status is a + * coloured dot *and* a text label — the colour alone never carries the + * meaning. */ import { DerivedProperty, type TReadOnlyProperty } from "scenerystack/axon"; import { toFixed } from "scenerystack/dot"; -import { Circle, HBox, Node, Text, VBox } from "scenerystack/scenery"; +import { Circle, HBox, type Node, Text, VBox } from "scenerystack/scenery"; import { NumberControl, PhetFont } from "scenerystack/scenery-phet"; -import { AquaRadioButtonGroup, RectangularPushButton } from "scenerystack/sun"; -import type { SharedControlA11yStrings } from "../../i18n/StringManager.js"; +import { RectangularPushButton } from "scenerystack/sun"; +import type { ScreenControlA11yStrings } from "../../i18n/StringManager.js"; import { StringManager } from "../../i18n/StringManager.js"; import RadioactivityAndStatisticsColors from "../../RadioactivityAndStatisticsColors.js"; import { CONTROL_PANEL_WIDTH } from "../../RadioactivityAndStatisticsConstants.js"; import { getWebBluetoothStatus, WebBluetoothStatus } from "../hardware/webBluetoothSupport.js"; import { ConnectionState } from "../model/ConnectionState.js"; -import { CountSourceType } from "../model/CountSource.js"; +import { CountSourceType, type CountSourceTypeValue } from "../model/CountSource.js"; import type { RadioactivityModel } from "../model/RadioactivityModel.js"; import { FLAT_PANEL_PUSH_BUTTON_OPTIONS, LIGHT_SURFACE_TEXT_FILL } from "../RadioactivityAndStatisticsButtonOptions.js"; import { RadioactivityAndStatisticsPanel } from "../RadioactivityAndStatisticsPanel.js"; @@ -35,7 +37,8 @@ export class SourcePanel extends RadioactivityAndStatisticsPanel { public constructor( model: RadioactivityModel, - a11y: SharedControlA11yStrings, + fixedSourceType: CountSourceTypeValue, + a11y: ScreenControlA11yStrings, showDiagnosticsProperty: TReadOnlyProperty, ) { const stringManager = StringManager.getInstance(); @@ -47,251 +50,229 @@ export class SourcePanel extends RadioactivityAndStatisticsPanel { fill: RadioactivityAndStatisticsColors.textColorProperty, }); - // ── Source selection ────────────────────────────────────────────────────── - const sourceRadioGroup = new AquaRadioButtonGroup( - model.sourceTypeProperty, - [ - { - value: CountSourceType.SIMULATED, - createNode: () => - new Text(strings.simulatedStringProperty, { - font: new PhetFont(13), - fill: RadioactivityAndStatisticsColors.textColorProperty, - maxWidth: 170, - }), - }, - { - value: CountSourceType.GEIGER_COUNTER, - createNode: () => - new Text(strings.geigerCounterStringProperty, { - font: new PhetFont(13), - fill: RadioactivityAndStatisticsColors.textColorProperty, - maxWidth: 170, - }), - }, - ], - { - spacing: 6, - radioButtonOptions: { radius: 7 }, - accessibleName: a11y.sourceRadioGroupStringProperty, - }, - ); + const sourceControls: Node = + fixedSourceType === CountSourceType.SIMULATED + ? createSimulatedControls(model, strings, a11y) + : createGeigerControls(model, strings, a11y, showDiagnosticsProperty, disposables); - // ── Simulated-source controls ───────────────────────────────────────────── - const activityControl = new NumberControl( - strings.activityStringProperty, - model.simulatedSource.activityProperty, - model.activityRange, - { - ...SIM_NUMBER_CONTROL_OPTIONS, - delta: 1, - titleNodeOptions: { - font: new PhetFont(13), - fill: RadioactivityAndStatisticsColors.textColorProperty, - maxWidth: CONTROL_PANEL_WIDTH - 40, - }, - numberDisplayOptions: { - valuePattern: "{{value}} /s", - textOptions: { font: new PhetFont(13) }, - }, - accessibleName: a11y.activitySliderStringProperty, - }, + super( + new VBox({ + align: "left", + spacing: 8, + preferredWidth: CONTROL_PANEL_WIDTH - 24, + stretch: true, + children: [title, sourceControls], + }), + { minWidth: CONTROL_PANEL_WIDTH }, ); - const simulatedControls = new VBox({ - align: "left", - spacing: 6, - children: [activityControl], - }); + this.disposeSourcePanel = () => { + for (const disposable of disposables) { + disposable.dispose(); + } + }; + } - // ── Hardware-source controls ────────────────────────────────────────────── - const geigerSource = model.geigerSource; + public override dispose(): void { + this.disposeSourcePanel(); + super.dispose(); + } +} - const statusTextProperty = new DerivedProperty( - [ - geigerSource.connectionStateProperty, - strings.statusDisconnectedStringProperty, - strings.statusConnectingStringProperty, - strings.statusConnectedStringProperty, - strings.statusErrorStringProperty, - ], - (state, disconnected, connecting, connected, errored) => { - if (state === ConnectionState.CONNECTING) { - return connecting; - } - if (state === ConnectionState.CONNECTED) { - return connected; - } - if (state === ConnectionState.ERROR) { - return errored; - } - return disconnected; +/** Just the simulated activity slider. */ +function createSimulatedControls( + model: RadioactivityModel, + strings: ReturnType, + a11y: ScreenControlA11yStrings, +): Node { + const activityControl = new NumberControl( + strings.activityStringProperty, + model.simulatedSource.activityProperty, + model.activityRange, + { + ...SIM_NUMBER_CONTROL_OPTIONS, + delta: 1, + titleNodeOptions: { + font: new PhetFont(13), + fill: RadioactivityAndStatisticsColors.textColorProperty, + maxWidth: CONTROL_PANEL_WIDTH - 40, }, - ); + numberDisplayOptions: { + valuePattern: "{{value}} /s", + textOptions: { font: new PhetFont(13) }, + }, + accessibleName: a11y.activitySliderStringProperty, + }, + ); - const statusColorProperty = new DerivedProperty([geigerSource.connectionStateProperty], (state) => { - if (state === ConnectionState.CONNECTED) { - return RadioactivityAndStatisticsColors.statusGoodColorProperty.value; - } + return new VBox({ + align: "left", + spacing: 6, + children: [activityControl], + }); +} + +/** Connection status, connect/disconnect buttons, and diagnostics for a real Geiger counter. */ +function createGeigerControls( + model: RadioactivityModel, + strings: ReturnType, + a11y: ScreenControlA11yStrings, + showDiagnosticsProperty: TReadOnlyProperty, + disposables: { dispose: () => void }[], +): Node { + const geigerSource = model.geigerSource; + + const statusTextProperty = new DerivedProperty( + [ + geigerSource.connectionStateProperty, + strings.statusDisconnectedStringProperty, + strings.statusConnectingStringProperty, + strings.statusConnectedStringProperty, + strings.statusErrorStringProperty, + ], + (state, disconnected, connecting, connected, errored) => { if (state === ConnectionState.CONNECTING) { - return RadioactivityAndStatisticsColors.statusWarningColorProperty.value; + return connecting; + } + if (state === ConnectionState.CONNECTED) { + return connected; } if (state === ConnectionState.ERROR) { - return RadioactivityAndStatisticsColors.statusCriticalColorProperty.value; + return errored; } - return RadioactivityAndStatisticsColors.statusIdleColorProperty.value; - }); - - const statusRow = new HBox({ - spacing: 6, - children: [ - new Circle(STATUS_DOT_RADIUS, { fill: statusColorProperty }), - new Text(statusTextProperty, { - font: new PhetFont(13), - fill: RadioactivityAndStatisticsColors.textColorProperty, - maxWidth: CONTROL_PANEL_WIDTH - 60, - }), - ], - }); - - // The device's own name is the only way to tell two counters apart on a - // bench with several of them running. - const deviceNameProperty = new DerivedProperty( - [geigerSource.deviceInfoProperty], - (info) => info?.advertisedName ?? "", - ); - const deviceNameText = new Text(deviceNameProperty, { - font: new PhetFont(11), - fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, - maxWidth: CONTROL_PANEL_WIDTH - 30, - visibleProperty: new DerivedProperty([deviceNameProperty], (name) => name.length > 0), - }); + return disconnected; + }, + ); - const isConnectedProperty = new DerivedProperty( - [geigerSource.connectionStateProperty], - (state) => state === ConnectionState.CONNECTED, - ); + const statusColorProperty = new DerivedProperty([geigerSource.connectionStateProperty], (state) => { + if (state === ConnectionState.CONNECTED) { + return RadioactivityAndStatisticsColors.statusGoodColorProperty.value; + } + if (state === ConnectionState.CONNECTING) { + return RadioactivityAndStatisticsColors.statusWarningColorProperty.value; + } + if (state === ConnectionState.ERROR) { + return RadioactivityAndStatisticsColors.statusCriticalColorProperty.value; + } + return RadioactivityAndStatisticsColors.statusIdleColorProperty.value; + }); - const connectButton = new RectangularPushButton({ - ...FLAT_PANEL_PUSH_BUTTON_OPTIONS, - content: new Text(strings.connectStringProperty, { font: new PhetFont(13), fill: LIGHT_SURFACE_TEXT_FILL }), - // Web Bluetooth only opens its picker during a user gesture, so this must - // reach requestDevice without an intervening await — it does: connect() - // runs synchronously up to that call. - listener: () => { - geigerSource.connect().catch(() => undefined); - }, - accessibleName: a11y.connectButtonStringProperty, - visibleProperty: new DerivedProperty([isConnectedProperty], (connected) => !connected), - }); - - const disconnectButton = new RectangularPushButton({ - ...FLAT_PANEL_PUSH_BUTTON_OPTIONS, - content: new Text(strings.disconnectStringProperty, { font: new PhetFont(13), fill: LIGHT_SURFACE_TEXT_FILL }), - listener: () => { - geigerSource.disconnect().catch(() => undefined); - }, - accessibleName: a11y.disconnectButtonStringProperty, - visibleProperty: isConnectedProperty, - }); + const statusRow = new HBox({ + spacing: 6, + children: [ + new Circle(STATUS_DOT_RADIUS, { fill: statusColorProperty }), + new Text(statusTextProperty, { + font: new PhetFont(13), + fill: RadioactivityAndStatisticsColors.textColorProperty, + maxWidth: CONTROL_PANEL_WIDTH - 60, + }), + ], + }); - // Reasons a connection can never succeed here are worth stating up front, - // rather than after a click that silently does nothing. - const browserStatus = getWebBluetoothStatus(); - const unavailableMessage = - browserStatus === WebBluetoothStatus.INSECURE_CONTEXT - ? strings.insecureContextStringProperty - : strings.unsupportedBrowserStringProperty; - const unavailableText = new Text(unavailableMessage, { - font: new PhetFont(11), - fill: RadioactivityAndStatisticsColors.statusCriticalColorProperty, - maxWidth: CONTROL_PANEL_WIDTH - 30, - visible: browserStatus !== WebBluetoothStatus.AVAILABLE, - }); + // The device's own name is the only way to tell two counters apart on a + // bench with several of them running. + const deviceNameProperty = new DerivedProperty( + [geigerSource.deviceInfoProperty], + (info) => info?.advertisedName ?? "", + ); + const deviceNameText = new Text(deviceNameProperty, { + font: new PhetFont(11), + fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, + maxWidth: CONTROL_PANEL_WIDTH - 30, + visibleProperty: new DerivedProperty([deviceNameProperty], (name) => name.length > 0), + }); - const errorTextProperty = new DerivedProperty([geigerSource.errorMessageProperty], (message) => message ?? ""); - const errorText = new Text(errorTextProperty, { - font: new PhetFont(11), - fill: RadioactivityAndStatisticsColors.statusCriticalColorProperty, - maxWidth: CONTROL_PANEL_WIDTH - 30, - visibleProperty: new DerivedProperty([errorTextProperty], (message) => message.length > 0), - }); + const isConnectedProperty = new DerivedProperty( + [geigerSource.connectionStateProperty], + (state) => state === ConnectionState.CONNECTED, + ); - // ── Diagnostics ─────────────────────────────────────────────────────────── - // Off by default. A live look at the two registers, for confirming a - // counter is reporting sanely — a healthy tube sits near 500 V. - const registerValueProperty = new DerivedProperty( - [geigerSource.countRegisterProperty, strings.rawRegisterStringProperty], - (register, label) => `${label}: ${register}`, - ); - const tubeVoltageValueProperty = new DerivedProperty( - [geigerSource.tubeVoltageProperty, strings.tubeVoltageStringProperty], - (volts, label) => `${label}: ${toFixed(volts, 0)} V`, - ); + const connectButton = new RectangularPushButton({ + ...FLAT_PANEL_PUSH_BUTTON_OPTIONS, + content: new Text(strings.connectStringProperty, { font: new PhetFont(13), fill: LIGHT_SURFACE_TEXT_FILL }), + // Web Bluetooth only opens its picker during a user gesture, so this must + // reach requestDevice without an intervening await — it does: connect() + // runs synchronously up to that call. + listener: () => { + geigerSource.connect().catch(() => undefined); + }, + accessibleName: a11y.connectButtonStringProperty, + visibleProperty: new DerivedProperty([isConnectedProperty], (connected) => !connected), + }); - const diagnostics = new VBox({ - align: "left", - spacing: 4, - visibleProperty: showDiagnosticsProperty, - children: [ - new Text(registerValueProperty, { - font: new PhetFont(11), - fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, - }), - new Text(tubeVoltageValueProperty, { - font: new PhetFont(11), - fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, - }), - ], - }); + const disconnectButton = new RectangularPushButton({ + ...FLAT_PANEL_PUSH_BUTTON_OPTIONS, + content: new Text(strings.disconnectStringProperty, { font: new PhetFont(13), fill: LIGHT_SURFACE_TEXT_FILL }), + listener: () => { + geigerSource.disconnect().catch(() => undefined); + }, + accessibleName: a11y.disconnectButtonStringProperty, + visibleProperty: isConnectedProperty, + }); - const geigerControls = new VBox({ - align: "left", - spacing: 6, - children: [statusRow, deviceNameText, connectButton, disconnectButton, unavailableText, errorText, diagnostics], - }); + // Reasons a connection can never succeed here are worth stating up front, + // rather than after a click that silently does nothing. + const browserStatus = getWebBluetoothStatus(); + const unavailableMessage = + browserStatus === WebBluetoothStatus.INSECURE_CONTEXT + ? strings.insecureContextStringProperty + : strings.unsupportedBrowserStringProperty; + const unavailableText = new Text(unavailableMessage, { + font: new PhetFont(11), + fill: RadioactivityAndStatisticsColors.statusCriticalColorProperty, + maxWidth: CONTROL_PANEL_WIDTH - 30, + visible: browserStatus !== WebBluetoothStatus.AVAILABLE, + }); - // ── Assemble ────────────────────────────────────────────────────────────── - const isSimulatedProperty = new DerivedProperty( - [model.sourceTypeProperty], - (sourceType) => sourceType === CountSourceType.SIMULATED, - ); - simulatedControls.visibleProperty = isSimulatedProperty; - geigerControls.visibleProperty = new DerivedProperty([isSimulatedProperty], (isSimulated) => !isSimulated); + const errorTextProperty = new DerivedProperty([geigerSource.errorMessageProperty], (message) => message ?? ""); + const errorText = new Text(errorTextProperty, { + font: new PhetFont(11), + fill: RadioactivityAndStatisticsColors.statusCriticalColorProperty, + maxWidth: CONTROL_PANEL_WIDTH - 30, + visibleProperty: new DerivedProperty([errorTextProperty], (message) => message.length > 0), + }); - disposables.push( - statusTextProperty, - statusColorProperty, - deviceNameProperty, - isConnectedProperty, - errorTextProperty, - registerValueProperty, - tubeVoltageValueProperty, - isSimulatedProperty, - ); + // ── Diagnostics ───────────────────────────────────────────────────────────── + // Off by default. A live look at the two registers, for confirming a counter + // is reporting sanely — a healthy tube sits near 500 V. + const registerValueProperty = new DerivedProperty( + [geigerSource.countRegisterProperty, strings.rawRegisterStringProperty], + (register, label) => `${label}: ${register}`, + ); + const tubeVoltageValueProperty = new DerivedProperty( + [geigerSource.tubeVoltageProperty, strings.tubeVoltageStringProperty], + (volts, label) => `${label}: ${toFixed(volts, 0)} V`, + ); - super( - new VBox({ - align: "left", - spacing: 8, - // A fixed width keeps the control column from reflowing as the two - // source sub-panels swap in and out. - preferredWidth: CONTROL_PANEL_WIDTH - 24, - stretch: true, - children: [title, sourceRadioGroup, new Node({ children: [simulatedControls, geigerControls] })], + const diagnostics = new VBox({ + align: "left", + spacing: 4, + visibleProperty: showDiagnosticsProperty, + children: [ + new Text(registerValueProperty, { + font: new PhetFont(11), + fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, }), - { minWidth: CONTROL_PANEL_WIDTH }, - ); + new Text(tubeVoltageValueProperty, { + font: new PhetFont(11), + fill: RadioactivityAndStatisticsColors.secondaryTextColorProperty, + }), + ], + }); - this.disposeSourcePanel = () => { - for (const disposable of disposables) { - disposable.dispose(); - } - }; - } + disposables.push( + statusTextProperty, + statusColorProperty, + deviceNameProperty, + isConnectedProperty, + errorTextProperty, + registerValueProperty, + tubeVoltageValueProperty, + ); - public override dispose(): void { - this.disposeSourcePanel(); - super.dispose(); - } + return new VBox({ + align: "left", + spacing: 6, + children: [statusRow, deviceNameText, connectButton, disconnectButton, unavailableText, errorText, diagnostics], + }); } diff --git a/src/device/DeviceScreen.ts b/src/device/DeviceScreen.ts new file mode 100644 index 0000000..0f1aa81 --- /dev/null +++ b/src/device/DeviceScreen.ts @@ -0,0 +1,50 @@ +/** + * DeviceScreen.ts + * + * Wires the Device model and view together and supplies screen-level options. + * Registered in the screens array in src/main.ts. + */ +import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; +import type { ScreenOptions } from "scenerystack/sim"; +import { Screen } from "scenerystack/sim"; +import type { Tandem } from "scenerystack/tandem"; +import { CountSourceType } from "../common/model/CountSource.js"; +import { createDeviceIcon } from "../common/RadioactivityAndStatisticsScreenIcons.js"; +import { RadioactivityKeyboardHelpContent } from "../common/view/RadioactivityKeyboardHelpContent.js"; +import { RadioactivityScreenView } from "../common/view/RadioactivityScreenView.js"; +import { StringManager } from "../i18n/StringManager.js"; +import type { RadioactivityAndStatisticsPreferencesModel } from "../preferences/RadioactivityAndStatisticsPreferencesModel.js"; +import RadioactivityAndStatisticsColors from "../RadioactivityAndStatisticsColors.js"; +import { DeviceModel } from "./model/DeviceModel.js"; +import { DeviceScreenSummaryContent } from "./view/DeviceScreenSummaryContent.js"; + +// Require tandem to be explicit — accidental omission would break PhET-iO. +type DeviceScreenOptions = ScreenOptions & { tandem: Tandem }; + +export class DeviceScreen extends Screen { + public constructor(preferences: RadioactivityAndStatisticsPreferencesModel, options: DeviceScreenOptions) { + super( + () => new DeviceModel(preferences), + (model) => + new RadioactivityScreenView( + model, + CountSourceType.GEIGER_COUNTER, + preferences.showDiagnosticsProperty, + StringManager.getInstance().getDeviceA11yStrings().controls, + { + screenSummaryContent: new DeviceScreenSummaryContent(model), + tandem: options.tandem.createTandem("view"), + }, + ), + optionize()( + { + backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, + createKeyboardHelpNode: () => new RadioactivityKeyboardHelpContent(), + homeScreenIcon: createDeviceIcon(), + navigationBarIcon: createDeviceIcon(), + }, + options, + ), + ); + } +} diff --git a/src/device/model/DeviceModel.ts b/src/device/model/DeviceModel.ts new file mode 100644 index 0000000..9009f46 --- /dev/null +++ b/src/device/model/DeviceModel.ts @@ -0,0 +1,23 @@ +/** + * DeviceModel.ts + * + * Model for the Device screen: a real PASCO Wireless Geiger Counter connected + * over Bluetooth, that can be viewed as either the histogram or the + * count-rate chart. + */ + +import { ChartViewType } from "../../common/model/ChartViewType.js"; +import { CountSourceType } from "../../common/model/CountSource.js"; +import { RadioactivityScreenModel } from "../../common/model/RadioactivityScreenModel.js"; +import type { RadioactivityAndStatisticsPreferencesModel } from "../../preferences/RadioactivityAndStatisticsPreferencesModel.js"; + +export class DeviceModel extends RadioactivityScreenModel { + public constructor(preferences: RadioactivityAndStatisticsPreferencesModel) { + super(preferences, { + fixedSourceType: CountSourceType.GEIGER_COUNTER, + // Opens on the distribution, the comparison a real source is collected + // for, once it is connected and a run exists to compare. + initialChartView: ChartViewType.HISTOGRAM, + }); + } +} diff --git a/src/device/view/DeviceScreenSummaryContent.ts b/src/device/view/DeviceScreenSummaryContent.ts new file mode 100644 index 0000000..7fe100c --- /dev/null +++ b/src/device/view/DeviceScreenSummaryContent.ts @@ -0,0 +1,38 @@ +/** + * DeviceScreenSummaryContent.ts + * + * The accessible screen summary for the Device screen. + * + * Shares the live current-details paragraph with the Simulation screen — the + * state being described (how much data, what it says) is the same on both + * screens; only the source and the chart-neutral phrasing of the control area + * differ. + */ + +import { ScreenSummaryContent } from "scenerystack/sim"; +import { createCurrentDetailsProperty } from "../../common/view/currentDetailsProperty.js"; +import { StringManager } from "../../i18n/StringManager.js"; +import type { DeviceModel } from "../model/DeviceModel.js"; + +export class DeviceScreenSummaryContent extends ScreenSummaryContent { + private readonly disposeDeviceScreenSummaryContent: () => void; + + public constructor(model: DeviceModel) { + const a11y = StringManager.getInstance().getDeviceA11yStrings(); + const currentDetails = createCurrentDetailsProperty(model.acquisition, a11y); + + super({ + playAreaContent: a11y.screenSummary.playAreaStringProperty, + controlAreaContent: a11y.screenSummary.controlAreaStringProperty, + currentDetailsContent: currentDetails.property, + interactionHintContent: a11y.screenSummary.interactionHintStringProperty, + }); + + this.disposeDeviceScreenSummaryContent = currentDetails.dispose; + } + + public override dispose(): void { + this.disposeDeviceScreenSummaryContent(); + super.dispose(); + } +} diff --git a/src/i18n/StringManager.ts b/src/i18n/StringManager.ts index 59d5ee7..c5e976a 100644 --- a/src/i18n/StringManager.ts +++ b/src/i18n/StringManager.ts @@ -46,16 +46,14 @@ const stringProperties = LocalizedString.getNestedStringProperties({ }); /** - * Explicit `a11y` shape exposed by {@link StringManager.getIntroA11yStrings} and - * {@link StringManager.getLabA11yStrings}. Keep this in sync with the `a11y` - * key in `strings_en.json` — a locale key rename that is not mirrored here - * fails at the getter return (not silently). - * - * The Lab screen adds curve and binning controls on top of the shared set, so - * its `controls` group is a superset of the Intro screen's. + * Every control name used across the two screens. Both screens now carry the + * same panels — only the fixed counting source and the default chart differ + * between them — so one shape covers both `a11y.simulation.controls` and + * `a11y.device.controls`; a screen simply never wires up the accessible name + * that belongs to the other one's source (e.g. the Bluetooth screen never + * builds `activitySlider`). */ -export type SharedControlA11yStrings = { - readonly sourceRadioGroupStringProperty: ReadOnlyProperty; +export type ScreenControlA11yStrings = { readonly activitySliderStringProperty: ReadOnlyProperty; readonly connectButtonStringProperty: ReadOnlyProperty; readonly disconnectButtonStringProperty: ReadOnlyProperty; @@ -66,13 +64,7 @@ export type SharedControlA11yStrings = { readonly stopButtonStringProperty: ReadOnlyProperty; readonly clearButtonStringProperty: ReadOnlyProperty; readonly exportButtonStringProperty: ReadOnlyProperty; -}; - -/** - * The Lab screen's control names: the shared set plus the curve-visibility and - * binning controls that only it has. - */ -export type LabControlA11yStrings = SharedControlA11yStrings & { + readonly chartViewRadioGroupStringProperty: ReadOnlyProperty; readonly poissonCheckboxStringProperty: ReadOnlyProperty; readonly gaussianPredictionCheckboxStringProperty: ReadOnlyProperty; readonly gaussianFitCheckboxStringProperty: ReadOnlyProperty; @@ -87,7 +79,7 @@ export type SimA11yStrings = { readonly interactionHintStringProperty: ReadOnlyProperty; }; readonly currentDetailsStringProperty: ReadOnlyProperty; - readonly controls: SharedControlA11yStrings; + readonly controls: ScreenControlA11yStrings; }; /** @@ -136,26 +128,26 @@ export class StringManager { * Each property updates automatically when the locale changes. */ public getScreenNames(): { - readonly introStringProperty: ReadOnlyProperty; - readonly labStringProperty: ReadOnlyProperty; + readonly simulationStringProperty: ReadOnlyProperty; + readonly deviceStringProperty: ReadOnlyProperty; } { return { - introStringProperty: stringProperties.screens.introStringProperty, - labStringProperty: stringProperties.screens.labStringProperty, + simulationStringProperty: stringProperties.screens.simulationStringProperty, + deviceStringProperty: stringProperties.screens.deviceStringProperty, }; } - /** Accessibility strings for the Intro screen. */ - public getIntroA11yStrings() { - return stringProperties.a11y.intro; + /** Accessibility strings for the Simulation screen (the mock-up counter). */ + public getSimulationA11yStrings() { + return stringProperties.a11y.simulation; } - /** Accessibility strings for the Lab screen. */ - public getLabA11yStrings() { - return stringProperties.a11y.lab; + /** Accessibility strings for the Device screen (the Bluetooth Geiger counter). */ + public getDeviceA11yStrings() { + return stringProperties.a11y.device; } - /** Labels for the source panel: source choice, connection, diagnostics. */ + /** Labels for the source panel: connection and diagnostics. */ public getSourceStrings() { return stringProperties.source; } @@ -185,11 +177,16 @@ export class StringManager { return stringProperties.histogram; } - /** Axis titles for the Intro screen's count-rate chart. */ + /** Axis titles for the count-rate chart. */ public getRateChartStrings() { return stringProperties.rateChart; } + /** Labels for the histogram / count-rate view switch. */ + public getChartViewStrings() { + return stringProperties.chartView; + } + /** * Simulation-specific preference labels shown in Preferences → Simulation. */ diff --git a/src/i18n/strings_en.json b/src/i18n/strings_en.json index 20b97d4..38170ac 100644 --- a/src/i18n/strings_en.json +++ b/src/i18n/strings_en.json @@ -1,13 +1,11 @@ { "title": "Radioactivity and Statistics", "screens": { - "intro": "Intro", - "lab": "Lab" + "simulation": "Simulation", + "device": "Bluetooth Device" }, "source": { "title": "Source", - "simulated": "Simulated", - "geigerCounter": "Geiger counter", "activity": "Activity", "activityUnits": "counts/s", "connect": "Connect", @@ -74,18 +72,22 @@ "axisTime": "Time (s)", "axisRate": "Counts/s" }, + "chartView": { + "title": "View", + "histogram": "Histogram", + "countRate": "Count rate over time" + }, "a11y": { - "intro": { + "simulation": { "screenSummary": { - "playArea": "The play area shows a live count-rate readout, a chart of the count rate over time, and a table of the measurements collected so far.", - "controlArea": "The control area lets you choose a counting source, set the counting interval and run length, start and stop recording, export the data, and reset the simulation.", - "interactionHint": "Press Record to collect a run of measurements, then read the count rate and the table." + "playArea": "The play area shows a histogram or a count-rate chart of a simulated counting source, alongside a panel of summary statistics.", + "controlArea": "The control area lets you set the simulated source's activity, choose which chart is shown, set the counting interval and run length, start and stop recording, choose which theoretical curves to show, set the histogram bin width, export the data, and reset the simulation.", + "interactionHint": "Collect a run of measurements, then switch between the histogram and the count-rate chart to see what the data shows." }, "currentDetails": "No measurements have been collected yet.", "currentDetailsRecording": "Recording. {{count}} measurements collected so far. Latest count rate {{rate}} counts per second.", "currentDetailsCollected": "{{count}} measurements collected. Mean {{mean}} counts per interval, standard deviation {{deviation}}, and the square root of the mean is {{poisson}}.", "controls": { - "sourceRadioGroup": "Counting source", "activitySlider": "Simulated activity in counts per second", "connectButton": "Connect to a Geiger counter over Bluetooth", "disconnectButton": "Disconnect the Geiger counter", @@ -95,20 +97,25 @@ "recordButton": "Start recording measurements", "stopButton": "Stop recording measurements", "clearButton": "Clear the collected measurements", - "exportButton": "Export the collected measurements as a CSV file" + "exportButton": "Export the collected measurements as a CSV file", + "chartViewRadioGroup": "Choose whether to show the histogram or the count rate over time", + "poissonCheckbox": "Show the Poisson prediction", + "gaussianPredictionCheckbox": "Show the Gaussian prediction with sigma equal to the square root of the mean", + "gaussianFitCheckbox": "Show the best-fit Gaussian", + "autoBinWidthCheckbox": "Choose the histogram bin width automatically", + "binWidthControl": "Histogram bin width in counts" } }, - "lab": { + "device": { "screenSummary": { - "playArea": "The play area shows a histogram of the collected measurements, with optional Poisson and Gaussian curves drawn on top, alongside a panel of summary statistics.", - "controlArea": "The control area lets you choose a counting source, set the counting interval and run length, start and stop recording, choose which theoretical curves to show, set the histogram bin width, export the data, and reset the simulation.", - "interactionHint": "Collect a run of measurements, then compare the histogram against the Poisson and Gaussian curves." + "playArea": "The play area shows a histogram or a count-rate chart of a real Geiger counter connected over Bluetooth, alongside a panel of summary statistics.", + "controlArea": "The control area lets you connect or disconnect a Bluetooth Geiger counter, choose which chart is shown, set the counting interval and run length, start and stop recording, choose which theoretical curves to show, set the histogram bin width, export the data, and reset the simulation.", + "interactionHint": "Connect a Geiger counter, collect a run of measurements, then switch between the histogram and the count-rate chart to see what the data shows." }, "currentDetails": "No measurements have been collected yet.", "currentDetailsRecording": "Recording. {{count}} measurements collected so far. Latest count rate {{rate}} counts per second.", "currentDetailsCollected": "{{count}} measurements collected. Mean {{mean}} counts per interval, standard deviation {{deviation}}, and the square root of the mean is {{poisson}}.", "controls": { - "sourceRadioGroup": "Counting source", "activitySlider": "Simulated activity in counts per second", "connectButton": "Connect to a Geiger counter over Bluetooth", "disconnectButton": "Disconnect the Geiger counter", @@ -119,6 +126,7 @@ "stopButton": "Stop recording measurements", "clearButton": "Clear the collected measurements", "exportButton": "Export the collected measurements as a CSV file", + "chartViewRadioGroup": "Choose whether to show the histogram or the count rate over time", "poissonCheckbox": "Show the Poisson prediction", "gaussianPredictionCheckbox": "Show the Gaussian prediction with sigma equal to the square root of the mean", "gaussianFitCheckbox": "Show the best-fit Gaussian", diff --git a/src/i18n/strings_es.json b/src/i18n/strings_es.json index 850f871..a28f3a8 100644 --- a/src/i18n/strings_es.json +++ b/src/i18n/strings_es.json @@ -1,13 +1,11 @@ { "title": "Radiactividad y estadísticas", "screens": { - "intro": "Intro", - "lab": "Laboratorio" + "simulation": "Simulación", + "device": "Dispositivo Bluetooth" }, "source": { "title": "Fuente", - "simulated": "Simulada", - "geigerCounter": "Contador Geiger", "activity": "Actividad", "activityUnits": "cuentas/s", "connect": "Conectar", @@ -74,18 +72,22 @@ "axisTime": "Tiempo (s)", "axisRate": "Cuentas/s" }, + "chartView": { + "title": "Vista", + "histogram": "Histograma", + "countRate": "Tasa de conteo en el tiempo" + }, "a11y": { - "intro": { + "simulation": { "screenSummary": { - "playArea": "El área de juego muestra una lectura en vivo de la tasa de conteo, un gráfico de la tasa de conteo en el tiempo y una tabla de las mediciones recogidas.", - "controlArea": "El área de control permite elegir una fuente de conteo, fijar el intervalo de conteo y la longitud de la serie, iniciar y detener la grabación, exportar los datos y reiniciar la simulación.", - "interactionHint": "Pulse Grabar para recoger una serie de mediciones y luego lea la tasa de conteo y la tabla." + "playArea": "El área de juego muestra un histograma o un gráfico de la tasa de conteo de una fuente de conteo simulada, junto a un panel de estadísticas.", + "controlArea": "El área de control permite ajustar la actividad de la fuente simulada, elegir qué gráfico se muestra, fijar el intervalo de conteo y la longitud de la serie, iniciar y detener la grabación, elegir qué curvas teóricas mostrar, fijar el ancho de clase, exportar los datos y reiniciar la simulación.", + "interactionHint": "Recoja una serie de mediciones y luego alterne entre el histograma y el gráfico de la tasa de conteo para ver lo que muestran los datos." }, "currentDetails": "Todavía no se ha recogido ninguna medición.", "currentDetailsRecording": "Grabando. {{count}} mediciones recogidas hasta ahora. Última tasa de conteo: {{rate}} cuentas por segundo.", "currentDetailsCollected": "{{count}} mediciones recogidas. Media de {{mean}} cuentas por intervalo, desviación estándar de {{deviation}}, y la raíz cuadrada de la media es {{poisson}}.", "controls": { - "sourceRadioGroup": "Fuente de conteo", "activitySlider": "Actividad simulada en cuentas por segundo", "connectButton": "Conectar un contador Geiger por Bluetooth", "disconnectButton": "Desconectar el contador Geiger", @@ -95,20 +97,25 @@ "recordButton": "Empezar a grabar mediciones", "stopButton": "Dejar de grabar mediciones", "clearButton": "Borrar las mediciones recogidas", - "exportButton": "Exportar las mediciones recogidas como archivo CSV" + "exportButton": "Exportar las mediciones recogidas como archivo CSV", + "chartViewRadioGroup": "Elegir si se muestra el histograma o la tasa de conteo en el tiempo", + "poissonCheckbox": "Mostrar la predicción de Poisson", + "gaussianPredictionCheckbox": "Mostrar la predicción gaussiana con sigma igual a la raíz cuadrada de la media", + "gaussianFitCheckbox": "Mostrar la gaussiana de mejor ajuste", + "autoBinWidthCheckbox": "Elegir automáticamente el ancho de clase del histograma", + "binWidthControl": "Ancho de clase del histograma en cuentas" } }, - "lab": { + "device": { "screenSummary": { - "playArea": "El área de juego muestra un histograma de las mediciones recogidas, con curvas de Poisson y gaussianas opcionales superpuestas, junto a un panel de estadísticas.", - "controlArea": "El área de control permite elegir una fuente de conteo, fijar el intervalo de conteo y la longitud de la serie, iniciar y detener la grabación, elegir qué curvas teóricas mostrar, fijar el ancho de clase, exportar los datos y reiniciar la simulación.", - "interactionHint": "Recoja una serie de mediciones y luego compare el histograma con las curvas de Poisson y gaussianas." + "playArea": "El área de juego muestra un histograma o un gráfico de la tasa de conteo de un contador Geiger real conectado por Bluetooth, junto a un panel de estadísticas.", + "controlArea": "El área de control permite conectar o desconectar un contador Geiger Bluetooth, elegir qué gráfico se muestra, fijar el intervalo de conteo y la longitud de la serie, iniciar y detener la grabación, elegir qué curvas teóricas mostrar, fijar el ancho de clase, exportar los datos y reiniciar la simulación.", + "interactionHint": "Conecte un contador Geiger, recoja una serie de mediciones y luego alterne entre el histograma y el gráfico de la tasa de conteo para ver lo que muestran los datos." }, "currentDetails": "Todavía no se ha recogido ninguna medición.", "currentDetailsRecording": "Grabando. {{count}} mediciones recogidas hasta ahora. Última tasa de conteo: {{rate}} cuentas por segundo.", "currentDetailsCollected": "{{count}} mediciones recogidas. Media de {{mean}} cuentas por intervalo, desviación estándar de {{deviation}}, y la raíz cuadrada de la media es {{poisson}}.", "controls": { - "sourceRadioGroup": "Fuente de conteo", "activitySlider": "Actividad simulada en cuentas por segundo", "connectButton": "Conectar un contador Geiger por Bluetooth", "disconnectButton": "Desconectar el contador Geiger", @@ -119,6 +126,7 @@ "stopButton": "Dejar de grabar mediciones", "clearButton": "Borrar las mediciones recogidas", "exportButton": "Exportar las mediciones recogidas como archivo CSV", + "chartViewRadioGroup": "Elegir si se muestra el histograma o la tasa de conteo en el tiempo", "poissonCheckbox": "Mostrar la predicción de Poisson", "gaussianPredictionCheckbox": "Mostrar la predicción gaussiana con sigma igual a la raíz cuadrada de la media", "gaussianFitCheckbox": "Mostrar la gaussiana de mejor ajuste", diff --git a/src/i18n/strings_fr.json b/src/i18n/strings_fr.json index 1f19e8a..0fc3f9a 100644 --- a/src/i18n/strings_fr.json +++ b/src/i18n/strings_fr.json @@ -1,13 +1,11 @@ { "title": "Radioactivité et statistiques", "screens": { - "intro": "Intro", - "lab": "Labo" + "simulation": "Simulation", + "device": "Appareil Bluetooth" }, "source": { "title": "Source", - "simulated": "Simulée", - "geigerCounter": "Compteur Geiger", "activity": "Activité", "activityUnits": "coups/s", "connect": "Connecter", @@ -74,18 +72,22 @@ "axisTime": "Temps (s)", "axisRate": "Coups/s" }, + "chartView": { + "title": "Affichage", + "histogram": "Histogramme", + "countRate": "Taux de comptage dans le temps" + }, "a11y": { - "intro": { + "simulation": { "screenSummary": { - "playArea": "La zone de jeu affiche un taux de comptage en direct, un graphique du taux de comptage dans le temps et un tableau des mesures recueillies.", - "controlArea": "La zone de contrôle permet de choisir une source de comptage, de régler l'intervalle de comptage et la longueur de la série, de démarrer et d'arrêter l'enregistrement, d'exporter les données et de réinitialiser la simulation.", - "interactionHint": "Appuyez sur Enregistrer pour recueillir une série de mesures, puis lisez le taux de comptage et le tableau." + "playArea": "La zone de jeu affiche un histogramme ou un graphique du taux de comptage d'une source de comptage simulée, à côté d'un panneau de statistiques.", + "controlArea": "La zone de contrôle permet de régler l'activité de la source simulée, de choisir le graphique affiché, de régler l'intervalle de comptage et la longueur de la série, de démarrer et d'arrêter l'enregistrement, de choisir les courbes théoriques affichées, de régler la largeur de classe, d'exporter les données et de réinitialiser la simulation.", + "interactionHint": "Recueillez une série de mesures, puis basculez entre l'histogramme et le graphique du taux de comptage pour voir ce que les données montrent." }, "currentDetails": "Aucune mesure n'a encore été recueillie.", "currentDetailsRecording": "Enregistrement en cours. {{count}} mesures recueillies jusqu'ici. Dernier taux de comptage : {{rate}} coups par seconde.", "currentDetailsCollected": "{{count}} mesures recueillies. Moyenne de {{mean}} coups par intervalle, écart-type de {{deviation}}, et la racine carrée de la moyenne vaut {{poisson}}.", "controls": { - "sourceRadioGroup": "Source de comptage", "activitySlider": "Activité simulée en coups par seconde", "connectButton": "Connecter un compteur Geiger par Bluetooth", "disconnectButton": "Déconnecter le compteur Geiger", @@ -95,20 +97,25 @@ "recordButton": "Commencer l'enregistrement des mesures", "stopButton": "Arrêter l'enregistrement des mesures", "clearButton": "Effacer les mesures recueillies", - "exportButton": "Exporter les mesures recueillies en fichier CSV" + "exportButton": "Exporter les mesures recueillies en fichier CSV", + "chartViewRadioGroup": "Choisir d'afficher l'histogramme ou le taux de comptage dans le temps", + "poissonCheckbox": "Afficher la prédiction de Poisson", + "gaussianPredictionCheckbox": "Afficher la prédiction gaussienne avec sigma égal à la racine carrée de la moyenne", + "gaussianFitCheckbox": "Afficher la gaussienne ajustée", + "autoBinWidthCheckbox": "Choisir automatiquement la largeur de classe de l'histogramme", + "binWidthControl": "Largeur de classe de l'histogramme en coups" } }, - "lab": { + "device": { "screenSummary": { - "playArea": "La zone de jeu affiche un histogramme des mesures recueillies, avec des courbes de Poisson et gaussiennes facultatives par-dessus, à côté d'un panneau de statistiques.", - "controlArea": "La zone de contrôle permet de choisir une source de comptage, de régler l'intervalle de comptage et la longueur de la série, de démarrer et d'arrêter l'enregistrement, de choisir les courbes théoriques affichées, de régler la largeur de classe, d'exporter les données et de réinitialiser la simulation.", - "interactionHint": "Recueillez une série de mesures, puis comparez l'histogramme aux courbes de Poisson et gaussiennes." + "playArea": "La zone de jeu affiche un histogramme ou un graphique du taux de comptage d'un vrai compteur Geiger connecté par Bluetooth, à côté d'un panneau de statistiques.", + "controlArea": "La zone de contrôle permet de connecter ou déconnecter un compteur Geiger Bluetooth, de choisir le graphique affiché, de régler l'intervalle de comptage et la longueur de la série, de démarrer et d'arrêter l'enregistrement, de choisir les courbes théoriques affichées, de régler la largeur de classe, d'exporter les données et de réinitialiser la simulation.", + "interactionHint": "Connectez un compteur Geiger, recueillez une série de mesures, puis basculez entre l'histogramme et le graphique du taux de comptage pour voir ce que les données montrent." }, "currentDetails": "Aucune mesure n'a encore été recueillie.", "currentDetailsRecording": "Enregistrement en cours. {{count}} mesures recueillies jusqu'ici. Dernier taux de comptage : {{rate}} coups par seconde.", "currentDetailsCollected": "{{count}} mesures recueillies. Moyenne de {{mean}} coups par intervalle, écart-type de {{deviation}}, et la racine carrée de la moyenne vaut {{poisson}}.", "controls": { - "sourceRadioGroup": "Source de comptage", "activitySlider": "Activité simulée en coups par seconde", "connectButton": "Connecter un compteur Geiger par Bluetooth", "disconnectButton": "Déconnecter le compteur Geiger", @@ -119,6 +126,7 @@ "stopButton": "Arrêter l'enregistrement des mesures", "clearButton": "Effacer les mesures recueillies", "exportButton": "Exporter les mesures recueillies en fichier CSV", + "chartViewRadioGroup": "Choisir d'afficher l'histogramme ou le taux de comptage dans le temps", "poissonCheckbox": "Afficher la prédiction de Poisson", "gaussianPredictionCheckbox": "Afficher la prédiction gaussienne avec sigma égal à la racine carrée de la moyenne", "gaussianFitCheckbox": "Afficher la gaussienne ajustée", diff --git a/src/intro/IntroScreen.ts b/src/intro/IntroScreen.ts deleted file mode 100644 index afc7408..0000000 --- a/src/intro/IntroScreen.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * IntroScreen.ts - * - * Wires the Intro model and view together and supplies screen-level options. - * Registered in the screens array in src/main.ts. - */ -import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; -import type { ScreenOptions } from "scenerystack/sim"; -import { Screen } from "scenerystack/sim"; -import type { Tandem } from "scenerystack/tandem"; -import { createIntroIcon } from "../common/RadioactivityAndStatisticsScreenIcons.js"; -import { RadioactivityKeyboardHelpContent } from "../common/view/RadioactivityKeyboardHelpContent.js"; -import type { RadioactivityAndStatisticsPreferencesModel } from "../preferences/RadioactivityAndStatisticsPreferencesModel.js"; -import RadioactivityAndStatisticsColors from "../RadioactivityAndStatisticsColors.js"; -import { IntroModel } from "./model/IntroModel.js"; -import { IntroScreenView } from "./view/IntroScreenView.js"; - -// Require tandem to be explicit — accidental omission would break PhET-iO. -type IntroScreenOptions = ScreenOptions & { tandem: Tandem }; - -export class IntroScreen extends Screen { - public constructor(preferences: RadioactivityAndStatisticsPreferencesModel, options: IntroScreenOptions) { - super( - () => new IntroModel(preferences), - (model) => - new IntroScreenView(model, preferences.showDiagnosticsProperty, { - tandem: options.tandem.createTandem("view"), - }), - optionize()( - { - backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, - createKeyboardHelpNode: () => new RadioactivityKeyboardHelpContent(), - homeScreenIcon: createIntroIcon(), - navigationBarIcon: createIntroIcon(), - }, - options, - ), - ); - } -} diff --git a/src/intro/model/IntroModel.ts b/src/intro/model/IntroModel.ts deleted file mode 100644 index 153ad98..0000000 --- a/src/intro/model/IntroModel.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * IntroModel.ts - * - * Model for the Intro screen: making measurements and watching them fluctuate. - * - * The Intro screen is deliberately thin. All of the acquisition machinery — - * sources, the counting cycle, the collected run, the statistics — lives in the - * shared {@link RadioactivityModel}, which this class composes rather than - * extends. Composition keeps the shared model free of any screen's assumptions - * and lets the Lab screen add its own state without touching this one. - */ - -import type { TModel } from "scenerystack/joist"; -import { RadioactivityModel } from "../../common/model/RadioactivityModel.js"; -import type { RadioactivityAndStatisticsPreferencesModel } from "../../preferences/RadioactivityAndStatisticsPreferencesModel.js"; - -export class IntroModel implements TModel { - /** Sources, counting cycle, collected run, and derived statistics. */ - public readonly acquisition: RadioactivityModel; - - public constructor(preferences: RadioactivityAndStatisticsPreferencesModel) { - this.acquisition = new RadioactivityModel({ - geigerControls: { - beepEnabledProperty: preferences.beepEnabledProperty, - tubeVoltageProperty: preferences.tubeVoltageProperty, - }, - }); - } - - public reset(): void { - this.acquisition.reset(); - } - - public step(dt: number): void { - this.acquisition.step(dt); - } -} diff --git a/src/intro/view/IntroScreenView.ts b/src/intro/view/IntroScreenView.ts deleted file mode 100644 index 9a1a871..0000000 --- a/src/intro/view/IntroScreenView.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * IntroScreenView.ts - * - * The Intro screen: make measurements and watch them fluctuate. - * - * ── Layout ──────────────────────────────────────────────────────────────────── - * Three columns. The left column carries the answer (the live rate) above the - * evidence (the table of every measurement). The centre shows the rate over - * time against its own mean, which is where the fluctuation becomes obvious. - * The right column holds the controls, in the order a user meets them: choose a - * source, then set up and run a measurement. - */ - -import type { TReadOnlyProperty } from "scenerystack/axon"; -import { Bounds2 } from "scenerystack/dot"; -import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; -import { AlignBox, Node, VBox } from "scenerystack/scenery"; -import { ResetAllButton } from "scenerystack/scenery-phet"; -import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; -import { FLAT_RESET_ALL_BUTTON_OPTIONS } from "../../common/RadioactivityAndStatisticsButtonOptions.js"; -import { AcquisitionPanel } from "../../common/view/AcquisitionPanel.js"; -import { CountRateChartNode } from "../../common/view/CountRateChartNode.js"; -import { CountRateDisplayNode } from "../../common/view/CountRateDisplayNode.js"; -import { DataTableNode } from "../../common/view/DataTableNode.js"; -import { SourcePanel } from "../../common/view/SourcePanel.js"; -import { StringManager } from "../../i18n/StringManager.js"; -import { CENTRE_COLUMN_PADDING, PANEL_SPACING, SCREEN_VIEW_MARGIN } from "../../RadioactivityAndStatisticsConstants.js"; -import type { IntroModel } from "../model/IntroModel.js"; -import { IntroScreenSummaryContent } from "./IntroScreenSummaryContent.js"; - -export type IntroScreenViewOptions = ScreenViewOptions; - -export class IntroScreenView extends ScreenView { - public constructor( - model: IntroModel, - showDiagnosticsProperty: TReadOnlyProperty, - providedOptions?: IntroScreenViewOptions, - ) { - const options = optionize()( - { - screenSummaryContent: new IntroScreenSummaryContent(model), - }, - providedOptions, - ); - super(options); - - const a11y = StringManager.getInstance().getIntroA11yStrings(); - const acquisition = model.acquisition; - - // ── Left column: the reading, then the record of every reading ──────────── - const countRateDisplay = new CountRateDisplayNode(acquisition); - const dataTable = new DataTableNode(acquisition); - - const leftColumn = new VBox({ - align: "center", - spacing: 16, - children: [countRateDisplay, dataTable], - left: this.layoutBounds.minX + SCREEN_VIEW_MARGIN, - top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - }); - - // ── Centre: the fluctuation itself ──────────────────────────────────────── - const rateChart = new CountRateChartNode(acquisition); - - // ── Right column: controls, in the order they are used ──────────────────── - const sourcePanel = new SourcePanel(acquisition, a11y.controls, showDiagnosticsProperty); - const acquisitionPanel = new AcquisitionPanel(acquisition, a11y.controls); - - const rightColumn = new VBox({ - align: "right", - spacing: PANEL_SPACING, - children: [sourcePanel, acquisitionPanel], - right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, - top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - }); - - // The chart occupies whatever the two columns leave. An AlignBox centres it - // in that gap and keeps it centred: the chart's bounds change as the axes - // rescale to incoming data, and a one-off centerX would drift out of place - // (and, before the axis lines were removed, straight under a panel). - // maxWidth is the backstop, capping it at the gap it has been given. - const availableBounds = new Bounds2( - leftColumn.right + CENTRE_COLUMN_PADDING, - this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - rightColumn.left - CENTRE_COLUMN_PADDING, - this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, - ); - rateChart.maxWidth = availableBounds.width; - const chartContainer = new AlignBox(rateChart, { - alignBounds: availableBounds, - xAlign: "center", - yAlign: "top", - }); - - this.addChild(leftColumn); - this.addChild(chartContainer); - this.addChild(rightColumn); - - const resetAllButton = new ResetAllButton({ - ...FLAT_RESET_ALL_BUTTON_OPTIONS, - listener: () => { - model.reset(); - this.reset(); - }, - right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, - bottom: this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, - }); - this.addChild(resetAllButton); - - // ── Keyboard / reading order ────────────────────────────────────────────── - // Controls before readouts: a keyboard user wants to reach the things they - // can operate, and the summary already describes the current state. - this.addChild( - new Node({ - pdomOrder: [sourcePanel, acquisitionPanel, dataTable, resetAllButton], - }), - ); - } - - /** Resets view-side state. The charts and table follow the model, so none. */ - public reset(): void { - // Nothing view-only to reset — every visible node derives from the model. - } -} diff --git a/src/lab/LabScreen.ts b/src/lab/LabScreen.ts deleted file mode 100644 index 1549408..0000000 --- a/src/lab/LabScreen.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * LabScreen.ts - * - * Wires the Lab model and view together and supplies screen-level options. - * Registered in the screens array in src/main.ts. - */ -import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; -import type { ScreenOptions } from "scenerystack/sim"; -import { Screen } from "scenerystack/sim"; -import type { Tandem } from "scenerystack/tandem"; -import { createLabIcon } from "../common/RadioactivityAndStatisticsScreenIcons.js"; -import { RadioactivityKeyboardHelpContent } from "../common/view/RadioactivityKeyboardHelpContent.js"; -import type { RadioactivityAndStatisticsPreferencesModel } from "../preferences/RadioactivityAndStatisticsPreferencesModel.js"; -import RadioactivityAndStatisticsColors from "../RadioactivityAndStatisticsColors.js"; -import { LabModel } from "./model/LabModel.js"; -import { LabScreenView } from "./view/LabScreenView.js"; - -// Require tandem to be explicit — accidental omission would break PhET-iO. -type LabScreenOptions = ScreenOptions & { tandem: Tandem }; - -export class LabScreen extends Screen { - public constructor(preferences: RadioactivityAndStatisticsPreferencesModel, options: LabScreenOptions) { - super( - () => new LabModel(preferences), - (model) => - new LabScreenView(model, preferences.showDiagnosticsProperty, { - tandem: options.tandem.createTandem("view"), - }), - optionize()( - { - backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, - createKeyboardHelpNode: () => new RadioactivityKeyboardHelpContent(), - homeScreenIcon: createLabIcon(), - navigationBarIcon: createLabIcon(), - }, - options, - ), - ); - } -} diff --git a/src/lab/view/LabScreenSummaryContent.ts b/src/lab/view/LabScreenSummaryContent.ts deleted file mode 100644 index c13c086..0000000 --- a/src/lab/view/LabScreenSummaryContent.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * LabScreenSummaryContent.ts - * - * The accessible screen summary for the Lab screen. - * - * Shares the live current-details paragraph with the Intro screen — the state - * being described (how much data, what it says) is the same on both screens, - * and the Lab screen's own additions are curve visibility, which the checkboxes - * already announce themselves. - */ - -import { ScreenSummaryContent } from "scenerystack/sim"; -import { createCurrentDetailsProperty } from "../../common/view/currentDetailsProperty.js"; -import { StringManager } from "../../i18n/StringManager.js"; -import type { LabModel } from "../model/LabModel.js"; - -export class LabScreenSummaryContent extends ScreenSummaryContent { - private readonly disposeLabScreenSummaryContent: () => void; - - public constructor(model: LabModel) { - const a11y = StringManager.getInstance().getLabA11yStrings(); - const currentDetails = createCurrentDetailsProperty(model.acquisition, a11y); - - super({ - playAreaContent: a11y.screenSummary.playAreaStringProperty, - controlAreaContent: a11y.screenSummary.controlAreaStringProperty, - currentDetailsContent: currentDetails.property, - interactionHintContent: a11y.screenSummary.interactionHintStringProperty, - }); - - this.disposeLabScreenSummaryContent = currentDetails.dispose; - } - - public override dispose(): void { - this.disposeLabScreenSummaryContent(); - super.dispose(); - } -} diff --git a/src/lab/view/LabScreenView.ts b/src/lab/view/LabScreenView.ts deleted file mode 100644 index 49c32cc..0000000 --- a/src/lab/view/LabScreenView.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * LabScreenView.ts - * - * The Lab screen: what the collected measurements add up to. - * - * ── Layout ──────────────────────────────────────────────────────────────────── - * The histogram takes the centre, because the whole screen exists to be - * compared against it. Acquisition controls stay on the left, in the same order - * as on the Intro screen, so moving between screens does not relocate them. The - * right column holds what is derived from the data — the statistics, and the - * display choices that govern the curves drawn over the bars. - */ - -import type { TReadOnlyProperty } from "scenerystack/axon"; -import { Bounds2 } from "scenerystack/dot"; -import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; -import { AlignBox, Node, VBox } from "scenerystack/scenery"; -import { ResetAllButton } from "scenerystack/scenery-phet"; -import { ScreenView, type ScreenViewOptions } from "scenerystack/sim"; -import { FLAT_RESET_ALL_BUTTON_OPTIONS } from "../../common/RadioactivityAndStatisticsButtonOptions.js"; -import { AcquisitionPanel } from "../../common/view/AcquisitionPanel.js"; -import { DistributionControlsPanel } from "../../common/view/DistributionControlsPanel.js"; -import { HistogramNode } from "../../common/view/HistogramNode.js"; -import { SourcePanel } from "../../common/view/SourcePanel.js"; -import { StatisticsPanel } from "../../common/view/StatisticsPanel.js"; -import { StringManager } from "../../i18n/StringManager.js"; -import { CENTRE_COLUMN_PADDING, PANEL_SPACING, SCREEN_VIEW_MARGIN } from "../../RadioactivityAndStatisticsConstants.js"; -import type { LabModel } from "../model/LabModel.js"; -import { LabScreenSummaryContent } from "./LabScreenSummaryContent.js"; - -export type LabScreenViewOptions = ScreenViewOptions; - -export class LabScreenView extends ScreenView { - public constructor( - model: LabModel, - showDiagnosticsProperty: TReadOnlyProperty, - providedOptions?: LabScreenViewOptions, - ) { - const options = optionize()( - { - screenSummaryContent: new LabScreenSummaryContent(model), - }, - providedOptions, - ); - super(options); - - const a11y = StringManager.getInstance().getLabA11yStrings(); - const acquisition = model.acquisition; - - // ── Left column: the same acquisition controls as the Intro screen ──────── - const sourcePanel = new SourcePanel(acquisition, a11y.controls, showDiagnosticsProperty); - const acquisitionPanel = new AcquisitionPanel(acquisition, a11y.controls); - - const leftColumn = new VBox({ - align: "left", - spacing: PANEL_SPACING, - children: [sourcePanel, acquisitionPanel], - left: this.layoutBounds.minX + SCREEN_VIEW_MARGIN, - top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - }); - - // ── Right column: what the data implies ─────────────────────────────────── - const statisticsPanel = new StatisticsPanel(acquisition); - const distributionControlsPanel = new DistributionControlsPanel(acquisition, model, a11y.controls); - - const rightColumn = new VBox({ - align: "right", - spacing: PANEL_SPACING, - children: [statisticsPanel, distributionControlsPanel], - right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, - top: this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - }); - - // ── Centre: the distribution ────────────────────────────────────────────── - const histogram = new HistogramNode(acquisition, model); - // The chart occupies whatever the two columns leave. An AlignBox centres it - // in that gap and keeps it centred: the chart's bounds change as the axes - // rescale to incoming data, and a one-off centerX would drift out of place - // (and, before the axis lines were removed, straight under a panel). - // maxWidth is the backstop, capping it at the gap it has been given. - const availableBounds = new Bounds2( - leftColumn.right + CENTRE_COLUMN_PADDING, - this.layoutBounds.minY + SCREEN_VIEW_MARGIN, - rightColumn.left - CENTRE_COLUMN_PADDING, - this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, - ); - histogram.maxWidth = availableBounds.width; - const histogramContainer = new AlignBox(histogram, { - alignBounds: availableBounds, - xAlign: "center", - yAlign: "top", - }); - - this.addChild(leftColumn); - this.addChild(histogramContainer); - this.addChild(rightColumn); - - const resetAllButton = new ResetAllButton({ - ...FLAT_RESET_ALL_BUTTON_OPTIONS, - listener: () => { - model.reset(); - this.reset(); - }, - right: this.layoutBounds.maxX - SCREEN_VIEW_MARGIN, - bottom: this.layoutBounds.maxY - SCREEN_VIEW_MARGIN, - }); - this.addChild(resetAllButton); - - // ── Keyboard / reading order ────────────────────────────────────────────── - // Collect data first, then decide how to look at it — the same order the - // screen is meant to be worked through. - this.addChild( - new Node({ - pdomOrder: [sourcePanel, acquisitionPanel, distributionControlsPanel, resetAllButton], - }), - ); - } - - /** Resets view-side state. Every visible node derives from the model. */ - public reset(): void { - // Nothing view-only to reset. - } -} diff --git a/src/main.ts b/src/main.ts index 1e20874..b85aae6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,12 +21,12 @@ import "./brand.js"; import { onReadyToLaunch, PreferencesModel, Sim } from "scenerystack/sim"; import { Tandem } from "scenerystack/tandem"; +import { DeviceScreen } from "./device/DeviceScreen.js"; import { StringManager } from "./i18n/StringManager.js"; -import { IntroScreen } from "./intro/IntroScreen.js"; -import { LabScreen } from "./lab/LabScreen.js"; import { RadioactivityAndStatisticsPreferencesModel } from "./preferences/RadioactivityAndStatisticsPreferencesModel.js"; import { RadioactivityAndStatisticsPreferencesNode } from "./preferences/RadioactivityAndStatisticsPreferencesNode.js"; import RadioactivityAndStatisticsColors from "./RadioactivityAndStatisticsColors.js"; +import { SimulationScreen } from "./simulation/SimulationScreen.js"; onReadyToLaunch(() => { const stringManager = StringManager.getInstance(); @@ -35,14 +35,14 @@ onReadyToLaunch(() => { const simPreferences = new RadioactivityAndStatisticsPreferencesModel(Tandem.ROOT.createTandem("preferences")); const screens = [ - new IntroScreen(simPreferences, { - name: stringManager.getScreenNames().introStringProperty, - tandem: Tandem.ROOT.createTandem("introScreen"), + new SimulationScreen(simPreferences, { + name: stringManager.getScreenNames().simulationStringProperty, + tandem: Tandem.ROOT.createTandem("simulationScreen"), backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, }), - new LabScreen(simPreferences, { - name: stringManager.getScreenNames().labStringProperty, - tandem: Tandem.ROOT.createTandem("labScreen"), + new DeviceScreen(simPreferences, { + name: stringManager.getScreenNames().deviceStringProperty, + tandem: Tandem.ROOT.createTandem("deviceScreen"), backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, }), ]; diff --git a/src/simulation/SimulationScreen.ts b/src/simulation/SimulationScreen.ts new file mode 100644 index 0000000..bf61949 --- /dev/null +++ b/src/simulation/SimulationScreen.ts @@ -0,0 +1,50 @@ +/** + * SimulationScreen.ts + * + * Wires the Simulation model and view together and supplies screen-level + * options. Registered in the screens array in src/main.ts. + */ +import { type EmptySelfOptions, optionize } from "scenerystack/phet-core"; +import type { ScreenOptions } from "scenerystack/sim"; +import { Screen } from "scenerystack/sim"; +import type { Tandem } from "scenerystack/tandem"; +import { CountSourceType } from "../common/model/CountSource.js"; +import { createSimulationIcon } from "../common/RadioactivityAndStatisticsScreenIcons.js"; +import { RadioactivityKeyboardHelpContent } from "../common/view/RadioactivityKeyboardHelpContent.js"; +import { RadioactivityScreenView } from "../common/view/RadioactivityScreenView.js"; +import { StringManager } from "../i18n/StringManager.js"; +import type { RadioactivityAndStatisticsPreferencesModel } from "../preferences/RadioactivityAndStatisticsPreferencesModel.js"; +import RadioactivityAndStatisticsColors from "../RadioactivityAndStatisticsColors.js"; +import { SimulationModel } from "./model/SimulationModel.js"; +import { SimulationScreenSummaryContent } from "./view/SimulationScreenSummaryContent.js"; + +// Require tandem to be explicit — accidental omission would break PhET-iO. +type SimulationScreenOptions = ScreenOptions & { tandem: Tandem }; + +export class SimulationScreen extends Screen { + public constructor(preferences: RadioactivityAndStatisticsPreferencesModel, options: SimulationScreenOptions) { + super( + () => new SimulationModel(preferences), + (model) => + new RadioactivityScreenView( + model, + CountSourceType.SIMULATED, + preferences.showDiagnosticsProperty, + StringManager.getInstance().getSimulationA11yStrings().controls, + { + screenSummaryContent: new SimulationScreenSummaryContent(model), + tandem: options.tandem.createTandem("view"), + }, + ), + optionize()( + { + backgroundColorProperty: RadioactivityAndStatisticsColors.backgroundColorProperty, + createKeyboardHelpNode: () => new RadioactivityKeyboardHelpContent(), + homeScreenIcon: createSimulationIcon(), + navigationBarIcon: createSimulationIcon(), + }, + options, + ), + ); + } +} diff --git a/src/simulation/model/SimulationModel.ts b/src/simulation/model/SimulationModel.ts new file mode 100644 index 0000000..e3870a5 --- /dev/null +++ b/src/simulation/model/SimulationModel.ts @@ -0,0 +1,26 @@ +/** + * SimulationModel.ts + * + * Model for the Simulation screen: a mock-up counting source with a known, + * adjustable activity, that can be viewed as either the histogram or the + * count-rate chart. + * + * Being the only source whose true λ is known, this is the one screen where + * the σ = √λ prediction can be checked against an answer known in advance. + */ + +import { ChartViewType } from "../../common/model/ChartViewType.js"; +import { CountSourceType } from "../../common/model/CountSource.js"; +import { RadioactivityScreenModel } from "../../common/model/RadioactivityScreenModel.js"; +import type { RadioactivityAndStatisticsPreferencesModel } from "../../preferences/RadioactivityAndStatisticsPreferencesModel.js"; + +export class SimulationModel extends RadioactivityScreenModel { + public constructor(preferences: RadioactivityAndStatisticsPreferencesModel) { + super(preferences, { + fixedSourceType: CountSourceType.SIMULATED, + // Opens on the fluctuating trace, the sim's central point, before any + // statistic has been computed from it. + initialChartView: ChartViewType.COUNT_RATE, + }); + } +} diff --git a/src/intro/view/IntroScreenSummaryContent.ts b/src/simulation/view/SimulationScreenSummaryContent.ts similarity index 60% rename from src/intro/view/IntroScreenSummaryContent.ts rename to src/simulation/view/SimulationScreenSummaryContent.ts index e51be69..8bcdae0 100644 --- a/src/intro/view/IntroScreenSummaryContent.ts +++ b/src/simulation/view/SimulationScreenSummaryContent.ts @@ -1,25 +1,25 @@ /** - * IntroScreenSummaryContent.ts + * SimulationScreenSummaryContent.ts * - * The accessible screen summary for the Intro screen — the first thing a + * The accessible screen summary for the Simulation screen — the first thing a * screen-reader user encounters, and the place they return to for the sim's * current state. * * The current-details region is live: it reports how much data has been * collected and what it says, so a non-visual user gets the same information - * the count-rate readout and the chart carry visually. + * the charts carry visually. */ import { ScreenSummaryContent } from "scenerystack/sim"; import { createCurrentDetailsProperty } from "../../common/view/currentDetailsProperty.js"; import { StringManager } from "../../i18n/StringManager.js"; -import type { IntroModel } from "../model/IntroModel.js"; +import type { SimulationModel } from "../model/SimulationModel.js"; -export class IntroScreenSummaryContent extends ScreenSummaryContent { - private readonly disposeIntroScreenSummaryContent: () => void; +export class SimulationScreenSummaryContent extends ScreenSummaryContent { + private readonly disposeSimulationScreenSummaryContent: () => void; - public constructor(model: IntroModel) { - const a11y = StringManager.getInstance().getIntroA11yStrings(); + public constructor(model: SimulationModel) { + const a11y = StringManager.getInstance().getSimulationA11yStrings(); const currentDetails = createCurrentDetailsProperty(model.acquisition, a11y); super({ @@ -29,11 +29,11 @@ export class IntroScreenSummaryContent extends ScreenSummaryContent { interactionHintContent: a11y.screenSummary.interactionHintStringProperty, }); - this.disposeIntroScreenSummaryContent = currentDetails.dispose; + this.disposeSimulationScreenSummaryContent = currentDetails.dispose; } public override dispose(): void { - this.disposeIntroScreenSummaryContent(); + this.disposeSimulationScreenSummaryContent(); super.dispose(); } }