diff --git a/js/common/NumberPairsConstants.ts b/js/common/NumberPairsConstants.ts index 9f381e3..a4a5c83 100644 --- a/js/common/NumberPairsConstants.ts +++ b/js/common/NumberPairsConstants.ts @@ -38,7 +38,7 @@ const NumberPairsConstants = { COUNTING_AREA_CORNER_RADIUS: 5, COUNTING_AREA_BOUNDS: new Bounds2( ScreenView.DEFAULT_LAYOUT_BOUNDS.minX + COUNTING_AREA_X_MARGIN, COUNTING_AREA_MIN_Y, ScreenView.DEFAULT_LAYOUT_BOUNDS.maxX - COUNTING_AREA_X_MARGIN, COUNTING_AREA_MIN_Y + 340 ), - + COUNTING_AREA_INNER_MARGIN: 5, TEN_TOTAL_RANGE: TEN_TOTAL_RANGE, TEN_NUMBER_LINE_RANGE: new Range( 0, TEN_TOTAL_RANGE.max ), TWENTY_TOTAL_RANGE: TWENTY_TOTAL_RANGE, diff --git a/js/common/model/NumberPairsModel.ts b/js/common/model/NumberPairsModel.ts index b2856da..05bf661 100644 --- a/js/common/model/NumberPairsModel.ts +++ b/js/common/model/NumberPairsModel.ts @@ -29,6 +29,7 @@ import numberPairs from '../../numberPairs.js'; import NumberPairsConstants from '../NumberPairsConstants.js'; import CountingObject, { AddendType, KITTEN_PANEL_WIDTH } from './CountingObject.js'; import RepresentationType from './RepresentationType.js'; +import Dimension2 from '../../../../dot/js/Dimension2.js'; type AnimationTarget = { property: Property; @@ -228,15 +229,75 @@ export default class NumberPairsModel implements TModel { } ); } + private getValidDropPoint( invalidBounds: Bounds2, proposedPoint: Vector2, allowedDirection: 'upLeft' | 'upRight' ): Vector2 { + + if ( invalidBounds.containsPoint( proposedPoint ) ) { + const closestXEdge = allowedDirection === 'upLeft' ? invalidBounds.minX : invalidBounds.maxX; + const closestYEdge = invalidBounds.minY; + + if ( Math.abs( closestXEdge - proposedPoint.x ) < Math.abs( closestYEdge - proposedPoint.y ) ) { + return new Vector2( closestXEdge, proposedPoint.y ); + } + else { + return new Vector2( proposedPoint.x, closestYEdge ); + } + } + else { + return proposedPoint; + } + } + + private sendToValidDropPoint( countingObject: CountingObject, positionPropertyType: 'attribute' | 'location' ): Vector2 { + const countingAreaBounds = NumberPairsConstants.COUNTING_AREA_BOUNDS; + const positionProperty = positionPropertyType === 'attribute' ? countingObject.attributePositionProperty : + countingObject.locationPositionProperty; + + const addendVisibleButtonDimension = new Dimension2( + NumberPairsConstants.RECTANGULAR_PUSH_BUTTON_OPTIONS.size.width + NumberPairsConstants.COUNTING_AREA_INNER_MARGIN + DROP_ZONE_MARGIN, + NumberPairsConstants.RECTANGULAR_PUSH_BUTTON_OPTIONS.size.height + NumberPairsConstants.COUNTING_AREA_INNER_MARGIN + DROP_ZONE_MARGIN + ); + const leftAddendVisibleButtonBounds = new Bounds2( + countingAreaBounds.minX, + countingAreaBounds.maxY - addendVisibleButtonDimension.height, + countingAreaBounds.minX + addendVisibleButtonDimension.width, + countingAreaBounds.maxY ); + const invalidDropBounds = positionPropertyType === 'attribute' ? [ leftAddendVisibleButtonBounds ] : + [ + leftAddendVisibleButtonBounds, + leftAddendVisibleButtonBounds.shiftedX( countingAreaBounds.width - addendVisibleButtonDimension.width ) + ]; + + let validDropPoint = positionProperty.value; + invalidDropBounds.forEach( ( bounds, i ) => { + + // The first bounds is on the left side of the counting area + const direction = i === 0 ? 'upRight' : 'upLeft'; + validDropPoint = this.getValidDropPoint( bounds, validDropPoint, direction ); + } ); + + const animation = new Animation( { + duration: 0.4, + targets: [ { + property: positionProperty, + to: validDropPoint + } ] + } ); + animation.start(); + return validDropPoint; + } + + /** * Animates the dropped counting object and any overlapping objects to the closest boundary point of the drop zone. * @param droppedCountingObject * @param positionPropertyType + * + * // TODO: We still need to handle when a point is calculated to be outside of the Counting Area bounds. */ public dropCountingObject( droppedCountingObject: CountingObject, positionPropertyType: 'attribute' | 'location' ): void { - const dropZoneBounds = positionPropertyType === 'attribute' ? - this.getDropZoneBounds( droppedCountingObject.attributePositionProperty.value ) : - this.getDropZoneBounds( droppedCountingObject.locationPositionProperty.value ); + + const countingObjectValidDropPoint = this.sendToValidDropPoint( droppedCountingObject, positionPropertyType ); + const dropZoneBounds = this.getDropZoneBounds( countingObjectValidDropPoint ); const activeCountingObjects = this.countingObjects.filter( countingObject => countingObject.addendTypeProperty.value !== AddendType.INACTIVE && countingObject !== droppedCountingObject ); diff --git a/js/common/view/CountingAreaNode.ts b/js/common/view/CountingAreaNode.ts index cdd4a11..4085cb7 100644 --- a/js/common/view/CountingAreaNode.ts +++ b/js/common/view/CountingAreaNode.ts @@ -30,7 +30,7 @@ type SelfOptions = { type CountingAreaNodeOptions = SelfOptions & StrictOmit & PickRequired; export const COUNTING_AREA_LINE_WIDTH = 1.5; -export const COUNTING_AREA_MARGIN = 5; +export const COUNTING_AREA_MARGIN = NumberPairsConstants.COUNTING_AREA_INNER_MARGIN; export default class CountingAreaNode extends Node { public constructor(