phetsims / number-pairs

"Number Pairs" is an educational simulation in HTML5, by PhET Interactive Simulations.
GNU General Public License v3.0
0 stars 0 forks source link

Preliminary review of view code. #20

Closed pixelzoom closed 1 week ago

pixelzoom commented 1 month ago

Similar to the model review that I did in https://github.com/phetsims/number-pairs/issues/9#issuecomment-2375275553 ...

Most of the view code is in number-pairs/common/view/.

Focus on these classes first:

pixelzoom commented 1 month ago

@marlitas The view is looking really great - some nice solutions to complicated problems. Well done!

Below is a check list of things thast I noted. I'm going to add to this list as I go, and will assign to you to review when I'm done. Let me know if you'd like to discuss any of these.







// EllipticalArrowNode needs the starting and ending values to be Property<number> instances. Even though
// the starting value for the leftAddendArrow and totalArrow will always be 0.
- const zeroNumberProperty = new Property( options.numberLineRange.min );
+ const zeroNumberProperty = new Property( options.numberLineRange.min, {
+   isValidValue: value => value === options.numberLineRange.min
+ } );

- const options = combineOptions<NodeOptions>( {
+ const options = optionize<CubeNodeOptions, SelfOptions, NodeOptions>()( {
  children: [ cubeLeftAddendImage, cubeRightAddendImage ],
  visibleProperty: new DerivedProperty( [ model.addendTypeProperty ], addendType => addendType !== AddendType.INACTIVE )
}, providedOptions );

There are similar misuses of combineOptions in:


public constructor( model: NumberPairsModel, countingAreaBounds: Bounds2, providedOptions: WithRequired<VerticalCheckboxGroupOptions, 'tandem'> ) {
  const options = combineOptions<VerticalCheckboxGroupOptions>( {
    bottom: countingAreaBounds.top - 8,
    right: countingAreaBounds.right
  }, providedOptions );

- type SceneSelectionRadioButtonGroupOptions = SelfOptions & RectangularRadioButtonGroupOptions;
+ type TotalRadioButtonGroupOptions = SelfOptions & RectangularRadioButtonGroupOptions;

- public constructor( providedOptions?: SpeechSynthesisButtonOptions )
+ public constructor( providedOptions: SpeechSynthesisButtonOptions )

- public constructor( countingAreaBounds: Bounds2, providedOptions?: NodeOptions ) {
+ type SelfOptions = EmptySelfOptions
+ type SplitCountingAreaNode = SelfOptions & StrictOmit<NodeOptions, ‘children’>;
+ public constructor( countingAreaBounds: Bounds2, providedOptions?: SplitCountingAreaNodeOptions ) {

In NumberPairsModel, countingRepresentationTypeProperty should probably have validValues, instead of relying on the default for EnumerationProperty:

this.countingRepresentationTypeProperty = new EnumerationProperty( options.initialCountingRepresentationType, {
  tandem: options.tandem.createTandem( 'countingRepresentationTypeProperty' )
} );

Where countingRepresentationTypeProperty is used in the view, it would be preferrable to get the validValues from countingRepresentationTypeProperty, rather than as a separate arg or options. Otherwise, you run the risk of providing an invalid set of values.

For example, in CountingRepresentationRadioButtonGroup:

- const groupItems = providedOptions.countingRepresentations.map( countingRepresentationType => {
+ const groupItems = countingRepresentationTypeProperty.validValues.map( countingRepresentationType => {

leftAddendNumberProperty => leftAddendProperty rightAddendNumberProperty => rightAddendProperty totalNumberProperty => totalProperty


- public constructor( selectedSceneModelProperty: PhetioProperty<NumberPairsSceneModel>, sceneModels: NumberPairsSceneModel[],
+ public constructor( selectedSceneProperty: PhetioProperty<NumberPairsScene>, scenes: NumberPairsScene[],

- type NumberLineSliderOptions = WithRequired<SliderOptions, 'tandem'> & SelfOptions;
+ type NumberLineSliderOptions = SelfOptions & WithRequired<SliderOptions, 'tandem'>;

In my experience, sim code is more robust if you add only the options that are actually needed, so that options are not set unintentionally, and so that you don’t need to be vigilant about revisiting options when you add responsibilities to a class. I realize that not everyone shares that philosophy, so up to you about how "narrow" to make your Option types. But they should not be so "wide" that options that can break the implementation are exposed.

Here are examples, with a comment indicating which options I can set to break the implementation.

// content, listener
type CommutativeButtonOptions = SelfOptions & WithRequired<RectangularPushButtonOptions, 'tandem'>;

// content, listener
type TenFrameButtonOptions = SelfOptions & WithRequired<RectangularPushButtonOptions, 'tandem'>;

// content
type SpeechSynthesisButtonOptions = WithRequired<RectangularPushButtonOptions, 'tandem'>;

// visibilityProperty
type CubeNodeOptions = SelfOptions & PickRequired<NodeOptions, 'tandem'> & StrictOmit<NodeOptions, 'children'>;

// radius, because it’s also passed in as the first constructor arg, and CircleOptions.radius will override that.
type NumberCircleOptions = StrictOmit<CircleOptions, 'children'>;

// rectSize, which is computed from the squareDimension constructor arg.
type NumberSquareOptions = SelfOptions & StrictOmit<RectangleOptions, 'children'>;

// thumbNode, trackNode, constrainValue, enabledRangeProperty
type NumberLineSliderOptions = WithRequired<SliderOptions, 'tandem'> & SelfOptions;

// orientation
type CountingRepresentationRadioButtonGroupOptions = SelfOptions & WithRequired<RectangularRadioButtonGroupOptions, 'tandem'>;

// children
export type NumberPairsScreenViewOptions = SelfOptions & WithRequired<ScreenViewOptions, 'tandem'>;

// Is being able to override showTitleWhenExpanded desirable?
export type TotalRepresentationAccordionBoxOptions = WithRequired<AccordionBoxOptions, 'titleNode' | 'tandem'>;
pixelzoom commented 1 month ago

Review completed, over to you @marlitas.

marlitas commented 3 weeks ago

You may also wish to exclude NodeTranslationOptions if this class is really responsive for translation.

I would like those translation options to be the default, but I don't think passing in a different translation in a non-default use case would break the component.

I have a question about options pattern that I documented with this issue number in KittenNode

Valid values for countingRepresentationTypeProperty should be obtained from that Property.

This is an option because each screen has a different set of counting representation types. I can't determine that based on valid values. Perhaps an assertion to confirm that any values I pass in are of CountingRepresentationType. But I thought that would be covered by type checking with the option's type defined as: CountingRepresentationType[]. Or wait... perhaps you're indicating that the valid values for that Property are specifically defined for each screen? Let's confirm I'm understanding on Monday.

Is being able to override showTitleWhenExpanded desirable?

Mmmmm... maybe? I don't think it breaks anything and it feels like an unnecessary way to tie hands in the future if we do want it for one of the accordion boxes.

pixelzoom commented 3 weeks ago

Or wait... perhaps you're indicating that the valid values for that Property are specifically defined for each screen?

Correct. The default for validValues in this case is CountingRepresentationType.enumeration.values. That is not appropriate for all screens -- validValues should be the subset of CountingRepresentationType.enumeration.values supported by the screen. Otherwise you (or a PhET-iO client!) can set countingRepresentationTypeProperty to a value that is not supported by a screen.

marlitas commented 1 week ago

I believe all of the items in this issue have been addressed. @pixelzoom feel free to re-open if you disagree.