Skip to content

Commit

Permalink
Move dragIndicator logic to model, see: #189
Browse files Browse the repository at this point in the history
# Conflicts:
#	js/common/view/CAVScreenView.ts
#	js/common/view/SceneView.ts
  • Loading branch information
marlitas committed May 11, 2023
1 parent 587dc9e commit 5370b5d
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 55 deletions.
75 changes: 75 additions & 0 deletions js/common/model/CAVModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import Property from '../../../../axon/js/Property.js';
import CAVSceneModel from './CAVSceneModel.js';
import ReferenceIO from '../../../../tandem/js/types/ReferenceIO.js';
import IOType from '../../../../tandem/js/types/IOType.js';
import DynamicProperty from '../../../../axon/js/DynamicProperty.js';
import Multilink from '../../../../axon/js/Multilink.js';

type SelfOptions = {
tandem: Tandem;
Expand All @@ -25,6 +27,11 @@ export type CAVModelOptions = SelfOptions;

export default class CAVModel {

public readonly dragIndicatorVisibleProperty: Property<boolean>; // Screens 1-3
public readonly dragIndicatorValueProperty: Property<number | null>;
public readonly objectNodesInputEnabledProperty: Property<boolean>; // Screens 1-3

// TODO: Should these be playAreaMedianVisibleProperty? https://github.com/phetsims/center-and-variability/issues/189
public readonly isShowingPlayAreaMedianProperty: BooleanProperty; // Screens 1-3
public readonly isShowingPlayAreaMeanProperty: BooleanProperty; // Screens 2-3
public readonly isShowingMedianPredictionProperty: BooleanProperty; // Screens 1-2
Expand Down Expand Up @@ -68,6 +75,74 @@ export default class CAVModel {
this.soccerBallHasBeenDraggedProperty = new BooleanProperty( false, {
tandem: options.tandem.createTandem( 'soccerBallHasBeenDraggedProperty' )
} );

const objectNodeGroupTandem = options.tandem.createTandem( 'soccerBallNodeGroup' );

this.objectNodesInputEnabledProperty = new BooleanProperty( true, {
tandem: objectNodeGroupTandem.createTandem( 'inputEnabledProperty' )
} );

// These DynamicProperties allow us to track all the necessary scenes Properties for dragIndicator update, and not
// just the first selectedScene
const selectedSceneSoccerBallCountProperty = new DynamicProperty<number, number, CAVSceneModel>( this.selectedSceneModelProperty, {
derive: 'soccerBallCountProperty'
} );
const selectedSceneMaxKicksProperty = new DynamicProperty<number, number, CAVSceneModel>( this.selectedSceneModelProperty, {
derive: 'maxKicksProperty'
} );

this.dragIndicatorVisibleProperty = new BooleanProperty( false, { tandem: options.tandem.createTandem( 'dragIndicatorVisibleProperty' ) } );
this.dragIndicatorValueProperty = new Property<number | null>( null, { tandem: options.tandem.createTandem( 'dragIndicatorValueProperty' ) } );

Multilink.multilink( [ this.selectedSceneModelProperty,
this.soccerBallHasBeenDraggedProperty, selectedSceneSoccerBallCountProperty,
selectedSceneMaxKicksProperty
],
( selectedSceneModel, soccerBallHasBeenDragged, soccerBallCount, maxKicks ) => {

if ( soccerBallCount !== null ) {
this.updateDragIndicator( selectedSceneModel, soccerBallHasBeenDragged, soccerBallCount, maxKicks );
}
} );

// It is important to link to the values of all the soccer balls in the screen, so that the dragIndicator can be
// updated after all the balls have landed, and not just after they have been kicked.
sceneModels.forEach( sceneModel => {
sceneModel.soccerBalls.forEach( soccerBall => soccerBall.valueProperty.link( value => {
if ( value !== null ) {
this.updateDragIndicator( this.selectedSceneModelProperty.value, this.soccerBallHasBeenDraggedProperty.value,
selectedSceneSoccerBallCountProperty.value, selectedSceneMaxKicksProperty.value );
}
} ) );
} );

}

private updateDragIndicator( selectedSceneModel: CAVSceneModel, soccerBallHasBeenDragged: boolean, soccerBallCount: number, maxKicks: number ): void {

// if an object was moved, objects are not input enabled, or the max number of balls haven't been kicked out
// don't show the dragIndicatorArrowNode
const indicatorVisible = soccerBallCount === maxKicks &&
this.objectNodesInputEnabledProperty.value &&
_.every( selectedSceneModel?.getActiveSoccerBalls(), soccerBall => soccerBall.valueProperty.value !== null ) &&
!soccerBallHasBeenDragged;
this.dragIndicatorVisibleProperty.value = indicatorVisible;

if ( indicatorVisible ) {
const selectedSceneModel = this.selectedSceneModelProperty.value;
const reversedBalls = selectedSceneModel.getActiveSoccerBalls().reverse();

// TODO: The only way we can assume that there will always be a stack that is not the median stack https://github.com/phetsims/center-and-variability/issues/189
// TODO: Is if we can confirm that balls will never land at the same spot without dragging. https://github.com/phetsims/center-and-variability/issues/189
// TODO: Should we not show the dragIndicator in this case? https://github.com/phetsims/center-and-variability/issues/189
// add the dragIndicatorArrowNode above the last object when it is added to the play area.
// However, we also want to make sure that the dragIndicator is not in the same position as the Median Indicator.
const value: number = reversedBalls
.find( soccerBall => soccerBall.valueProperty.value !== selectedSceneModel.medianValueProperty.value )!
.valueProperty.value!;

this.dragIndicatorValueProperty.value = value;
}
}

public step( dt: number ): void {
Expand Down
29 changes: 29 additions & 0 deletions js/common/view/CAVScreenView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import DynamicProperty from '../../../../axon/js/DynamicProperty.js';
import SoccerPlayerNode, { SoccerPlayerImageSet } from './SoccerPlayerNode.js';
import SoccerPlayer from '../model/SoccerPlayer.js';
import CAVSceneModel from '../model/CAVSceneModel.js';
import DragIndicatorArrowNode from './DragIndicatorArrowNode.js';
import CAVObjectType from '../model/CAVObjectType.js';
import Multilink from '../../../../axon/js/Multilink.js';

type SelfOptions = {
questionBarOptions: QuestionBarOptions;
Expand Down Expand Up @@ -162,6 +165,32 @@ export default class CAVScreenView extends ScreenView {
centerY: ( GROUND_POSITION_Y + this.layoutBounds.maxY ) / 2 + 2,
tandem: options.tandem.createTandem( 'kickButtonGroup' )
} ) );

const dragIndicatorArrowNode = new DragIndicatorArrowNode( {
tandem: options.tandem.createTandem( 'dragIndicatorArrowNode' )
} );

Multilink.multilink( [ model.dragIndicatorVisibleProperty, model.dragIndicatorValueProperty ],
( dragIndicatorVisible, dragIndicatorValue ) => {
dragIndicatorArrowNode.visible = dragIndicatorVisible;

if ( dragIndicatorVisible && dragIndicatorValue ) {
const selectedSceneModel = model.selectedSceneModelProperty.value;

dragIndicatorArrowNode.centerX = modelViewTransform.modelToViewX( dragIndicatorValue );
const dragIndicatorArrowNodeMargin = 6;

// calculate where the top object is
const topObjectPositionY = modelViewTransform.modelToViewY( 0 ) -
( selectedSceneModel.getStackAtLocation( dragIndicatorValue ).length ) *
Math.abs( modelViewTransform.modelToViewDeltaY( CAVObjectType.SOCCER_BALL.radius ) ) * 2 -
dragIndicatorArrowNodeMargin;

dragIndicatorArrowNode.bottom = topObjectPositionY;
}
} );

this.backObjectLayer.addChild( dragIndicatorArrowNode );
}

private updateAccordionBoxPosition(): void {
Expand Down
56 changes: 1 addition & 55 deletions js/common/view/SceneView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ import { AnimationMode } from '../model/AnimationMode.js';
import CAVObjectType from '../model/CAVObjectType.js';
import CAVSceneModel from '../model/CAVSceneModel.js';
import SoccerPlayerNode, { SoccerPlayerImageSet } from './SoccerPlayerNode.js';
import BooleanProperty from '../../../../axon/js/BooleanProperty.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import CAVModel from '../model/CAVModel.js';
import centerAndVariability from '../../centerAndVariability.js';
import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js';
import DragIndicatorArrowNode from './DragIndicatorArrowNode.js';
import PlayAreaMedianIndicatorNode from './PlayAreaMedianIndicatorNode.js';
import AccordionBox from '../../../../sun/js/AccordionBox.js';
import SoccerBall from '../model/SoccerBall.js';
Expand All @@ -46,56 +44,6 @@ export default class SceneView {
getAccordionBox: () => AccordionBox | null,
options: { tandem: Tandem } ) {

const objectNodeGroupTandem = options.tandem.createTandem( 'soccerBallNodeGroup' );

const objectNodesInputEnabledProperty = new BooleanProperty( true, {
tandem: objectNodeGroupTandem.createTandem( 'inputEnabledProperty' )
} );

const dragIndicatorArrowNode = new DragIndicatorArrowNode( {
tandem: options.tandem.createTandem( 'dragIndicatorArrowNode' ),
visible: false
} );

backObjectLayer.addChild( dragIndicatorArrowNode );

// TODO: https://github.com/phetsims/center-and-variability/issues/189 Move this to CAVScreenView
// TODO: Move parts to the model as appropriate https://github.com/phetsims/center-and-variability/issues/189
// TODO: When the selectedSceneModel changes (or any of its components change), check if we should show the arrow https://github.com/phetsims/center-and-variability/issues/189
const updateDragIndictatorVisible = () => {

// add the dragIndicatorArrowNode above the last object when it is added to the play area. if an object was
// moved before this happens, don't show the dragIndicatorArrowNode
if ( sceneModel.soccerBallCountProperty.value === sceneModel.maxKicksProperty.value &&
objectNodesInputEnabledProperty.value &&
_.every( sceneModel.getActiveSoccerBalls(), soccerBall => soccerBall.valueProperty.value !== null ) &&
!model.soccerBallHasBeenDraggedProperty.value &&
model.selectedSceneModelProperty.value === sceneModel ) {

const lastBall = sceneModel.getActiveSoccerBalls()[ sceneModel.getActiveSoccerBalls().length - 1 ];
const value = lastBall.valueProperty.value!;

dragIndicatorArrowNode.centerX = modelViewTransform.modelToViewX( value );

const dragIndicatorArrowNodeMargin = 6;

// calculate where the top object is
const topObjectPositionY = modelViewTransform.modelToViewY( 0 ) -
( sceneModel.getStackAtLocation( value ).length ) *
Math.abs( modelViewTransform.modelToViewDeltaY( CAVObjectType.SOCCER_BALL.radius ) ) * 2 -
dragIndicatorArrowNodeMargin;

dragIndicatorArrowNode.bottom = topObjectPositionY;
dragIndicatorArrowNode.visible = true;
}
else {
dragIndicatorArrowNode.visible = false;
}
};

model.soccerBallHasBeenDraggedProperty.link( updateDragIndictatorVisible );
model.maxKicksProperty.link( updateDragIndictatorVisible );
model.selectedSceneModelProperty.link( updateDragIndictatorVisible );

const soccerBallMap = new Map<SoccerBall, SoccerBallNode>();

Expand All @@ -105,7 +53,7 @@ export default class SceneView {
sceneModel.isVisibleProperty,
model.isShowingPlayAreaMedianProperty,
modelViewTransform,
objectNodesInputEnabledProperty, {
model.objectNodesInputEnabledProperty, {
tandem: options.tandem.createTandem( 'soccerBallNodes' ).createTandem( 'soccerBallNode' + index )
} );

Expand All @@ -125,8 +73,6 @@ export default class SceneView {

soccerBall.valueProperty.link( ( value, oldValue ) => {

updateDragIndictatorVisible();

// If the value changed from numeric to numeric, it must have been by user dragging it.
// It's simpler to have the listener here because in the model or drag listener, there is rounding/snapping
// And we only want to hide the indicator of the user dragged the ball a full tick mark
Expand Down

0 comments on commit 5370b5d

Please sign in to comment.