Skip to content

Commit

Permalink
Create a initialValues array that phet-io clients can set, see: #334
Browse files Browse the repository at this point in the history
  • Loading branch information
marlitas committed Aug 2, 2024
1 parent 7230ca2 commit e4f21c5
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 21 deletions.
27 changes: 19 additions & 8 deletions js/common/model/Plate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ type SelfOptions<T extends Snack> = {
initiallyActive?: boolean;
initialXPosition?: number;
linePlacement: number; // 0-indexed, this is primarily used for debugging and tandem names.
startingNumberOfSnacks?: number;

// The function used to position the snacks on the snack stack.
snackStackingFunction?: ( plateXPosition: number, index: number ) => Vector2;
Expand Down Expand Up @@ -65,9 +64,6 @@ export default class Plate<T extends Snack> extends PhetioObject {
// The plate's index, 0-indexed. This is primarily used for debugging.
public readonly linePlacement: number;

// The number of snacks this plate should have on it when it becomes active.
public readonly startingNumberOfSnacks: number;

// The function used to set the positions of the snacks on this plate to form what looks like a stack.
private readonly snackStackingFunction: ( plateXPosition: number, index: number ) => Vector2;

Expand All @@ -76,20 +72,24 @@ export default class Plate<T extends Snack> extends PhetioObject {
private readonly releaseSnack: ( snack: T ) => void;
private readonly handleFraction: ( plate: Plate<T>, fraction: Fraction ) => void;

// The number of snacks this plate should have on it when it becomes active.
public readonly startingNumberOfSnacksProperty: TReadOnlyProperty<number>;

/**
* @param getAvailableSnack - a function that can be used to get an available snack from the parent model
* @param releaseSnack - a function for releasing a snack that is no longer needed by this plate to the parent model
* @param initialPlateValuesProperty - the property that contains the array of initial number of snacks for the screen
* @param providedOptions
*/
public constructor( getAvailableSnack: () => T | null,
releaseSnack: ( snack: T ) => void,
initialPlateValuesProperty: TReadOnlyProperty<number[]>,
providedOptions: PlateOptions<T> ) {

const options = optionize<PlateOptions<T>, SelfOptions<T>, PhetioObjectOptions>()( {
initiallyActive: false,
initialXPosition: 0,
phetioState: false,
startingNumberOfSnacks: 1,
snackStackingFunction: SnackStacker.getStackedCandyBarPosition,
handleFraction: _.noop, // By default plates _.noop snack fraction values.
isDisposable: false
Expand All @@ -99,7 +99,6 @@ export default class Plate<T extends Snack> extends PhetioObject {

this.getAvailableSnack = getAvailableSnack;
this.releaseSnack = releaseSnack;
this.startingNumberOfSnacks = options.startingNumberOfSnacks;
this.snackStackingFunction = options.snackStackingFunction;

this.isActiveProperty = new BooleanProperty( options.initiallyActive, {
Expand All @@ -112,16 +111,28 @@ export default class Plate<T extends Snack> extends PhetioObject {

this.xPositionProperty = new NumberProperty( options.initialXPosition );

this.startingNumberOfSnacksProperty = new DerivedProperty( [ initialPlateValuesProperty ], initialPlateValues =>
initialPlateValues[ options.linePlacement ] );

// So that reset of isActiveProperty and reset of tableSnackNumberProperty are in agreement, make sure their initial
// states are compatible.
const initialTableSnackNumber = options.initiallyActive ? options.startingNumberOfSnacks : 0;
const initialTableSnackNumber = options.initiallyActive ? this.startingNumberOfSnacksProperty.value : 0;
this.tableSnackNumberProperty = new NumberProperty( initialTableSnackNumber, {
range: new Range( 0, 10 ),
numberType: 'Integer',

// phet-io
tandem: options.tandem.createTandem( 'tableSnackNumberProperty' ),
phetioFeatured: true
phetioFeatured: false,
phetioReadOnly: true
} );

// We want to change the value of all active plates and the initial value of initially active plates.
// This is because client has set initial snack values that should be immediately represented by active plates,
// and additionally we want tableSnackNumberProperty.reset to behave correctly.
this.startingNumberOfSnacksProperty.link( startingNumberOfSnacks => {
this.isActiveProperty.value && ( this.tableSnackNumberProperty.value = startingNumberOfSnacks );
options.initiallyActive && this.tableSnackNumberProperty.setInitialValue( startingNumberOfSnacks );
} );

// Create the observable array that tracks the snacks a notepad plate has.
Expand Down
17 changes: 16 additions & 1 deletion js/common/model/SharingModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export default class SharingModel<T extends Snack> extends PhetioObject implemen
// Allows PhET-iO clients to modify the max number of plates in the screen.
private readonly maxPlatesProperty: Property<number>;

protected readonly initialPlateValuesProperty: Property<number[]>;

/**
*
* @param snackCreator - A function that creates a new snack based on the specific needs of each model.
Expand Down Expand Up @@ -147,18 +149,31 @@ export default class SharingModel<T extends Snack> extends PhetioObject implemen

// Create the set of plates that will hold the snacks.
assert && assert( options.initialPlateValues.length === MAX_PLATES, 'initialPlateValues must have the same length as the number of plates' );
this.initialPlateValuesProperty = new Property( options.initialPlateValues, {
phetioValueType: ArrayIO( NumberIO ),
isValidValue: ( snackValues: number[] ) =>
snackValues.length === MAX_PLATES &&
snackValues.every(
value => value >= MeanShareAndBalanceConstants.MIN_NUMBER_OF_SNACKS_PER_PLATE &&
value <= MeanShareAndBalanceConstants.MAX_NUMBER_OF_SNACKS_PER_PLATE ),
tandem: options.tandem.createTandem( 'initialPlateValuesProperty' ),
phetioDocumentation: `Set the initial snack value of each plate. The array length should be equal to ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_DATA_SETS} (the default max number of plates).` +
`The values should be between ${MeanShareAndBalanceConstants.MIN_NUMBER_OF_SNACKS_PER_PLATE} and ${MeanShareAndBalanceConstants.MAX_NUMBER_OF_SNACKS_PER_PLATE}.` +
`The default initial values are: ${options.initialPlateValues}.`,
phetioFeatured: true
} );
this.plates = [];
const platesParentTandem = options.tandem.createTandem( 'plates' );
_.times( MAX_PLATES, plateIndex => {
const initialXPosition = plateIndex * INTER_PLATE_DISTANCE;
const plate = new Plate(
this.getUnusedSnack.bind( this ),
this.releaseSnack.bind( this ),
this.initialPlateValuesProperty,
{
initialXPosition: initialXPosition,
initiallyActive: plateIndex < this.numberOfPlatesProperty.value,
linePlacement: plateIndex,
startingNumberOfSnacks: options.initialPlateValues[ plateIndex ],
handleFraction: handleFraction,
snackStackingFunction: snackStackingFunction,

Expand Down
2 changes: 1 addition & 1 deletion js/distribute/model/DistributeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export default class DistributeModel extends SharingModel<Snack> {

// Set this plate's number of table snacks to zero so that it will release the snacks it has. This must be done
// after the redistribution above.
plate.tableSnackNumberProperty.value = isActive ? plate.startingNumberOfSnacks : 0;
plate.tableSnackNumberProperty.value = isActive ? plate.startingNumberOfSnacksProperty.value : 0;
} );
} );

Expand Down
2 changes: 1 addition & 1 deletion js/fair-share/model/FairShareModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export default class FairShareModel extends SharingModel<Apple> {
plate.tableSnackNumberProperty.set( 0 );
}
else {
plate.tableSnackNumberProperty.value = plate.startingNumberOfSnacks;
plate.tableSnackNumberProperty.value = plate.startingNumberOfSnacksProperty.value;
}
} );

Expand Down
3 changes: 2 additions & 1 deletion js/level-out/model/Cup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export default class Cup {

// phet-io
tandem: tandem.createTandem( 'waterLevelProperty' ),
phetioFeatured: options.isTableCup
phetioFeatured: false,
phetioReadOnly: true
}, options.waterLevelPropertyOptions )
);

Expand Down
45 changes: 36 additions & 9 deletions js/level-out/model/LevelOutModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import WithRequired from '../../../../phet-core/js/types/WithRequired.js';
type LevelOutModelOptions = EmptySelfOptions & WithRequired<PhetioObjectOptions, 'tandem'>;

// constants
const INITIAL_WATER_LEVELS = [ 0.75, 0.5, 0.2, 0.65, 0.9, 0.35, 0.75 ];
const DEFAULT_INITIAL_WATER_LEVELS = [ 0.75, 0.5, 0.2, 0.65, 0.9, 0.35, 0.75 ];
const NUMBER_OF_CUPS_RANGE = new Range( 1, MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_DATA_SETS );
const INTER_CUP_DISTANCE = MeanShareAndBalanceConstants.CUP_WIDTH + MeanShareAndBalanceConstants.PIPE_LENGTH;

Expand Down Expand Up @@ -64,6 +64,7 @@ export default class LevelOutModel extends PhetioObject implements TModel {
// phet-io specific Properties
public readonly successIndicatorsOperatingProperty: Property<boolean>;
private readonly maxCupsProperty: Property<number>;
private readonly initialWaterLevelsProperty: Property<number[]>;

public constructor( providedOptions: LevelOutModelOptions ) {

Expand Down Expand Up @@ -117,11 +118,6 @@ export default class LevelOutModel extends PhetioObject implements TModel {
tandem: options.tandem.createTandem( 'maxCupsProperty' )
} );

// The tableCups are the "ground truth" and the notepadCups mirror them.
this.tableCups = [];
this.notepadCups = [];
this.pipeArray = [];

const pipesParentTandem = options.tandem.createTandem( 'pipes' );

this.pipesOpenProperty = new BooleanProperty( false, {
Expand All @@ -134,12 +130,30 @@ export default class LevelOutModel extends PhetioObject implements TModel {
const notepadCupsParentTandem = options.tandem.createTandem( 'notepadCups' );
const tableCupsParentTandem = options.tandem.createTandem( 'tableCups' );

this.initialWaterLevelsProperty = new Property( DEFAULT_INITIAL_WATER_LEVELS, {
tandem: options.tandem.createTandem( 'initialWaterLevelsProperty' ),
isValidValue: ( waterLevels: number[] ) =>
waterLevels.length === MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_DATA_SETS &&
_.every( waterLevels,
waterLevel => waterLevel >= MeanShareAndBalanceConstants.WATER_LEVEL_RANGE_MIN
&& waterLevel <= MeanShareAndBalanceConstants.WATER_LEVEL_RANGE_MAX ),
phetioValueType: ArrayIO( NumberIO ),
phetioFeatured: true,
phetioDocumentation: `Set the initial water levels of each cup. The array length should be equal to ${MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_DATA_SETS} (the default max number of cups).` +
`The water levels should be between ${MeanShareAndBalanceConstants.WATER_LEVEL_RANGE_MIN} and ${MeanShareAndBalanceConstants.WATER_LEVEL_RANGE_MAX}.` +
`The default initial water levels are: ${DEFAULT_INITIAL_WATER_LEVELS}.`
} );

// Statically allocate cups and pipes
// The tableCups are the "ground truth" and the notepadCups mirror them.
this.tableCups = [];
this.notepadCups = [];
this.pipeArray = [];
for ( let i = 0; i < MeanShareAndBalanceConstants.MAXIMUM_NUMBER_OF_DATA_SETS; i++ ) {
const totalSpan = Math.max( ( this.numberOfCupsProperty.value - 1 ) * ( INTER_CUP_DISTANCE ), 0 );
const leftCupCenterX = -( totalSpan / 2 );
const initialXPosition = leftCupCenterX + ( i * INTER_CUP_DISTANCE ) - MeanShareAndBalanceConstants.CUP_WIDTH / 2;
const waterLevel = INITIAL_WATER_LEVELS[ i ];
const waterLevel = this.initialWaterLevelsProperty.value[ i ];
this.tableCups.push( new Cup( tableCupsParentTandem.createTandem( `tableCup${i + 1}` ), {
waterLevel: waterLevel,
xPosition: initialXPosition,
Expand Down Expand Up @@ -241,6 +255,19 @@ export default class LevelOutModel extends PhetioObject implements TModel {
this.numberOfCupsProperty.value = Math.min( this.numberOfCupsProperty.value, max );
this.numberOfCupsRangeProperty.value = new Range( NUMBER_OF_CUPS_RANGE.min, max );
} );
this.initialWaterLevelsProperty.link( waterLevels => {

this.iterateCups( ( notepadCup, tableCup, i ) => {

// We change both the value and initial value of each cup's waterLevelProperty because client has set initial
// water values and we want waterLevelProperty.reset to behave correctly.
notepadCup.waterLevelProperty.value = waterLevels[ i ];
tableCup.waterLevelProperty.value = waterLevels[ i ];
notepadCup.waterLevelProperty.setInitialValue( this.initialWaterLevelsProperty.value[ i ] );
tableCup.waterLevelProperty.setInitialValue( this.initialWaterLevelsProperty.value[ i ] );
} );
this.matchCupWaterLevels();
} );
}

private getNumberOfActiveCups(): number {
Expand Down Expand Up @@ -363,11 +390,11 @@ export default class LevelOutModel extends PhetioObject implements TModel {
/**
* Visit pairs of table/notepad cups
*/
private iterateCups( callback: ( notepadCup: Cup, tableCup: Cup ) => void ): void {
private iterateCups( callback: ( notepadCup: Cup, tableCup: Cup, index: number ) => void ): void {
this.assertConsistentState();

for ( let i = 0; i < this.numberOfCupsProperty.value; i++ ) {
callback( this.getActiveNotepadCups()[ i ], this.getActiveTableCups()[ i ] );
callback( this.getActiveNotepadCups()[ i ], this.getActiveTableCups()[ i ], i );
}
}

Expand Down

0 comments on commit e4f21c5

Please sign in to comment.