diff --git a/js/common/NumberPlayConstants.js b/js/common/NumberPlayConstants.js index 64322816..fc439378 100644 --- a/js/common/NumberPlayConstants.js +++ b/js/common/NumberPlayConstants.js @@ -9,6 +9,7 @@ define( require => { 'use strict'; // modules + const Dimension2 = require( 'DOT/Dimension2' ); const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); const PhetFont = require( 'SCENERY_PHET/PhetFont' ); @@ -67,7 +68,10 @@ define( require => { ORANGE_BACKGROUND: 'rgb( 255, 218, 176 )', PURPLE_BACKGROUND: 'rgb( 254, 202, 255 )', BLUE_BACKGROUND: 'rgb( 190, 232, 255 )', - BUCKET_BASE_COLOR: 'rgb( 100, 101, 162 )' + BUCKET_BASE_COLOR: 'rgb( 100, 101, 162 )', + + // misc TODO: when base classes exist, move bucket specs there + BUCKET_SIZE: new Dimension2( 100, 50 ) // in screen coordinates }; return numberPlay.register( 'NumberPlayConstants', NumberPlayConstants ); diff --git a/js/common/model/NumberPlayModel.js b/js/common/model/NumberPlayModel.js index b99eb586..81773a14 100644 --- a/js/common/model/NumberPlayModel.js +++ b/js/common/model/NumberPlayModel.js @@ -11,6 +11,7 @@ define( require => { // modules const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); const NumberPlayPlayArea = require( 'NUMBER_PLAY/common/model/NumberPlayPlayArea' ); + const OnesPlayArea = require( 'NUMBER_PLAY/common/model/OnesPlayArea' ); const NumberProperty = require( 'AXON/NumberProperty' ); const Range = require( 'DOT/Range' ); @@ -27,15 +28,27 @@ define( require => { range: new Range( 0, highestCount ) } ); + // @public (read-only) - the model for managing the play area in the OnesAccordionBox + this.onesPlayArea = new OnesPlayArea( this.currentNumberProperty ); + // @public (read-only) - the model for managing the play area in the ObjectsAccordionBox this.objectsPlayArea = new NumberPlayPlayArea( this.currentNumberProperty ); } + /** + * Steps the model. + * @param {number} dt + */ + step( dt ) { + this.onesPlayArea.step(); + } + /** * Resets the model. * @public */ reset() { + this.onesPlayArea.reset(); this.currentNumberProperty.reset(); this.objectsPlayArea.reset(); } diff --git a/js/common/model/NumberPlayPlayArea.js b/js/common/model/NumberPlayPlayArea.js index 7ccde34f..7fc1f382 100644 --- a/js/common/model/NumberPlayPlayArea.js +++ b/js/common/model/NumberPlayPlayArea.js @@ -22,7 +22,7 @@ define( require => { const Vector2 = require( 'DOT/Vector2' ); // constants - const BUCKET_SIZE = new Dimension2( 100, 50 ); // in screen coordinates + const BUCKET_SIZE = NumberPlayConstants.BUCKET_SIZE; const ANIMATION_SPEED = 200; // in screen coordinates per second const MAX_ANIMATION_TIME = 1; // in seconds @@ -73,6 +73,7 @@ define( require => { // if the current number changes, add or remove playObjects from the play area currentNumberProperty.link( ( currentNumber, previousNumber ) => { + console.log( currentNumber, previousNumber ); if ( currentNumber < this.playObjectsInPlayArea.lengthProperty.value ) { assert && assert( currentNumber < previousNumber ); diff --git a/js/common/model/OnesPlayArea.js b/js/common/model/OnesPlayArea.js new file mode 100644 index 00000000..79721c4b --- /dev/null +++ b/js/common/model/OnesPlayArea.js @@ -0,0 +1,155 @@ +// Copyright 2015-2019, University of Colorado Boulder + +/** + * Model for the Explore screen in Make a Ten. Includes the total, cues, and adding in initial numbers. This file was + * copied from make-a-ten/common/model/MakeATenCommonModel.js and make-a-ten/explore/model/MakeATenExploreModel.js and + * then modified by @chrisklus to be used in number-play. + * + * @author Sharfudeen Ashraf + * @author Chris Klusendorf (PhET Interactive Simulations) + */ +define( require => { + 'use strict'; + + // modules + const Bucket = require( 'PHETCOMMON/model/Bucket' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' ); + const ObservableArray = require( 'AXON/ObservableArray' ); + + class OnesPlayArea { + + /** + * @param {NumberProperty} currentNumberProperty + */ + constructor( currentNumberProperty ) { + + // @public (read-only) + this.bucket = new Bucket( { + baseColor: NumberPlayConstants.BUCKET_BASE_COLOR, + size: NumberPlayConstants.BUCKET_SIZE + } ); + + // @public {NumberProperty} - The total sum of the current numbers + this.sumProperty = currentNumberProperty; + + // @public {ObservableArray.} - Numbers in play that can be interacted with. + this.paperNumbers = new ObservableArray(); + + // @private {Function} - To be called when we need to recalculate the total + const calculateTotalListener = this.calculateTotal.bind( this ); + + this.paperNumbers.lengthProperty.link( calculateTotalListener ); + + // Listen to number changes of paper numbers + this.paperNumbers.addItemAddedListener( paperNumber => { + paperNumber.numberValueProperty.link( calculateTotalListener ); + } ); + this.paperNumbers.addItemRemovedListener( paperNumber => { + paperNumber.numberValueProperty.unlink( calculateTotalListener ); + } ); + } + + /** + * @param {number} dt + */ + step( dt ) { + + // Cap large dt values, which can occur when the tab containing + // the sim had been hidden and then re-shown + dt = Math.min( 0.1, dt ); + + for ( let i = 0; i < this.paperNumbers.length; i++ ) { + this.paperNumbers.get( i ).step( dt ); + } + + // Animate fading if necessary + // this.splitCue.step( dt ); + } + + /** + * Updates the total sum of the paper numbers. + * @private + */ + calculateTotal() { + let total = 0; + this.paperNumbers.forEach( function( paperNumber ) { + total += paperNumber.numberValueProperty.value; + } ); + this.sumProperty.value = total; + } + + /** + * Given two paper numbers, combine them (set one's value to the sum of their previous values, and remove the + * other). + * + * @param {Bounds2} availableModelBounds - Constrain the location to be inside these bounds + * @param {PaperNumber} draggedPaperNumber + * @param {PaperNumber} dropTargetNumber + */ + collapseNumberModels( availableModelBounds, draggedPaperNumber, dropTargetNumber ) { + const dropTargetNumberValue = dropTargetNumber.numberValueProperty.value; + const draggedNumberValue = draggedPaperNumber.numberValueProperty.value; + const newValue = dropTargetNumberValue + draggedNumberValue; + + let numberToRemove; + let numberToChange; + + // See https://github.com/phetsims/make-a-ten/issues/260 + if ( draggedPaperNumber.digitLength === dropTargetNumber.digitLength ) { + numberToRemove = draggedPaperNumber; + numberToChange = dropTargetNumber; + } + else { + // The larger number gets changed, the smaller one gets removed. + const droppingOnLarger = dropTargetNumberValue > draggedNumberValue; + numberToRemove = droppingOnLarger ? draggedPaperNumber : dropTargetNumber; + numberToChange = droppingOnLarger ? dropTargetNumber : draggedPaperNumber; + } + + // Apply changes + this.removePaperNumber( numberToRemove ); + numberToChange.changeNumber( newValue ); + numberToChange.setConstrainedDestination( availableModelBounds, numberToChange.positionProperty.value, false ); + } + + /** + * Add a PaperNumber to the model + * @public + * + * @param {PaperNumber} paperNumber + */ + addPaperNumber( paperNumber ) { + this.paperNumbers.push( paperNumber ); + } + + /** + * Remove a PaperNumber from the model + * @public + * + * @param {PaperNumber} paperNumber + */ + removePaperNumber( paperNumber ) { + this.paperNumbers.remove( paperNumber ); + } + + /** + * Remove all PaperNumbers from the model. + * @public + * + * @param {PaperNumber} paperNumber + */ + removeAllPaperNumbers() { + this.paperNumbers.clear(); + } + + /** + * @override + */ + reset() { + this.removeAllPaperNumbers(); + } + } + + return numberPlay.register( 'OnesPlayArea', OnesPlayArea ); +} ); diff --git a/js/common/view/NumberPlayScreenView.js b/js/common/view/NumberPlayScreenView.js index 8261b049..5f5b3d9b 100644 --- a/js/common/view/NumberPlayScreenView.js +++ b/js/common/view/NumberPlayScreenView.js @@ -119,7 +119,7 @@ define( require => { // create and add the OnesAccordionBox const onesAccordionBox = new OnesAccordionBox( - model.currentNumberProperty, + model.onesPlayArea, config.lowerAccordionBoxHeight, merge( { expandedProperty: this.onesAccordionBoxExpandedProperty }, config.onesAccordionBoxConfig ) ); diff --git a/js/common/view/OnesAccordionBox.js b/js/common/view/OnesAccordionBox.js index 3b8c6f48..5c3938d7 100644 --- a/js/common/view/OnesAccordionBox.js +++ b/js/common/view/OnesAccordionBox.js @@ -11,11 +11,15 @@ define( require => { // modules const AccordionBox = require( 'SUN/AccordionBox' ); + const Bounds2 = require( 'DOT/Bounds2' ); const merge = require( 'PHET_CORE/merge' ); + const ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' ); const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); const NumberPlayConstants = require( 'NUMBER_PLAY/common/NumberPlayConstants' ); + const OnesPlayAreaNode = require( 'NUMBER_PLAY/common/view/OnesPlayAreaNode' ); const Rectangle = require( 'SCENERY/nodes/Rectangle' ); const Text = require( 'SCENERY/nodes/Text' ); + const Vector2 = require( 'DOT/Vector2' ); // strings const onesString = require( 'string!NUMBER_PLAY/ones' ); @@ -23,20 +27,48 @@ define( require => { class OnesAccordionBox extends AccordionBox { /** - * @param {NumberProperty} currentNumberProperty + * @param {OnesPlayArea} onesPlayArea * @param {number} height - the height of this accordion box * @param {Object} [options] */ - constructor( currentNumberProperty, height, options ) { + constructor( onesPlayArea, height, config ) { - options = merge( { + config = merge( { titleNode: new Text( onesString, { font: NumberPlayConstants.ACCORDION_BOX_TITLE_FONT } ), - fill: NumberPlayConstants.PURPLE_BACKGROUND - }, NumberPlayConstants.ACCORDION_BOX_OPTIONS, options ); + fill: NumberPlayConstants.PURPLE_BACKGROUND, - const contentNode = new Rectangle( { rectHeight: height } ); + contentWidth: null, // {number} @required + }, NumberPlayConstants.ACCORDION_BOX_OPTIONS, config ); - super( contentNode, options ); + assert && assert( config.contentWidth, `contentWidth is required: ${config.contentWidth}`); + + const contentNode = new Rectangle( { + rectHeight: height, + rectWidth: config.contentWidth + } ); + + const playAreaMarginY = 15; + const playAreaViewBounds = new Bounds2( + contentNode.left, + contentNode.top, + contentNode.right, + contentNode.bottom - playAreaMarginY + ); + + const translateMVT = ModelViewTransform2.createSinglePointScaleInvertedYMapping( + Vector2.ZERO, + new Vector2( playAreaViewBounds.centerX, playAreaViewBounds.bottom ), + 1 + ); + + const onesPlayAreaNode = new OnesPlayAreaNode( + onesPlayArea, + playAreaViewBounds.dilatedX( -10 ), + translateMVT + ); + contentNode.addChild( onesPlayAreaNode ); + + super( contentNode, config ); } } diff --git a/js/common/view/OnesCreatorNode.js b/js/common/view/OnesCreatorNode.js new file mode 100644 index 00000000..ddb12406 --- /dev/null +++ b/js/common/view/OnesCreatorNode.js @@ -0,0 +1,116 @@ +// Copyright 2016-2019, University of Colorado Boulder + +/** + * Node that contains a 1, which can be clicked/dragged to create draggable paper numbers. This file was + * copied from make-a-ten/explore/view/ExplorePanel.js and then modified by @chrisklus to be used in number-play. + * + * @author Jonathan Olson + * @author Chris Klusendorf (PhET Interactive Simulations) + */ +define( require => { + 'use strict'; + + // modules + const BaseNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/BaseNumber' ); + const BaseNumberNode = require( 'MAKE_A_TEN/make-a-ten/common/view/BaseNumberNode' ); + const DerivedProperty = require( 'AXON/DerivedProperty' ); + const Node = require( 'SCENERY/nodes/Node' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); + const Vector2 = require( 'DOT/Vector2' ); + + class OnesCreatorNode extends Node { + + /** + * @param {OnesPlayAreaNode} playAreaNode + * @param {NumberProperty} sumProperty + * @param {Object} [options] - Passed to Node + */ + constructor( playAreaNode, sumProperty ) { + super(); + + assert && assert( sumProperty.range, `Range is required: ${sumProperty.range}` ); + const maxSum = sumProperty.range.max; + + // @private {MakeATenExploreScreenView} + this.playAreaNode = playAreaNode; + + function createTarget( place ) { + const numberValue = Math.pow( 10, place ); + const node = new Node( { + cursor: 'pointer', + // empirically determined stacking + children: [ new Vector2( -8, -8 ), new Vector2( 0, 0 ) ].map( function( offset ) { + const paperNode = new BaseNumberNode( new BaseNumber( 1, place ), 1 ); + paperNode.scale( 0.64, 0.55 ); + paperNode.translation = offset; + return paperNode; + } ) + } ); + node.touchArea = node.localBounds.dilatedX( 15 ).dilatedY( 5 ); + + // We need to be disabled if adding this number would increase the sum past the maximum sum. + new DerivedProperty( [ sumProperty ], function( sum ) { + return sum + numberValue <= maxSum; + } ).linkAttribute( node, 'visible' ); + + node.addInputListener( { + down: function( event ) { + if ( !event.canStartPress() ) { return; } + + // We want this relative to the screen view, so it is guaranteed to be the proper view coordinates. + const viewPosition = playAreaNode.globalToLocalPoint( event.pointer.point ); + const paperNumber = new PaperNumber( numberValue, new Vector2( 0, 0 ) ); + + // Once we have the number's bounds, we set the position so that our pointer is in the middle of the drag target. + paperNumber.setDestination( viewPosition.minus( paperNumber.getDragTargetOffset() ), false ); + + // Create and start dragging the new paper number node + playAreaNode.addAndDragNumber( event, paperNumber ); + } + } ); + + return node; + } + + // @private {Node} + this.oneTarget = createTarget( 0 ); + this.addChild( this.oneTarget ); + } + + /** + * Given a specified number of digits for a paper number, return the view coordinates of the closest matching + * target, so that it can animate back to this location. + * @public + * + * @param {number} digits + * @returns {Vector2} + */ + getOriginLocation( digits ) { + let target; + switch( digits ) { + case 1: + target = this.oneTarget; + break; + case 2: + target = this.tenTarget; + break; + case 3: + target = this.hundredTarget; + break; + default: + // Probably something big, no better place to send it + target = this.hundredTarget; + } + + // Trail to playAreaNode, not including the playAreaNode + let trail = this.playAreaNode.getUniqueLeafTrailTo( target ); + trail = trail.slice( 1, trail.length ); + + // Transformed to view coordinates + return trail.localToGlobalPoint( target.localBounds.center ); + } + } + + return numberPlay.register( 'OnesCreatorNode', OnesCreatorNode ); +} ); diff --git a/js/common/view/OnesPlayAreaNode.js b/js/common/view/OnesPlayAreaNode.js new file mode 100644 index 00000000..f7b0397b --- /dev/null +++ b/js/common/view/OnesPlayAreaNode.js @@ -0,0 +1,325 @@ +// Copyright 2015-2019, University of Colorado Boulder + +/** + * Play area node for the OnesAccordionBox. This file was copied from make-a-ten/common/view/MakeATenCommonView.js and + * make-a-ten/explore/view/MakeATenExploreScreenView.js and then modified by @chrisklus to be used in number-play. + * + * @author Sharfudeen Ashraf + * @author Chris Klusendorf (PhET Interactive Simulations) + */ +define( require => { + 'use strict'; + + // modules + const ArithmeticRules = require( 'MAKE_A_TEN/make-a-ten/common/model/ArithmeticRules' ); + const BucketFront = require( 'SCENERY_PHET/bucket/BucketFront' ); + const BucketHole = require( 'SCENERY_PHET/bucket/BucketHole' ); + const ClosestDragListener = require( 'SUN/ClosestDragListener' ); + const Node = require( 'SCENERY/nodes/Node' ); + const numberPlay = require( 'NUMBER_PLAY/numberPlay' ); + const OnesCreatorNode = require( 'NUMBER_PLAY/common/view/OnesCreatorNode' ); + const PaperNumber = require( 'MAKE_A_TEN/make-a-ten/common/model/PaperNumber' ); + const PaperNumberNode = require( 'MAKE_A_TEN/make-a-ten/common/view/PaperNumberNode' ); + const Property = require( 'AXON/Property' ); + const Rectangle = require( 'SCENERY/nodes/Rectangle' ); + const Vector2 = require( 'DOT/Vector2' ); + + class OnesPlayAreaNode extends Node { + + /** + * @param {OnesPlayArea} playArea + * @param {Bounds2} playAreaViewBounds + * @param {ModelViewTransform2} translateMVT + */ + constructor( playArea, playAreaViewBounds, translateMVT ) { + super(); + + // @private {Function} - Called with function( paperNumberNode ) on number splits + this.numberSplitListener = this.onNumberSplit.bind( this ); + + // @private {Function} - Called with function( paperNumberNode ) when a number begins to be interacted with. + this.numberInteractionListener = this.onNumberInteractionStarted.bind( this ); + + // @private {Function} - Called with function( paperNumber ) when a number finishes animation + this.numberAnimationFinishedListener = this.onNumberAnimationFinished.bind( this ); + + // @private {Function} - Called with function( paperNumber ) when a number finishes being dragged + this.numberDragFinishedListener = this.onNumberDragFinished.bind( this ); + + // @public {OnesPlayArea} + this.playArea = playArea; + + // @protected {Node} - Where all of the paper numbers are. NOTE: Subtypes need to add this as a child with the + // proper place in layering (this common view doesn't do that). + this.paperNumberLayerNode = new Node(); + + // @private {Function} + this.tryToCombineNumbersCallback = this.tryToCombineNumbers.bind( this ); + + // @private {Function} + this.addAndDragNumberCallback = this.addAndDragNumber.bind( this ); + + // @private {number} PaperNumber.id => {PaperNumberNode} - lookup map for efficiency + this.paperNumberNodeMap = {}; + + // @public {Property.} - The view coordinates where numbers can be dragged. Can update when the sim + // is resized. + this.availableViewBoundsProperty = new Property( playAreaViewBounds ); + + // @private {ClosestDragListener} - Handle touches nearby to the numbers, and interpret those as the proper drag. + this.closestDragListener = new ClosestDragListener( 30, 0 ); + const backgroundDragTarget = new Rectangle( playAreaViewBounds ); + backgroundDragTarget.addInputListener( this.closestDragListener ); + this.addChild( backgroundDragTarget ); + + const paperNumberAddedListener = this.onPaperNumberAdded.bind( this ); + const paperNumberRemovedListener = this.onPaperNumberRemoved.bind( this ); + + // Add nodes for every already-existing paper number + playArea.paperNumbers.forEach( paperNumberAddedListener ); + + // Add and remove nodes to match the playArea + playArea.paperNumbers.addItemAddedListener( paperNumberAddedListener ); + playArea.paperNumbers.addItemRemovedListener( paperNumberRemovedListener ); + + // Persistent, no need to unlink + this.availableViewBoundsProperty.lazyLink( function( availableViewBounds ) { + playArea.paperNumbers.forEach( function( paperNumber ) { + paperNumber.setConstrainedDestination( availableViewBounds, paperNumber.positionProperty.value ); + } ); + } ); + + // create and add the bucket back + const bucketHole = new BucketHole( playArea.bucket, translateMVT ); + this.addChild( bucketHole ); + + // @private {OnesCreatorNode} - Shows the 1 that can be dragged. + this.onesCreatorNode = new OnesCreatorNode( this, playArea.sumProperty ); + this.addChild( this.onesCreatorNode ); + + // create and add the bucket front + const bucketFrontNode = new BucketFront( playArea.bucket, translateMVT ); + bucketFrontNode.centerBottom = translateMVT.modelToViewPosition( playArea.bucket.position ); + bucketHole.center = bucketFrontNode.centerTop; + this.onesCreatorNode.centerBottom = bucketFrontNode.center; + this.addChild( bucketFrontNode ); + + this.addChild( this.paperNumberLayerNode ); + } + + /** + * Add a paper number to the playArea and immediately start dragging it with the provided event. + * @public + * + * @param {Event} event - The Scenery event that triggered this. + * @param {PaperNumber} paperNumber - The paper number to add and then drag + */ + addAndDragNumber( event, paperNumber ) { + + // Add it and lookup the related node. + this.playArea.addPaperNumber( paperNumber ); + + const paperNumberNode = this.findPaperNumberNode( paperNumber ); + paperNumberNode.startSyntheticDrag( event ); + } + + /** + * Creates and adds a PaperNumberNode. + * @public + * + * @param {PaperNumber} paperNumber + * @returns {PaperNumberNode} - The created node + */ + onPaperNumberAdded( paperNumber ) { + const paperNumberNode = new PaperNumberNode( paperNumber, this.availableViewBoundsProperty, + this.addAndDragNumberCallback, this.tryToCombineNumbersCallback ); + + this.paperNumberNodeMap[ paperNumberNode.paperNumber.id ] = paperNumberNode; + this.paperNumberLayerNode.addChild( paperNumberNode ); + paperNumberNode.attachListeners(); + + this.closestDragListener.addDraggableItem( paperNumberNode ); + + // Add listeners + paperNumberNode.splitEmitter.addListener( this.numberSplitListener ); + paperNumberNode.interactionStartedEmitter.addListener( this.numberInteractionListener ); + paperNumber.endAnimationEmitter.addListener( this.numberAnimationFinishedListener ); + paperNumber.endDragEmitter.addListener( this.numberDragFinishedListener ); + } + + /** + * Handles removing the relevant PaperNumberNode + * @public + * + * @param {PaperNumber} paperNumber + */ + onPaperNumberRemoved( paperNumber ) { + const paperNumberNode = this.findPaperNumberNode( paperNumber ); + + // Remove listeners + paperNumber.endDragEmitter.removeListener( this.numberDragFinishedListener ); + paperNumber.endAnimationEmitter.removeListener( this.numberAnimationFinishedListener ); + paperNumberNode.interactionStartedEmitter.removeListener( this.numberInteractionListener ); + paperNumberNode.splitEmitter.removeListener( this.numberSplitListener ); + + delete this.paperNumberNodeMap[ paperNumberNode.paperNumber.id ]; + this.paperNumberLayerNode.removeChild( paperNumberNode ); + paperNumberNode.detachListeners(); + + this.closestDragListener.removeDraggableItem( paperNumberNode ); + } + + /** + * Given a {PaperNumber}, find our current display ({PaperNumberNode}) of it. + * @public + * + * @param {PaperNumber} paperNumber + * @returns {PaperNumberNode} + */ + findPaperNumberNode( paperNumber ) { + const result = this.paperNumberNodeMap[ paperNumber.id ]; + assert && assert( result, 'Did not find matching Node' ); + return result; + } + + /** + * When the user drops a paper number they were dragging, see if it can combine with any other nearby paper numbers. + * @public + * + * @param {PaperNumber} draggedPaperNumber + */ + tryToCombineNumbers( draggedPaperNumber ) { + const draggedNode = this.findPaperNumberNode( draggedPaperNumber ); + const draggedNumberValue = draggedPaperNumber.numberValueProperty.value; + const allPaperNumberNodes = this.paperNumberLayerNode.children; + const droppedNodes = draggedNode.findAttachableNodes( allPaperNumberNodes ); + + // Check them in reverse order (the one on the top should get more priority) + droppedNodes.reverse(); + + for ( let i = 0; i < droppedNodes.length; i++ ) { + const droppedNode = droppedNodes[ i ]; + const droppedPaperNumber = droppedNode.paperNumber; + const droppedNumberValue = droppedPaperNumber.numberValueProperty.value; + + if ( ArithmeticRules.canAddNumbers( draggedNumberValue, droppedNumberValue ) ) { + this.playArea.collapseNumberModels( this.availableViewBoundsProperty.value, draggedPaperNumber, droppedPaperNumber ); + return; // A bit weird, but no need to relayer or try combining with others? + } + else { + assert && assert( false, 'repelling numbers should not be possible'); + } + } + + // if the dragged number is larger than the node below it (dropped node), reorder + // them in a way to bring small number on the top. see issue #39 + for ( let i = 0; i < allPaperNumberNodes.length; i++ ) { + if ( allPaperNumberNodes[ i ] === draggedNode ) { + continue; + } + + if ( allPaperNumberNodes[ i ].bounds.intersectsBounds( draggedNode.bounds ) ) { + if ( draggedNode.bounds.width > allPaperNumberNodes[ i ].bounds.width ) { + allPaperNumberNodes[ i ].moveToFront(); + } + } + } + } + + /** + * Whether the paper number is predominantly over the explore panel (should be collected). + * @private + * + * @param {PaperNumber} paperNumber + * @returns {boolean} + */ + isNumberInReturnZone( paperNumber ) { + + // Compute the local point on the number that would need to go into the return zone. + // This point is a bit farther down than the exact center, as it was annoying to "miss" the return zone + // slightly by being too high (while the mouse WAS in the return zone). + const localBounds = paperNumber.getLocalBounds(); + const localReturnPoint = localBounds.center.plus( localBounds.centerBottom ).dividedScalar( 2 ); + + // And the bounds of our panel + const panelBounds = this.onesCreatorNode.bounds.withMaxY( this.availableViewBoundsProperty.value.bottom ); + + // View coordinate of our return point + const paperCenter = paperNumber.positionProperty.value.plus( localReturnPoint ); + + return panelBounds.containsPoint( paperCenter ); + } + + /** + * Called when a paper number node is split. + * @private + * + * @param {PaperNumberNode} paperNumberNode + */ + onNumberSplit( paperNumberNode ) { + // this.playArea.splitCue.triggerFade(); + } + + /** + * Called when a paper number node starts being interacted with. + * @private + * + * @param {PaperNumberNode} paperNumberNode + */ + onNumberInteractionStarted( paperNumberNode ) { + const paperNumber = paperNumberNode.paperNumber; + if ( paperNumber.numberValueProperty.value > 1 ) { + // this.playArea.splitCue.attachToNumber( paperNumber ); + } + } + + /** + * Called when a paper number has finished animating to its destination. + * @private + * + * @param {PaperNumber} paperNumber + */ + onNumberAnimationFinished( paperNumber ) { + + // If it animated to the return zone, it's probably split and meant to be returned. + if ( this.isNumberInReturnZone( paperNumber ) ) { + this.playArea.removePaperNumber( paperNumber ); + } + } + + /** + * Called when a paper number has finished being dragged. + * @private + * + * @param {PaperNumber} paperNumber + */ + onNumberDragFinished( paperNumber ) { + + // Return it to the panel if it's been dropped in the panel. + if ( this.isNumberInReturnZone( paperNumber ) ) { + const baseNumbers = paperNumber.baseNumbers; + + // Split it into a PaperNumber for each of its base numbers, and animate them to their targets in the + // explore panel. + for ( let i = baseNumbers.length - 1; i >= 0; i-- ) { + const baseNumber = baseNumbers[ i ]; + const basePaperNumber = new PaperNumber( baseNumber.numberValue, paperNumber.positionProperty.value ); + + // Set its destination to the proper target (with the offset so that it will disappear once centered). + let targetPosition = this.onesCreatorNode.getOriginLocation( baseNumber.digitLength ); + const paperCenterOffset = new PaperNumber( baseNumber.numberValue, new Vector2( 0, 0 ) ).getLocalBounds().center; + targetPosition = targetPosition.minus( paperCenterOffset ); + basePaperNumber.setDestination( targetPosition, true ); + + // Add the new base paper number + this.playArea.addPaperNumber( basePaperNumber ); + } + + // Remove the original paper number (as we have added its components). + this.playArea.removePaperNumber( paperNumber ); + } + } + } + + return numberPlay.register( 'OnesPlayAreaNode', OnesPlayAreaNode ); +} ); diff --git a/js/number-play-config.js b/js/number-play-config.js index 3f0f1cf0..4dcbc7ff 100644 --- a/js/number-play-config.js +++ b/js/number-play-config.js @@ -31,6 +31,7 @@ require.config( { DOT: '../../dot/js', JOIST: '../../joist/js', KITE: '../../kite/js', + MAKE_A_TEN: '../../make-a-ten/js', NUMBER_PLAY: '.', PHETCOMMON: '../../phetcommon/js', PHET_CORE: '../../phet-core/js', diff --git a/js/ten/TenScreen.js b/js/ten/TenScreen.js index 3a27f558..d84347cf 100644 --- a/js/ten/TenScreen.js +++ b/js/ten/TenScreen.js @@ -54,7 +54,8 @@ define( require => { }, onesAccordionBoxConfig: { minWidth: NumberPlayConstants.TEN_LOWER_ACCORDION_BOX_WIDTH, - maxWidth: NumberPlayConstants.TEN_LOWER_ACCORDION_BOX_WIDTH + maxWidth: NumberPlayConstants.TEN_LOWER_ACCORDION_BOX_WIDTH, + contentWidth: 370, // empirically determined }, objectsAccordionBoxConfig: { minWidth: NumberPlayConstants.TEN_LOWER_ACCORDION_BOX_WIDTH, diff --git a/js/twenty/TwentyScreen.js b/js/twenty/TwentyScreen.js index 29c99470..5b4c848b 100644 --- a/js/twenty/TwentyScreen.js +++ b/js/twenty/TwentyScreen.js @@ -54,7 +54,8 @@ define( require => { }, onesAccordionBoxConfig: { minWidth: NumberPlayConstants.TWENTY_ONES_ACCORDION_BOX_WIDTH, - maxWidth: NumberPlayConstants.TWENTY_ONES_ACCORDION_BOX_WIDTH + maxWidth: NumberPlayConstants.TWENTY_ONES_ACCORDION_BOX_WIDTH, + contentWidth: 492 // empirically determined }, objectsAccordionBoxConfig: { minWidth: NumberPlayConstants.TWENTY_OBJECTS_ACCORDION_BOX_WIDTH, diff --git a/package.json b/package.json index 3f1423ff..04dd0f82 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "phet": { "requirejsNamespace": "NUMBER_PLAY", "phetLibs": [ + "make-a-ten", "twixt" ], "simulation": true,