From c2a524e3bc0b5c713d233fadd93ade1ba12620d0 Mon Sep 17 00:00:00 2001 From: samreid Date: Thu, 27 Feb 2020 11:38:07 -0700 Subject: [PATCH] Add repo to migration list, see https://github.com/phetsims/chipper/issues/875 --- area-builder_en.html | 47 +- images/explore-icon_png.js | 5 + images/game-icon_png.js | 5 + js/area-builder-config.js | 51 - js/area-builder-main.js | 52 +- js/area-builder-strings.js | 14 + js/areaBuilder.js | 8 +- js/common/AreaBuilderQueryParameters.js | 18 +- js/common/AreaBuilderSharedConstants.js | 68 +- js/common/model/MovableShape.js | 276 ++- js/common/model/PerimeterShape.js | 252 ++- js/common/model/ShapePlacementBoard.js | 1838 +++++++++--------- js/common/view/AreaBuilderControlPanel.js | 137 +- js/common/view/AreaBuilderIconFactory.js | 288 ++- js/common/view/DimensionsIcon.js | 104 +- js/common/view/Grid.js | 65 +- js/common/view/GridIcon.js | 113 +- js/common/view/PerimeterShapeNode.js | 429 ++-- js/common/view/ShapeCreatorNode.js | 309 ++- js/common/view/ShapeNode.js | 279 ++- js/common/view/ShapePlacementBoardNode.js | 133 +- js/explore/AreaBuilderExploreScreen.js | 82 +- js/explore/model/AreaBuilderExploreModel.js | 344 ++-- js/explore/view/AreaAndPerimeterDisplay.js | 154 +- js/explore/view/AreaBuilderExploreView.js | 182 +- js/explore/view/BoardDisplayModePanel.js | 145 +- js/explore/view/ExploreNode.js | 300 ++- js/game/AreaBuilderGameScreen.js | 80 +- js/game/model/AreaBuilderChallengeFactory.js | 1788 +++++++++-------- js/game/model/AreaBuilderGameChallenge.js | 345 ++-- js/game/model/AreaBuilderGameModel.js | 604 +++--- js/game/model/BuildSpec.js | 162 +- js/game/model/GameState.js | 32 +- js/game/model/QuizGameModel.js | 406 ++-- js/game/view/AreaBuilderGameView.js | 1675 ++++++++-------- js/game/view/AreaBuilderScoreboard.js | 156 +- js/game/view/ColorProportionsPrompt.js | 186 +- js/game/view/FeedbackWindow.js | 116 +- js/game/view/FractionNode.js | 124 +- js/game/view/GameIconFactory.js | 300 ++- js/game/view/GameInfoBanner.js | 368 ++-- js/game/view/StartGameLevelNode.js | 241 ++- js/game/view/YouBuiltWindow.js | 242 ++- js/game/view/YouEnteredWindow.js | 84 +- 44 files changed, 6237 insertions(+), 6370 deletions(-) create mode 100644 images/explore-icon_png.js create mode 100644 images/game-icon_png.js delete mode 100644 js/area-builder-config.js create mode 100644 js/area-builder-strings.js diff --git a/area-builder_en.html b/area-builder_en.html index 9e5d933..15fee9e 100644 --- a/area-builder_en.html +++ b/area-builder_en.html @@ -17,6 +17,35 @@ \ No newline at end of file diff --git a/images/explore-icon_png.js b/images/explore-icon_png.js new file mode 100644 index 0000000..a223147 --- /dev/null +++ b/images/explore-icon_png.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +var img = new Image(); +window.phetImages.push( img ); +img.src = 'data:image/png;base64,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'; +export default img; diff --git a/images/game-icon_png.js b/images/game-icon_png.js new file mode 100644 index 0000000..499a52a --- /dev/null +++ b/images/game-icon_png.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +var img = new Image(); +window.phetImages.push( img ); +img.src = 'data:image/png;base64,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'; +export default img; diff --git a/js/area-builder-config.js b/js/area-builder-config.js deleted file mode 100644 index 3e0e734..0000000 --- a/js/area-builder-config.js +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2014-2019, University of Colorado Boulder - -/* - * IMPORTANT: This file was auto-generated by "grunt generate-config". Please do not modify this directly. Instead - * please modify area-builder/package.json to control dependencies. - * - * RequireJS configuration file for the area-builder sim. - * Paths are relative to the location of this file. - */ - -require.config( { - - deps: [ 'area-builder-main' ], - - paths: { - - // Third-party libs - text: '../../sherpa/lib/text-2.0.12', - - // PhET plugins - sound: '../../chipper/js/requirejs-plugins/sound', - image: '../../chipper/js/requirejs-plugins/image', - mipmap: '../../chipper/js/requirejs-plugins/mipmap', - string: '../../chipper/js/requirejs-plugins/string', - ifphetio: '../../chipper/js/requirejs-plugins/ifphetio', - - // PhET libs, uppercase names to identify them in require.js imports. - // IMPORTANT: DO NOT modify. This file is auto-generated. See documentation at the top. - AREA_BUILDER: '.', - AXON: '../../axon/js', - BRAND: '../../brand/' + phet.chipper.brand + '/js', - DOT: '../../dot/js', - JOIST: '../../joist/js', - KITE: '../../kite/js', - PHETCOMMON: '../../phetcommon/js', - PHET_CORE: '../../phet-core/js', - PHET_IO: '../../phet-io/js', - REPOSITORY: '..', - SCENERY: '../../scenery/js', - SCENERY_PHET: '../../scenery-phet/js', - SUN: '../../sun/js', - TAMBO: '../../tambo/js', - TANDEM: '../../tandem/js', - TWIXT: '../../twixt/js', - UTTERANCE_QUEUE: '../../utterance-queue/js', - VEGAS: '../../vegas/js' - }, - - // Cache busting is applied by default, but can be disabled via ?cacheBust=false, see initialize-globals.js - urlArgs: phet.chipper.getCacheBustArgs() -} ); diff --git a/js/area-builder-main.js b/js/area-builder-main.js index 85e1c3d..60c188b 100644 --- a/js/area-builder-main.js +++ b/js/area-builder-main.js @@ -5,37 +5,33 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const AreaBuilderExploreScreen = require( 'AREA_BUILDER/explore/AreaBuilderExploreScreen' ); - const AreaBuilderGameScreen = require( 'AREA_BUILDER/game/AreaBuilderGameScreen' ); - const Sim = require( 'JOIST/Sim' ); - const SimLauncher = require( 'JOIST/SimLauncher' ); - const Tandem = require( 'TANDEM/Tandem' ); +import Sim from '../../joist/js/Sim.js'; +import SimLauncher from '../../joist/js/SimLauncher.js'; +import Tandem from '../../tandem/js/Tandem.js'; +import areaBuilderStrings from './area-builder-strings.js'; +import AreaBuilderExploreScreen from './explore/AreaBuilderExploreScreen.js'; +import AreaBuilderGameScreen from './game/AreaBuilderGameScreen.js'; - // strings - const areaBuilderTitleString = require( 'string!AREA_BUILDER/area-builder.title' ); +const areaBuilderTitleString = areaBuilderStrings[ 'area-builder' ].title; - // constants - const tandem = Tandem.ROOT; +// constants +const tandem = Tandem.ROOT; - const simOptions = { - credits: { - leadDesign: 'Karina K. R. Hensberry', - softwareDevelopment: 'John Blanco', - team: 'Bryce Gruneich, Amanda McGarry, Ariel Paul, Kathy Perkins, Beth Stade', - qualityAssurance: 'Steele Dalton, Amanda Davis, Oliver Nix, Oliver Orejola, Arnab Purkayastha, ' + - 'Amy Rouinfar, Bryan Yoelin' - } - }; +const simOptions = { + credits: { + leadDesign: 'Karina K. R. Hensberry', + softwareDevelopment: 'John Blanco', + team: 'Bryce Gruneich, Amanda McGarry, Ariel Paul, Kathy Perkins, Beth Stade', + qualityAssurance: 'Steele Dalton, Amanda Davis, Oliver Nix, Oliver Orejola, Arnab Purkayastha, ' + + 'Amy Rouinfar, Bryan Yoelin' + } +}; - SimLauncher.launch( function() { - // create and start the sim - new Sim( areaBuilderTitleString, [ - new AreaBuilderExploreScreen( tandem.createTandem( 'exploreScreen' ) ), - new AreaBuilderGameScreen( tandem.createTandem( 'gameScreen' ) ) - ], simOptions ).start(); - } ); +SimLauncher.launch( function() { + // create and start the sim + new Sim( areaBuilderTitleString, [ + new AreaBuilderExploreScreen( tandem.createTandem( 'exploreScreen' ) ), + new AreaBuilderGameScreen( tandem.createTandem( 'gameScreen' ) ) + ], simOptions ).start(); } ); \ No newline at end of file diff --git a/js/area-builder-strings.js b/js/area-builder-strings.js new file mode 100644 index 0000000..bece55c --- /dev/null +++ b/js/area-builder-strings.js @@ -0,0 +1,14 @@ +// Copyright 2020, University of Colorado Boulder + +/** + * Auto-generated from modulify, DO NOT manually modify. + */ + +import getStringModule from '../../chipper/js/getStringModule.js'; +import areaBuilder from './areaBuilder.js'; + +const areaBuilderStrings = getStringModule( 'AREA_BUILDER' ); + +areaBuilder.register( 'areaBuilderStrings', areaBuilderStrings ); + +export default areaBuilderStrings; diff --git a/js/areaBuilder.js b/js/areaBuilder.js index 9c7837b..5b6b980 100644 --- a/js/areaBuilder.js +++ b/js/areaBuilder.js @@ -5,11 +5,7 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const Namespace = require( 'PHET_CORE/Namespace' ); +import Namespace from '../../phet-core/js/Namespace.js'; - return new Namespace( 'areaBuilder' ); -} ); \ No newline at end of file +export default new Namespace( 'areaBuilder' ); \ No newline at end of file diff --git a/js/common/AreaBuilderQueryParameters.js b/js/common/AreaBuilderQueryParameters.js index 91f6a1e..265fd8d 100644 --- a/js/common/AreaBuilderQueryParameters.js +++ b/js/common/AreaBuilderQueryParameters.js @@ -5,19 +5,15 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); +import areaBuilder from '../areaBuilder.js'; - const AreaBuilderQueryParameters = QueryStringMachine.getAll( { +const AreaBuilderQueryParameters = QueryStringMachine.getAll( { - // fill the shape placement boards on the 'Explore' screen during startup, useful for testing - prefillBoards: { type: 'flag' } - } ); + // fill the shape placement boards on the 'Explore' screen during startup, useful for testing + prefillBoards: { type: 'flag' } +} ); - areaBuilder.register( 'AreaBuilderQueryParameters', AreaBuilderQueryParameters ); +areaBuilder.register( 'AreaBuilderQueryParameters', AreaBuilderQueryParameters ); - return AreaBuilderQueryParameters; -} ); +export default AreaBuilderQueryParameters; \ No newline at end of file diff --git a/js/common/AreaBuilderSharedConstants.js b/js/common/AreaBuilderSharedConstants.js index 28d78db..0efe38a 100644 --- a/js/common/AreaBuilderSharedConstants.js +++ b/js/common/AreaBuilderSharedConstants.js @@ -5,50 +5,46 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const Bounds2 = require( 'DOT/Bounds2' ); +import Bounds2 from '../../../dot/js/Bounds2.js'; +import areaBuilderStrings from '../area-builder-strings.js'; +import areaBuilder from '../areaBuilder.js'; - // strings - const invalidValueString = require( 'string!AREA_BUILDER/invalidValue' ); +const invalidValueString = areaBuilderStrings.invalidValue; - const AreaBuilderSharedConstants = { +const AreaBuilderSharedConstants = { - // layout bounds used throughout the simulation for laying out the screens - LAYOUT_BOUNDS: new Bounds2( 0, 0, 768, 464 ), + // layout bounds used throughout the simulation for laying out the screens + LAYOUT_BOUNDS: new Bounds2( 0, 0, 768, 464 ), - // colors used for the various shapes - GREENISH_COLOR: '#33E16E', - DARK_GREEN_COLOR: '#1A7137', - PURPLISH_COLOR: '#9D87C9', - DARK_PURPLE_COLOR: '#634F8C', - ORANGISH_COLOR: '#FFA64D', - ORANGE_BROWN_COLOR: '#A95327', - PALE_BLUE_COLOR: '#5DB9E7', - DARK_BLUE_COLOR: '#277DA9', - PINKISH_COLOR: '#E88DC9', - PURPLE_PINK_COLOR: '#AA548D', - PERIMETER_DARKEN_FACTOR: 0.6, // The amount that the perimeter colors are darkened from the main shape color + // colors used for the various shapes + GREENISH_COLOR: '#33E16E', + DARK_GREEN_COLOR: '#1A7137', + PURPLISH_COLOR: '#9D87C9', + DARK_PURPLE_COLOR: '#634F8C', + ORANGISH_COLOR: '#FFA64D', + ORANGE_BROWN_COLOR: '#A95327', + PALE_BLUE_COLOR: '#5DB9E7', + DARK_BLUE_COLOR: '#277DA9', + PINKISH_COLOR: '#E88DC9', + PURPLE_PINK_COLOR: '#AA548D', + PERIMETER_DARKEN_FACTOR: 0.6, // The amount that the perimeter colors are darkened from the main shape color - // velocity at which animated elements move - ANIMATION_SPEED: 200, // In screen coordinates per second + // velocity at which animated elements move + ANIMATION_SPEED: 200, // In screen coordinates per second - // various other constants - BACKGROUND_COLOR: 'rgb( 225, 255, 255 )', - CONTROL_PANEL_BACKGROUND_COLOR: 'rgb( 254, 241, 233 )', - RESET_BUTTON_RADIUS: 22, - CONTROLS_INSET: 15, + // various other constants + BACKGROUND_COLOR: 'rgb( 225, 255, 255 )', + CONTROL_PANEL_BACKGROUND_COLOR: 'rgb( 254, 241, 233 )', + RESET_BUTTON_RADIUS: 22, + CONTROLS_INSET: 15, - UNIT_SQUARE_LENGTH: 32, // In screen coordinates, used in several places + UNIT_SQUARE_LENGTH: 32, // In screen coordinates, used in several places - // string used to indicate an invalid value for area and perimeter - INVALID_VALUE: invalidValueString - }; + // string used to indicate an invalid value for area and perimeter + INVALID_VALUE: invalidValueString +}; - areaBuilder.register( 'AreaBuilderSharedConstants', AreaBuilderSharedConstants ); +areaBuilder.register( 'AreaBuilderSharedConstants', AreaBuilderSharedConstants ); - return AreaBuilderSharedConstants; -} ); \ No newline at end of file +export default AreaBuilderSharedConstants; \ No newline at end of file diff --git a/js/common/model/MovableShape.js b/js/common/model/MovableShape.js index 1f20130..f87c158 100644 --- a/js/common/model/MovableShape.js +++ b/js/common/model/MovableShape.js @@ -5,157 +5,153 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Color = require( 'SCENERY/util/Color' ); - const Emitter = require( 'AXON/Emitter' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Property = require( 'AXON/Property' ); - const Shape = require( 'KITE/Shape' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const FADE_RATE = 2; // proportion per second - /** - * @param {Shape} shape - * @param {Color || string} color - * @param {Vector2} initialPosition - * @constructor - */ - function MovableShape( shape, color, initialPosition ) { - const self = this; - - // Property that indicates where in model space the upper left corner of this shape is. In general, this should - // not be set directly outside of this type, and should only be manipulated through the methods defined below. - this.positionProperty = new Property( initialPosition ); - - // Flag that tracks whether the user is dragging this shape around. Should be set externally ; generally by the a - // view node. - this.userControlledProperty = new Property( false ); - - // Flag that indicates whether this element is animating from one position to another ; should not be set externally. - this.animatingProperty = new Property( false, { - reentrant: true - } ); - - // Value that indicates how faded out this shape is. This is used as part of a feature where shapes can fade - // out. Once fade has started ; it doesn't stop until it is fully faded ; i.e. the value is 1. This should not be - // set externally. - this.fadeProportionProperty = new Property( 0 ); - - // A flag that indicates whether this individual shape should become invisible when it is done animating. This - // is generally used in cases where it becomes part of a larger composite shape that is depicted instead. - this.invisibleWhenStillProperty = new Property( true ); - - // Destination is used for animation, and should be set through accessor methods only. - this.destination = initialPosition.copy(); // @private - - // Emit an event whenever this shape returns to its original position. - this.returnedToOriginEmitter = new Emitter(); - this.positionProperty.lazyLink( function( position ) { - if ( position.equals( initialPosition ) ) { - self.returnedToOriginEmitter.emit(); - } - } ); +import Emitter from '../../../../axon/js/Emitter.js'; +import Property from '../../../../axon/js/Property.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; - // Non-dynamic attributes - this.shape = shape; // @public, read only - this.color = Color.toColor( color ); // @public +// constants +const FADE_RATE = 2; // proportion per second - // Internal vars - this.fading = false; // @private - } +/** + * @param {Shape} shape + * @param {Color || string} color + * @param {Vector2} initialPosition + * @constructor + */ +function MovableShape( shape, color, initialPosition ) { + const self = this; - areaBuilder.register( 'MovableShape', MovableShape ); + // Property that indicates where in model space the upper left corner of this shape is. In general, this should + // not be set directly outside of this type, and should only be manipulated through the methods defined below. + this.positionProperty = new Property( initialPosition ); - return inherit( Object, MovableShape, { + // Flag that tracks whether the user is dragging this shape around. Should be set externally ; generally by the a + // view node. + this.userControlledProperty = new Property( false ); - step: function( dt ) { - if ( !this.userControlledProperty.get() ) { + // Flag that indicates whether this element is animating from one position to another ; should not be set externally. + this.animatingProperty = new Property( false, { + reentrant: true + } ); - // perform any animation - const currentPosition = this.positionProperty.get(); - const distanceToDestination = currentPosition.distance( this.destination ); - if ( distanceToDestination > dt * AreaBuilderSharedConstants.ANIMATION_SPEED ) { - // Move a step toward the destination. - const stepAngle = Math.atan2( this.destination.y - currentPosition.y, this.destination.x - currentPosition.x ); - const stepVector = Vector2.createPolar( AreaBuilderSharedConstants.ANIMATION_SPEED * dt, stepAngle ); - this.positionProperty.set( currentPosition.plus( stepVector ) ); - } - else if ( this.animatingProperty.get() ) { - // Less than one time step away, so just go to the destination. - this.positionProperty.set( this.destination ); - this.animatingProperty.set( false ); - } + // Value that indicates how faded out this shape is. This is used as part of a feature where shapes can fade + // out. Once fade has started ; it doesn't stop until it is fully faded ; i.e. the value is 1. This should not be + // set externally. + this.fadeProportionProperty = new Property( 0 ); - // perform any fading - if ( this.fading ) { - this.fadeProportionProperty.set( Math.min( 1, this.fadeProportionProperty.get() + ( dt * FADE_RATE ) ) ); - if ( this.fadeProportionProperty.get() >= 1 ) { - this.fading = false; - } - } - } - }, - - /** - * Set the destination for this shape. - * @param {Vector2} destination - * @param {boolean} animate - */ - setDestination: function( destination, animate ) { - this.destination = destination; - if ( animate ) { - this.animatingProperty.set( true ); + // A flag that indicates whether this individual shape should become invisible when it is done animating. This + // is generally used in cases where it becomes part of a larger composite shape that is depicted instead. + this.invisibleWhenStillProperty = new Property( true ); + + // Destination is used for animation, and should be set through accessor methods only. + this.destination = initialPosition.copy(); // @private + + // Emit an event whenever this shape returns to its original position. + this.returnedToOriginEmitter = new Emitter(); + this.positionProperty.lazyLink( function( position ) { + if ( position.equals( initialPosition ) ) { + self.returnedToOriginEmitter.emit(); + } + } ); + + // Non-dynamic attributes + this.shape = shape; // @public, read only + this.color = Color.toColor( color ); // @public + + // Internal vars + this.fading = false; // @private +} + +areaBuilder.register( 'MovableShape', MovableShape ); + +export default inherit( Object, MovableShape, { + + step: function( dt ) { + if ( !this.userControlledProperty.get() ) { + + // perform any animation + const currentPosition = this.positionProperty.get(); + const distanceToDestination = currentPosition.distance( this.destination ); + if ( distanceToDestination > dt * AreaBuilderSharedConstants.ANIMATION_SPEED ) { + // Move a step toward the destination. + const stepAngle = Math.atan2( this.destination.y - currentPosition.y, this.destination.x - currentPosition.x ); + const stepVector = Vector2.createPolar( AreaBuilderSharedConstants.ANIMATION_SPEED * dt, stepAngle ); + this.positionProperty.set( currentPosition.plus( stepVector ) ); } - else { - this.animatingProperty.set( false ); + else if ( this.animatingProperty.get() ) { + // Less than one time step away, so just go to the destination. this.positionProperty.set( this.destination ); + this.animatingProperty.set( false ); } - }, - - /** - * Return the shape to the place where it was originally created. - * @param {boolean} animate - */ - returnToOrigin: function( animate ) { - this.setDestination( this.positionProperty.initialValue, animate ); - }, - - fadeAway: function() { - this.fading = true; - this.fadeProportionProperty.set( 0.0001 ); // this is done to make sure the shape is made unpickable as soon as fading starts - }, - - /** - * Returns a set of squares that are of the specified size and are positioned correctly such that they collectively - * make up the same shape as this rectangle. The specified length must be an integer value of the length and - * width or things will get weird. - * - * NOTE: This only works properly for rectangular shapes! - * - * @public - * @param squareLength - */ - decomposeIntoSquares: function( squareLength ) { - assert && assert( this.shape.bounds.width % squareLength === 0 && this.shape.bounds.height % squareLength === 0, - 'Error: A dimension of this movable shape is not an integer multiple of the provided dimension' ); - const shapes = []; - const unitSquareShape = Shape.rect( 0, 0, squareLength, squareLength ); - for ( let column = 0; column < this.shape.bounds.width; column += squareLength ) { - for ( let row = 0; row < this.shape.bounds.height; row += squareLength ) { - const constituentShape = new MovableShape( unitSquareShape, this.color, this.positionProperty.initialValue ); - constituentShape.setDestination( this.positionProperty.get().plusXY( column, row ), false ); - constituentShape.invisibleWhenStillProperty.set( this.invisibleWhenStillProperty.get() ); - shapes.push( constituentShape ); + + // perform any fading + if ( this.fading ) { + this.fadeProportionProperty.set( Math.min( 1, this.fadeProportionProperty.get() + ( dt * FADE_RATE ) ) ); + if ( this.fadeProportionProperty.get() >= 1 ) { + this.fading = false; } } - return shapes; } - } ); -} ); + }, + + /** + * Set the destination for this shape. + * @param {Vector2} destination + * @param {boolean} animate + */ + setDestination: function( destination, animate ) { + this.destination = destination; + if ( animate ) { + this.animatingProperty.set( true ); + } + else { + this.animatingProperty.set( false ); + this.positionProperty.set( this.destination ); + } + }, + + /** + * Return the shape to the place where it was originally created. + * @param {boolean} animate + */ + returnToOrigin: function( animate ) { + this.setDestination( this.positionProperty.initialValue, animate ); + }, + + fadeAway: function() { + this.fading = true; + this.fadeProportionProperty.set( 0.0001 ); // this is done to make sure the shape is made unpickable as soon as fading starts + }, + + /** + * Returns a set of squares that are of the specified size and are positioned correctly such that they collectively + * make up the same shape as this rectangle. The specified length must be an integer value of the length and + * width or things will get weird. + * + * NOTE: This only works properly for rectangular shapes! + * + * @public + * @param squareLength + */ + decomposeIntoSquares: function( squareLength ) { + assert && assert( this.shape.bounds.width % squareLength === 0 && this.shape.bounds.height % squareLength === 0, + 'Error: A dimension of this movable shape is not an integer multiple of the provided dimension' ); + const shapes = []; + const unitSquareShape = Shape.rect( 0, 0, squareLength, squareLength ); + for ( let column = 0; column < this.shape.bounds.width; column += squareLength ) { + for ( let row = 0; row < this.shape.bounds.height; row += squareLength ) { + const constituentShape = new MovableShape( unitSquareShape, this.color, this.positionProperty.initialValue ); + constituentShape.setDestination( this.positionProperty.get().plusXY( column, row ), false ); + constituentShape.invisibleWhenStillProperty.set( this.invisibleWhenStillProperty.get() ); + shapes.push( constituentShape ); + } + } + return shapes; + } +} ); \ No newline at end of file diff --git a/js/common/model/PerimeterShape.js b/js/common/model/PerimeterShape.js index bb04387..dcadb91 100644 --- a/js/common/model/PerimeterShape.js +++ b/js/common/model/PerimeterShape.js @@ -6,152 +6,148 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Shape = require( 'KITE/Shape' ); - const Vector2 = require( 'DOT/Vector2' ); +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import areaBuilder from '../../areaBuilder.js'; - // constants - const FLOATING_POINT_ERR_TOLERANCE = 1e-6; +// constants +const FLOATING_POINT_ERR_TOLERANCE = 1e-6; - // Utility function to compute the unit area of a perimeter shape. - function calculateUnitArea( shape, unitLength ) { +// Utility function to compute the unit area of a perimeter shape. +function calculateUnitArea( shape, unitLength ) { - if ( !shape.bounds.isFinite() ) { - return 0; - } + if ( !shape.bounds.isFinite() ) { + return 0; + } - assert && assert( shape.bounds.width % unitLength < FLOATING_POINT_ERR_TOLERANCE && - shape.bounds.height % unitLength < FLOATING_POINT_ERR_TOLERANCE, - 'Error: This method will only work with shapes that have bounds of unit width and height.' - ); - - // Compute the unit area by testing whether or not points on a sub-grid are contained in the shape. - let unitArea = 0; - const testPoint = new Vector2( 0, 0 ); - for ( let row = 0; row * unitLength < shape.bounds.height; row++ ) { - for ( let column = 0; column * unitLength < shape.bounds.width; column++ ) { - // Scan four points in the unit square. This allows support for triangular 1/2 unit square shapes. This is - // in-lined rather than looped for the sake of efficiency, since this approach avoids vector allocations. - testPoint.setXY( shape.bounds.minX + ( column + 0.25 ) * unitLength, shape.bounds.minY + ( row + 0.5 ) * unitLength ); - if ( shape.containsPoint( testPoint ) ) { - unitArea += 0.25; - } - testPoint.setXY( shape.bounds.minX + ( column + 0.5 ) * unitLength, shape.bounds.minY + ( row + 0.25 ) * unitLength ); - if ( shape.containsPoint( testPoint ) ) { - unitArea += 0.25; - } - testPoint.setXY( shape.bounds.minX + ( column + 0.5 ) * unitLength, shape.bounds.minY + ( row + 0.75 ) * unitLength ); - if ( shape.containsPoint( testPoint ) ) { - unitArea += 0.25; - } - testPoint.setXY( shape.bounds.minX + ( column + 0.75 ) * unitLength, shape.bounds.minY + ( row + 0.5 ) * unitLength ); - if ( shape.containsPoint( testPoint ) ) { - unitArea += 0.25; - } + assert && assert( shape.bounds.width % unitLength < FLOATING_POINT_ERR_TOLERANCE && + shape.bounds.height % unitLength < FLOATING_POINT_ERR_TOLERANCE, + 'Error: This method will only work with shapes that have bounds of unit width and height.' + ); + + // Compute the unit area by testing whether or not points on a sub-grid are contained in the shape. + let unitArea = 0; + const testPoint = new Vector2( 0, 0 ); + for ( let row = 0; row * unitLength < shape.bounds.height; row++ ) { + for ( let column = 0; column * unitLength < shape.bounds.width; column++ ) { + // Scan four points in the unit square. This allows support for triangular 1/2 unit square shapes. This is + // in-lined rather than looped for the sake of efficiency, since this approach avoids vector allocations. + testPoint.setXY( shape.bounds.minX + ( column + 0.25 ) * unitLength, shape.bounds.minY + ( row + 0.5 ) * unitLength ); + if ( shape.containsPoint( testPoint ) ) { + unitArea += 0.25; + } + testPoint.setXY( shape.bounds.minX + ( column + 0.5 ) * unitLength, shape.bounds.minY + ( row + 0.25 ) * unitLength ); + if ( shape.containsPoint( testPoint ) ) { + unitArea += 0.25; + } + testPoint.setXY( shape.bounds.minX + ( column + 0.5 ) * unitLength, shape.bounds.minY + ( row + 0.75 ) * unitLength ); + if ( shape.containsPoint( testPoint ) ) { + unitArea += 0.25; + } + testPoint.setXY( shape.bounds.minX + ( column + 0.75 ) * unitLength, shape.bounds.minY + ( row + 0.5 ) * unitLength ); + if ( shape.containsPoint( testPoint ) ) { + unitArea += 0.25; } } - return unitArea; } + return unitArea; +} - /** - * @param {Array>} exteriorPerimeters An array of perimeters, each of which is a sequential array of - * points. - * @param {Array>} interiorPerimeters An array of perimeters, each of which is a sequential array of - * points. Each interior perimeter must be fully contained within an exterior perimeter. - * @param {number} unitLength The unit length (i.e. the width or height of a unit square) of the unit sizes that - * this shape should be constructed from. - * @param {Object} [options] - * @constructor - */ - function PerimeterShape( exteriorPerimeters, interiorPerimeters, unitLength, options ) { - const self = this; - let i; - - options = merge( { - fillColor: null, - edgeColor: null - }, options ); // Make sure options is defined. - - // @public, read only - this.fillColor = options.fillColor; - - // @public, read only - this.edgeColor = options.edgeColor; - - // @public, read only - this.exteriorPerimeters = exteriorPerimeters; - - // @public, read only - this.interiorPerimeters = interiorPerimeters; - - // @private - this.unitLength = unitLength; - - // @private A Kite shape created from the points, useful in various situations. - this.kiteShape = new Shape(); - exteriorPerimeters.forEach( function( exteriorPerimeter ) { - self.kiteShape.moveToPoint( exteriorPerimeter[ 0 ] ); - for ( i = 1; i < exteriorPerimeter.length; i++ ) { - self.kiteShape.lineToPoint( exteriorPerimeter[ i ] ); - } - self.kiteShape.lineToPoint( exteriorPerimeter[ 0 ] ); - self.kiteShape.close(); - } ); +/** + * @param {Array>} exteriorPerimeters An array of perimeters, each of which is a sequential array of + * points. + * @param {Array>} interiorPerimeters An array of perimeters, each of which is a sequential array of + * points. Each interior perimeter must be fully contained within an exterior perimeter. + * @param {number} unitLength The unit length (i.e. the width or height of a unit square) of the unit sizes that + * this shape should be constructed from. + * @param {Object} [options] + * @constructor + */ +function PerimeterShape( exteriorPerimeters, interiorPerimeters, unitLength, options ) { + const self = this; + let i; - // Only add interior spaces if there is a legitimate external perimeter. - if ( !self.kiteShape.bounds.isEmpty() ) { - interiorPerimeters.forEach( function( interiorPerimeter ) { - self.kiteShape.moveToPoint( interiorPerimeter[ 0 ] ); - for ( i = 1; i < interiorPerimeter.length; i++ ) { - self.kiteShape.lineToPoint( interiorPerimeter[ i ] ); - } - self.kiteShape.lineToPoint( interiorPerimeter[ 0 ] ); - self.kiteShape.close(); - } ); + options = merge( { + fillColor: null, + edgeColor: null + }, options ); // Make sure options is defined. + + // @public, read only + this.fillColor = options.fillColor; + + // @public, read only + this.edgeColor = options.edgeColor; + + // @public, read only + this.exteriorPerimeters = exteriorPerimeters; + + // @public, read only + this.interiorPerimeters = interiorPerimeters; + + // @private + this.unitLength = unitLength; + + // @private A Kite shape created from the points, useful in various situations. + this.kiteShape = new Shape(); + exteriorPerimeters.forEach( function( exteriorPerimeter ) { + self.kiteShape.moveToPoint( exteriorPerimeter[ 0 ] ); + for ( i = 1; i < exteriorPerimeter.length; i++ ) { + self.kiteShape.lineToPoint( exteriorPerimeter[ i ] ); } + self.kiteShape.lineToPoint( exteriorPerimeter[ 0 ] ); + self.kiteShape.close(); + } ); - // @public, read only - this.unitArea = calculateUnitArea( this.kiteShape, unitLength ); + // Only add interior spaces if there is a legitimate external perimeter. + if ( !self.kiteShape.bounds.isEmpty() ) { + interiorPerimeters.forEach( function( interiorPerimeter ) { + self.kiteShape.moveToPoint( interiorPerimeter[ 0 ] ); + for ( i = 1; i < interiorPerimeter.length; i++ ) { + self.kiteShape.lineToPoint( interiorPerimeter[ i ] ); + } + self.kiteShape.lineToPoint( interiorPerimeter[ 0 ] ); + self.kiteShape.close(); + } ); } - areaBuilder.register( 'PerimeterShape', PerimeterShape ); + // @public, read only + this.unitArea = calculateUnitArea( this.kiteShape, unitLength ); +} - return inherit( Object, PerimeterShape, { +areaBuilder.register( 'PerimeterShape', PerimeterShape ); - // Returns a linearly translated version of this perimeter shape. - translated: function( x, y ) { - const exteriorPerimeters = []; - const interiorPerimeters = []; - this.exteriorPerimeters.forEach( function( exteriorPerimeter, index ) { - exteriorPerimeters.push( [] ); - exteriorPerimeter.forEach( function( point ) { - exteriorPerimeters[ index ].push( point.plusXY( x, y ) ); - } ); - } ); - this.interiorPerimeters.forEach( function( interiorPerimeter, index ) { - interiorPerimeters.push( [] ); - interiorPerimeter.forEach( function( point ) { - interiorPerimeters[ index ].push( point.plusXY( x, y ) ); - } ); +export default inherit( Object, PerimeterShape, { + + // Returns a linearly translated version of this perimeter shape. + translated: function( x, y ) { + const exteriorPerimeters = []; + const interiorPerimeters = []; + this.exteriorPerimeters.forEach( function( exteriorPerimeter, index ) { + exteriorPerimeters.push( [] ); + exteriorPerimeter.forEach( function( point ) { + exteriorPerimeters[ index ].push( point.plusXY( x, y ) ); } ); - return new PerimeterShape( exteriorPerimeters, interiorPerimeters, this.unitLength, { - fillColor: this.fillColor, - edgeColor: this.edgeColor + } ); + this.interiorPerimeters.forEach( function( interiorPerimeter, index ) { + interiorPerimeters.push( [] ); + interiorPerimeter.forEach( function( point ) { + interiorPerimeters[ index ].push( point.plusXY( x, y ) ); } ); - }, + } ); + return new PerimeterShape( exteriorPerimeters, interiorPerimeters, this.unitLength, { + fillColor: this.fillColor, + edgeColor: this.edgeColor + } ); + }, - getWidth: function() { - return this.kiteShape.bounds.width; - }, + getWidth: function() { + return this.kiteShape.bounds.width; + }, - getHeight: function() { - return this.kiteShape.bounds.height; - } - } ); + getHeight: function() { + return this.kiteShape.bounds.height; + } } ); \ No newline at end of file diff --git a/js/common/model/ShapePlacementBoard.js b/js/common/model/ShapePlacementBoard.js index 3b37726..fa58c85 100644 --- a/js/common/model/ShapePlacementBoard.js +++ b/js/common/model/ShapePlacementBoard.js @@ -5,1026 +5,1022 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Color = require( 'SCENERY/util/Color' ); - const Fraction = require( 'PHETCOMMON/model/Fraction' ); - const inherit = require( 'PHET_CORE/inherit' ); - const ObservableArray = require( 'AXON/ObservableArray' ); - const PerimeterShape = require( 'AREA_BUILDER/common/model/PerimeterShape' ); - const Property = require( 'AXON/Property' ); - const Shape = require( 'KITE/Shape' ); - const Utils = require( 'DOT/Utils' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const MOVEMENT_VECTORS = { - // This sim is using screen conventions, meaning positive Y indicates down. - up: new Vector2( 0, -1 ), - down: new Vector2( 0, 1 ), - left: new Vector2( -1, 0 ), - right: new Vector2( 1, 0 ) - }; - - // Functions used for scanning the edge of the perimeter. These are a key component of the "marching squares" - // algorithm that is used for perimeter traversal, see the function where they are used for more information. - const SCAN_AREA_MOVEMENT_FUNCTIONS = [ - null, // 0 - function() { return MOVEMENT_VECTORS.up; }, // 1 - function() { return MOVEMENT_VECTORS.right; }, // 2 - function() { return MOVEMENT_VECTORS.right; }, // 3 - function() { return MOVEMENT_VECTORS.left; }, // 4 - function() { return MOVEMENT_VECTORS.up; }, // 5 - function( previousStep ) { return previousStep === MOVEMENT_VECTORS.up ? MOVEMENT_VECTORS.left : MOVEMENT_VECTORS.right; }, // 6 - function() { return MOVEMENT_VECTORS.right; }, // 7 - function() { return MOVEMENT_VECTORS.down; }, // 8 - function( previousStep ) { return previousStep === MOVEMENT_VECTORS.right ? MOVEMENT_VECTORS.up : MOVEMENT_VECTORS.down; }, // 9 - function() { return MOVEMENT_VECTORS.down; }, // 10 - function() { return MOVEMENT_VECTORS.down; }, // 11 - function() { return MOVEMENT_VECTORS.left; }, // 12 - function() { return MOVEMENT_VECTORS.up; }, // 13 - function() { return MOVEMENT_VECTORS.left; }, // 14 - null // 15 - ]; - /** - * @param {Dimension2} size - * @param {number} unitSquareLength - * @param {Vector2} position - * @param {string || Color} colorHandled A string or Color object, can be wildcard string ('*') for all colors - * @param {Property} showGridProperty - * @param {Property} showDimensionsProperty - * @constructor - */ - function ShapePlacementBoard( size, unitSquareLength, position, colorHandled, showGridProperty, showDimensionsProperty ) { - - // The size should be an integer number of unit squares for both dimensions. - assert && assert( size.width % unitSquareLength === 0 && size.height % unitSquareLength === 0, - 'ShapePlacementBoard dimensions must be integral numbers of unit square dimensions' ); - - this.showGridProperty = showGridProperty; - this.showDimensionsProperty = showDimensionsProperty; - - // Set the initial fill and edge colors for the composite shape (defined in Property declarations below). - this.compositeShapeFillColor = colorHandled === '*' ? new Color( AreaBuilderSharedConstants.GREENISH_COLOR ) : Color.toColor( colorHandled ); - this.compositeShapeEdgeColor = this.compositeShapeFillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); - - // @public boolean Read/Write value that controls whether the placement board moves individual shapes that are - // added to the board such that they form a single, contiguous, composite shape, or if it just snaps them to the - // grid. The perimeter and area values are only updated when this is set to true. - this.formCompositeProperty = new Property( true ); - - // @public Read-only property that indicates the area and perimeter of the composite shape. These must be - // together in an object so that they can be updated simultaneously, otherwise race conditions can occur when - // evaluating challenges. - this.areaAndPerimeterProperty = new Property( { - area: 0, // {number||string} - number when valid, string when invalid - perimeter: 0 // {number||string} number when valid, string when invalid - } ); +import ObservableArray from '../../../../axon/js/ObservableArray.js'; +import Property from '../../../../axon/js/Property.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import Utils from '../../../../dot/js/Utils.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Fraction from '../../../../phetcommon/js/model/Fraction.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import PerimeterShape from './PerimeterShape.js'; + +// constants +const MOVEMENT_VECTORS = { + // This sim is using screen conventions, meaning positive Y indicates down. + up: new Vector2( 0, -1 ), + down: new Vector2( 0, 1 ), + left: new Vector2( -1, 0 ), + right: new Vector2( 1, 0 ) +}; + +// Functions used for scanning the edge of the perimeter. These are a key component of the "marching squares" +// algorithm that is used for perimeter traversal, see the function where they are used for more information. +const SCAN_AREA_MOVEMENT_FUNCTIONS = [ + null, // 0 + function() { return MOVEMENT_VECTORS.up; }, // 1 + function() { return MOVEMENT_VECTORS.right; }, // 2 + function() { return MOVEMENT_VECTORS.right; }, // 3 + function() { return MOVEMENT_VECTORS.left; }, // 4 + function() { return MOVEMENT_VECTORS.up; }, // 5 + function( previousStep ) { return previousStep === MOVEMENT_VECTORS.up ? MOVEMENT_VECTORS.left : MOVEMENT_VECTORS.right; }, // 6 + function() { return MOVEMENT_VECTORS.right; }, // 7 + function() { return MOVEMENT_VECTORS.down; }, // 8 + function( previousStep ) { return previousStep === MOVEMENT_VECTORS.right ? MOVEMENT_VECTORS.up : MOVEMENT_VECTORS.down; }, // 9 + function() { return MOVEMENT_VECTORS.down; }, // 10 + function() { return MOVEMENT_VECTORS.down; }, // 11 + function() { return MOVEMENT_VECTORS.left; }, // 12 + function() { return MOVEMENT_VECTORS.up; }, // 13 + function() { return MOVEMENT_VECTORS.left; }, // 14 + null // 15 +]; - // @public Read-only shape defined in terms of perimeter points that describes the composite shape created by all - // of the individual shapes placed on the board by the user. - this.compositeShapeProperty = new Property( new PerimeterShape( [], [], unitSquareLength, { - fillColor: this.compositeShapeFillColor, - edgeColor: this.compositeShapeEdgeColor - } ) ); - - // @public Read-only shape that can be placed on the board, generally as a template over which the user can add - // other shapes. The shape is positioned relative to this board, not in absolute model space. It should be - // set through the method provided on this class rather than directly. - this.backgroundShapeProperty = new Property( - new PerimeterShape( [], [], unitSquareLength, { fillColor: 'black' } ) - ); +/** + * @param {Dimension2} size + * @param {number} unitSquareLength + * @param {Vector2} position + * @param {string || Color} colorHandled A string or Color object, can be wildcard string ('*') for all colors + * @param {Property} showGridProperty + * @param {Property} showDimensionsProperty + * @constructor + */ +function ShapePlacementBoard( size, unitSquareLength, position, colorHandled, showGridProperty, showDimensionsProperty ) { + + // The size should be an integer number of unit squares for both dimensions. + assert && assert( size.width % unitSquareLength === 0 && size.height % unitSquareLength === 0, + 'ShapePlacementBoard dimensions must be integral numbers of unit square dimensions' ); + + this.showGridProperty = showGridProperty; + this.showDimensionsProperty = showDimensionsProperty; + + // Set the initial fill and edge colors for the composite shape (defined in Property declarations below). + this.compositeShapeFillColor = colorHandled === '*' ? new Color( AreaBuilderSharedConstants.GREENISH_COLOR ) : Color.toColor( colorHandled ); + this.compositeShapeEdgeColor = this.compositeShapeFillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); + + // @public boolean Read/Write value that controls whether the placement board moves individual shapes that are + // added to the board such that they form a single, contiguous, composite shape, or if it just snaps them to the + // grid. The perimeter and area values are only updated when this is set to true. + this.formCompositeProperty = new Property( true ); + + // @public Read-only property that indicates the area and perimeter of the composite shape. These must be + // together in an object so that they can be updated simultaneously, otherwise race conditions can occur when + // evaluating challenges. + this.areaAndPerimeterProperty = new Property( { + area: 0, // {number||string} - number when valid, string when invalid + perimeter: 0 // {number||string} number when valid, string when invalid + } ); - // @public Read/write value for controlling whether the background shape should show a grid when portrayed in the - // view. - this.showGridOnBackgroundShapeProperty = new Property( false ); - - // Observable array of the shapes that have been placed on this board. - this.residentShapes = new ObservableArray(); // @public, read only - - // Non-dynamic public values. - this.unitSquareLength = unitSquareLength; // @public - this.bounds = new Bounds2( position.x, position.y, position.x + size.width, position.y + size.height ); // @public - this.colorHandled = colorHandled === '*' ? colorHandled : Color.toColor( colorHandled ); // @public - - // Private variables - this.numRows = size.height / unitSquareLength; // @private - this.numColumns = size.width / unitSquareLength; // @private - this.incomingShapes = []; // @private, {Array}, list of shapes that are animating to a spot on this board but aren't here yet - this.updatesSuspended = false; // @private, used to improve performance when adding a bunch of shapes at once to the board - - // For efficiency and simplicity in evaluating the interior and exterior perimeter, identifying orphaned shapes, - // and so forth, a 2D array is used to track various state information about the 'cells' that correspond to the - // positions on this board where shapes may be placed. - this.cells = []; //@private - for ( let column = 0; column < this.numColumns; column++ ) { - const currentRow = []; - for ( let row = 0; row < this.numRows; row++ ) { - // Add an object that defines the information internally tracked for each cell. - currentRow.push( { - column: column, - row: row, - occupiedBy: null, // the shape occupying this cell, null if none - cataloged: false, // used by group identification algorithm - catalogedBy: null // used by group identification algorithm - } ); - } - this.cells.push( currentRow ); + // @public Read-only shape defined in terms of perimeter points that describes the composite shape created by all + // of the individual shapes placed on the board by the user. + this.compositeShapeProperty = new Property( new PerimeterShape( [], [], unitSquareLength, { + fillColor: this.compositeShapeFillColor, + edgeColor: this.compositeShapeEdgeColor + } ) ); + + // @public Read-only shape that can be placed on the board, generally as a template over which the user can add + // other shapes. The shape is positioned relative to this board, not in absolute model space. It should be + // set through the method provided on this class rather than directly. + this.backgroundShapeProperty = new Property( + new PerimeterShape( [], [], unitSquareLength, { fillColor: 'black' } ) + ); + + // @public Read/write value for controlling whether the background shape should show a grid when portrayed in the + // view. + this.showGridOnBackgroundShapeProperty = new Property( false ); + + // Observable array of the shapes that have been placed on this board. + this.residentShapes = new ObservableArray(); // @public, read only + + // Non-dynamic public values. + this.unitSquareLength = unitSquareLength; // @public + this.bounds = new Bounds2( position.x, position.y, position.x + size.width, position.y + size.height ); // @public + this.colorHandled = colorHandled === '*' ? colorHandled : Color.toColor( colorHandled ); // @public + + // Private variables + this.numRows = size.height / unitSquareLength; // @private + this.numColumns = size.width / unitSquareLength; // @private + this.incomingShapes = []; // @private, {Array}, list of shapes that are animating to a spot on this board but aren't here yet + this.updatesSuspended = false; // @private, used to improve performance when adding a bunch of shapes at once to the board + + // For efficiency and simplicity in evaluating the interior and exterior perimeter, identifying orphaned shapes, + // and so forth, a 2D array is used to track various state information about the 'cells' that correspond to the + // positions on this board where shapes may be placed. + this.cells = []; //@private + for ( let column = 0; column < this.numColumns; column++ ) { + const currentRow = []; + for ( let row = 0; row < this.numRows; row++ ) { + // Add an object that defines the information internally tracked for each cell. + currentRow.push( { + column: column, + row: row, + occupiedBy: null, // the shape occupying this cell, null if none + cataloged: false, // used by group identification algorithm + catalogedBy: null // used by group identification algorithm + } ); } + this.cells.push( currentRow ); } +} - areaBuilder.register( 'ShapePlacementBoard', ShapePlacementBoard ); +areaBuilder.register( 'ShapePlacementBoard', ShapePlacementBoard ); - return inherit( Object, ShapePlacementBoard, { +export default inherit( Object, ShapePlacementBoard, { - // @private - shapeOverlapsBoard: function( shape ) { - const shapePosition = shape.positionProperty.get(); - const shapeBounds = new Bounds2( - shapePosition.x, - shapePosition.y, - shapePosition.x + shape.shape.bounds.getWidth(), - shapePosition.y + shape.shape.bounds.getHeight() - ); - return this.bounds.intersectsBounds( shapeBounds ); - }, - - /** - * Place the provide shape on this board. Returns false if the color does not match the handled color or if the - * shape is not partially over the board. - * @public - * @param {MovableShape} movableShape A model shape - */ - placeShape: function( movableShape ) { - assert && assert( - movableShape.userControlledProperty.get() === false, - 'Shapes can\'t be placed when still controlled by user.' - ); - const self = this; + // @private + shapeOverlapsBoard: function( shape ) { + const shapePosition = shape.positionProperty.get(); + const shapeBounds = new Bounds2( + shapePosition.x, + shapePosition.y, + shapePosition.x + shape.shape.bounds.getWidth(), + shapePosition.y + shape.shape.bounds.getHeight() + ); + return this.bounds.intersectsBounds( shapeBounds ); + }, - // Only place the shape if it is of the correct color and is positioned so that it overlaps with the board. - if ( ( this.colorHandled !== '*' && !movableShape.color.equals( this.colorHandled ) ) || !this.shapeOverlapsBoard( movableShape ) ) { - return false; - } + /** + * Place the provide shape on this board. Returns false if the color does not match the handled color or if the + * shape is not partially over the board. + * @public + * @param {MovableShape} movableShape A model shape + */ + placeShape: function( movableShape ) { + assert && assert( + movableShape.userControlledProperty.get() === false, + 'Shapes can\'t be placed when still controlled by user.' + ); + const self = this; - // Set the shape's visibility behavior based on whether a composite shape is being depicted. - movableShape.invisibleWhenStillProperty.set( this.formCompositeProperty.get() ); - - // Determine where to place the shape on the board. - let placementPosition = null; - for ( let surroundingPointsLevel = 0; - surroundingPointsLevel < Math.max( this.numRows, this.numColumns ) && placementPosition === null; - surroundingPointsLevel++ ) { - - const surroundingPoints = this.getOuterSurroundingPoints( - movableShape.positionProperty.get(), - surroundingPointsLevel - ); - surroundingPoints.sort( function( p1, p2 ) { - return p1.distance( movableShape.positionProperty.get() ) - p2.distance( movableShape.positionProperty.get() ); - } ); - for ( let pointIndex = 0; pointIndex < surroundingPoints.length && placementPosition === null; pointIndex++ ) { - if ( self.isValidToPlace( movableShape, surroundingPoints[ pointIndex ] ) ) { - placementPosition = surroundingPoints[ pointIndex ]; - } - } - } - if ( placementPosition === null ) { - // No valid position found - bail out. - return false; - } + // Only place the shape if it is of the correct color and is positioned so that it overlaps with the board. + if ( ( this.colorHandled !== '*' && !movableShape.color.equals( this.colorHandled ) ) || !this.shapeOverlapsBoard( movableShape ) ) { + return false; + } - // add this shape to the list of incoming shapes - this.addIncomingShape( movableShape, placementPosition, true ); + // Set the shape's visibility behavior based on whether a composite shape is being depicted. + movableShape.invisibleWhenStillProperty.set( this.formCompositeProperty.get() ); - // If we made it to here, placement succeeded. - return true; - }, - - /** - * Add a shape directly to the specified cell. This bypasses the placement process, and is generally used when - * displaying solutions to challenges. The shape will animate to the chosen cell. - * @public - * @param cellColumn - * @param cellRow - * @param movableShape - */ - addShapeDirectlyToCell: function( cellColumn, cellRow, movableShape ) { - - // Set the shape's visibility behavior based on whether a composite shape is being depicted. - movableShape.invisibleWhenStillProperty.set( this.formCompositeProperty.get() ); - - // Add the shape by putting it on the list of incoming shapes and setting its destination. - this.addIncomingShape( movableShape, this.cellToModelCoords( cellColumn, cellRow, false ) ); - }, - - /** - * Get the proportion of area that match the provided color. - * - * @param color - */ - getProportionOfColor: function( color ) { - const self = this; - const compareColor = Color.toColor( color ); - let totalArea = 0; - let areaOfSpecifiedColor = 0; - this.residentShapes.forEach( function( residentShape ) { - const areaOfShape = residentShape.shape.bounds.width * residentShape.shape.bounds.height / ( self.unitSquareLength * self.unitSquareLength ); - totalArea += areaOfShape; - if ( compareColor.equals( residentShape.color ) ) { - areaOfSpecifiedColor += areaOfShape; - } + // Determine where to place the shape on the board. + let placementPosition = null; + for ( let surroundingPointsLevel = 0; + surroundingPointsLevel < Math.max( this.numRows, this.numColumns ) && placementPosition === null; + surroundingPointsLevel++ ) { + + const surroundingPoints = this.getOuterSurroundingPoints( + movableShape.positionProperty.get(), + surroundingPointsLevel + ); + surroundingPoints.sort( function( p1, p2 ) { + return p1.distance( movableShape.positionProperty.get() ) - p2.distance( movableShape.positionProperty.get() ); } ); + for ( let pointIndex = 0; pointIndex < surroundingPoints.length && placementPosition === null; pointIndex++ ) { + if ( self.isValidToPlace( movableShape, surroundingPoints[ pointIndex ] ) ) { + placementPosition = surroundingPoints[ pointIndex ]; + } + } + } + if ( placementPosition === null ) { + // No valid position found - bail out. + return false; + } - const proportion = new Fraction( areaOfSpecifiedColor, totalArea ); - proportion.reduce(); - return proportion; - }, + // add this shape to the list of incoming shapes + this.addIncomingShape( movableShape, placementPosition, true ); - // @private, add a shape to the list of residents and make the other updates that go along with this. - addResidentShape: function( movableShape, releaseOrphans ) { + // If we made it to here, placement succeeded. + return true; + }, - // Make sure that the shape is not moving - assert && assert( - movableShape.positionProperty.get().equals( movableShape.destination ), - 'Error: Shapes should not become residents until they have completed animating.' - ); + /** + * Add a shape directly to the specified cell. This bypasses the placement process, and is generally used when + * displaying solutions to challenges. The shape will animate to the chosen cell. + * @public + * @param cellColumn + * @param cellRow + * @param movableShape + */ + addShapeDirectlyToCell: function( cellColumn, cellRow, movableShape ) { - // Made sure that the shape isn't already a resident. - assert && assert( !this.isResidentShape( movableShape ), 'Error: Attempt to add shape that is already a resident.' ); + // Set the shape's visibility behavior based on whether a composite shape is being depicted. + movableShape.invisibleWhenStillProperty.set( this.formCompositeProperty.get() ); - this.residentShapes.push( movableShape ); + // Add the shape by putting it on the list of incoming shapes and setting its destination. + this.addIncomingShape( movableShape, this.cellToModelCoords( cellColumn, cellRow, false ) ); + }, - // Make the appropriate updates. - this.updateCellOccupation( movableShape, 'add' ); - if ( releaseOrphans ) { - this.releaseAnyOrphans(); + /** + * Get the proportion of area that match the provided color. + * + * @param color + */ + getProportionOfColor: function( color ) { + const self = this; + const compareColor = Color.toColor( color ); + let totalArea = 0; + let areaOfSpecifiedColor = 0; + this.residentShapes.forEach( function( residentShape ) { + const areaOfShape = residentShape.shape.bounds.width * residentShape.shape.bounds.height / ( self.unitSquareLength * self.unitSquareLength ); + totalArea += areaOfShape; + if ( compareColor.equals( residentShape.color ) ) { + areaOfSpecifiedColor += areaOfShape; } - this.updateAll(); - }, + } ); - //@private, remove the specified shape from the shape placement board - removeResidentShape: function( movableShape ) { - assert && assert( this.isResidentShape( movableShape ), 'Error: Attempt to remove shape that is not a resident.' ); - const self = this; - this.residentShapes.remove( movableShape ); - self.updateCellOccupation( movableShape, 'remove' ); - self.updateAll(); - - if ( movableShape.userControlledProperty.get() ) { - - // Watch the shape so that we can do needed updates when the user releases it. - movableShape.userControlledProperty.lazyLink( function releaseOrphansIfDroppedOfBoard( userControlled ) { - assert && assert( !userControlled, 'Unexpected transition of userControlled flag.' ); - if ( !self.shapeOverlapsBoard( movableShape ) ) { - // This shape isn't coming back, so we need to trigger an orphan release. - self.releaseAnyOrphans(); - self.updateAll(); - } - movableShape.userControlledProperty.unlink( releaseOrphansIfDroppedOfBoard ); - } ); - } - }, + const proportion = new Fraction( areaOfSpecifiedColor, totalArea ); + proportion.reduce(); + return proportion; + }, - // @private, add the shape to the list of incoming shapes and set up a listener to move it to resident shapes - addIncomingShape: function( movableShape, destination, releaseOrphans ) { + // @private, add a shape to the list of residents and make the other updates that go along with this. + addResidentShape: function( movableShape, releaseOrphans ) { - const self = this; + // Make sure that the shape is not moving + assert && assert( + movableShape.positionProperty.get().equals( movableShape.destination ), + 'Error: Shapes should not become residents until they have completed animating.' + ); - movableShape.setDestination( destination, true ); - - // The remaining code in this method assumes that the shape is animating to the new position, and will cause - // odd results if it isn't, so we double check it here. - assert && assert( movableShape.animatingProperty.get(), 'Shape is is expected to be animating' ); - - // The shape is moving to a spot on the board. We don't want to add it to the list of resident shapes yet, or we - // may trigger a change to the exterior and interior perimeters, but we need to keep a reference to it so that - // the valid placement positions can be updated, especially in multi-touch environments. So, basically, there is - // an intermediate 'holding place' for incoming shapes. - this.incomingShapes.push( movableShape ); - - // Create a listener that will move this shape from the incoming shape list to the resident list once the - // animation completes. - function animationCompleteListener( animating ) { - if ( !animating ) { - // Move the shape from the incoming list to the resident list. - self.incomingShapes.splice( self.incomingShapes.indexOf( movableShape ), 1 ); - self.addResidentShape( movableShape, releaseOrphans ); - movableShape.animatingProperty.unlink( animationCompleteListener ); - if ( self.updatesSuspended && self.incomingShapes.length === 0 ) { - // updates had been suspended (for better performance), and the last incoming shapes was added, so resume updates - self.updatesSuspended = false; - self.updateAll(); - } - } + // Made sure that the shape isn't already a resident. + assert && assert( !this.isResidentShape( movableShape ), 'Error: Attempt to add shape that is already a resident.' ); - // Set up a listener to remove this shape if and when the user grabs it. - self.addRemovalListener( movableShape ); + this.residentShapes.push( movableShape ); + + // Make the appropriate updates. + this.updateCellOccupation( movableShape, 'add' ); + if ( releaseOrphans ) { + this.releaseAnyOrphans(); + } + this.updateAll(); + }, + + //@private, remove the specified shape from the shape placement board + removeResidentShape: function( movableShape ) { + assert && assert( this.isResidentShape( movableShape ), 'Error: Attempt to remove shape that is not a resident.' ); + const self = this; + this.residentShapes.remove( movableShape ); + self.updateCellOccupation( movableShape, 'remove' ); + self.updateAll(); + + if ( movableShape.userControlledProperty.get() ) { + + // Watch the shape so that we can do needed updates when the user releases it. + movableShape.userControlledProperty.lazyLink( function releaseOrphansIfDroppedOfBoard( userControlled ) { + assert && assert( !userControlled, 'Unexpected transition of userControlled flag.' ); + if ( !self.shapeOverlapsBoard( movableShape ) ) { + // This shape isn't coming back, so we need to trigger an orphan release. + self.releaseAnyOrphans(); + self.updateAll(); + } + movableShape.userControlledProperty.unlink( releaseOrphansIfDroppedOfBoard ); + } ); + } + }, + + // @private, add the shape to the list of incoming shapes and set up a listener to move it to resident shapes + addIncomingShape: function( movableShape, destination, releaseOrphans ) { + + const self = this; + + movableShape.setDestination( destination, true ); + + // The remaining code in this method assumes that the shape is animating to the new position, and will cause + // odd results if it isn't, so we double check it here. + assert && assert( movableShape.animatingProperty.get(), 'Shape is is expected to be animating' ); + + // The shape is moving to a spot on the board. We don't want to add it to the list of resident shapes yet, or we + // may trigger a change to the exterior and interior perimeters, but we need to keep a reference to it so that + // the valid placement positions can be updated, especially in multi-touch environments. So, basically, there is + // an intermediate 'holding place' for incoming shapes. + this.incomingShapes.push( movableShape ); + + // Create a listener that will move this shape from the incoming shape list to the resident list once the + // animation completes. + function animationCompleteListener( animating ) { + if ( !animating ) { + // Move the shape from the incoming list to the resident list. + self.incomingShapes.splice( self.incomingShapes.indexOf( movableShape ), 1 ); + self.addResidentShape( movableShape, releaseOrphans ); + movableShape.animatingProperty.unlink( animationCompleteListener ); + if ( self.updatesSuspended && self.incomingShapes.length === 0 ) { + // updates had been suspended (for better performance), and the last incoming shapes was added, so resume updates + self.updatesSuspended = false; + self.updateAll(); + } } - // Tag the listener so that it can be removed without firing if needed, such as when the board is cleared. - this.tagListener( animationCompleteListener ); + // Set up a listener to remove this shape if and when the user grabs it. + self.addRemovalListener( movableShape ); + } + + // Tag the listener so that it can be removed without firing if needed, such as when the board is cleared. + this.tagListener( animationCompleteListener ); + + // Hook up the listener. + movableShape.animatingProperty.lazyLink( animationCompleteListener ); + }, + + + // @private, tag a listener for removal + tagListener: function( listener ) { + listener.shapePlacementBoard = this; + }, + + // @private, check if listener function was tagged by this instance + listenerTagMatches: function( listener ) { + return ( listener.shapePlacementBoard && listener.shapePlacementBoard === this ); + }, - // Hook up the listener. - movableShape.animatingProperty.lazyLink( animationCompleteListener ); - }, + // TODO: This is rather ugly. Work with SR to improve or find alternative, or to bake into Axon. Maybe a map. + // @private, remove all observers from a property that have been tagged by this shape placement board. + removeTaggedObservers: function( property ) { + const self = this; + const taggedObservers = []; + property.changedEmitter.listeners.forEach( function( observer ) { + if ( self.listenerTagMatches( observer ) ) { + taggedObservers.push( observer ); + } + } ); + taggedObservers.forEach( function( taggedObserver ) { + property.unlink( taggedObserver ); + } ); + }, + // @private Convenience function for returning a cell or null if row or column are out of range. + getCell: function( column, row ) { + if ( column < 0 || row < 0 || column >= this.numColumns || row >= this.numRows ) { + return null; + } + return this.cells[ column ][ row ]; + }, - // @private, tag a listener for removal - tagListener: function( listener ) { - listener.shapePlacementBoard = this; - }, + // @private Function for getting the occupant of the specified cell, does bounds checking. + getCellOccupant: function( column, row ) { + const cell = this.getCell( column, row ); + return cell ? cell.occupiedBy : null; + }, - // @private, check if listener function was tagged by this instance - listenerTagMatches: function( listener ) { - return ( listener.shapePlacementBoard && listener.shapePlacementBoard === this ); - }, + /** + * Set or clear the occupation status of the cells. + * + * @param movableShape + * @param operation + */ + updateCellOccupation: function( movableShape, operation ) { + const xIndex = Utils.roundSymmetric( ( movableShape.destination.x - this.bounds.minX ) / this.unitSquareLength ); + const yIndex = Utils.roundSymmetric( ( movableShape.destination.y - this.bounds.minY ) / this.unitSquareLength ); + // Mark all cells occupied by this shape. + for ( let row = 0; row < movableShape.shape.bounds.height / this.unitSquareLength; row++ ) { + for ( let column = 0; column < movableShape.shape.bounds.width / this.unitSquareLength; column++ ) { + this.cells[ xIndex + column ][ yIndex + row ].occupiedBy = operation === 'add' ? movableShape : null; + } + } + }, - // TODO: This is rather ugly. Work with SR to improve or find alternative, or to bake into Axon. Maybe a map. - // @private, remove all observers from a property that have been tagged by this shape placement board. - removeTaggedObservers: function( property ) { + // @private + updateAreaAndTotalPerimeter: function() { + if ( this.compositeShapeProperty.get().exteriorPerimeters.length <= 1 ) { const self = this; - const taggedObservers = []; - property.changedEmitter.listeners.forEach( function( observer ) { - if ( self.listenerTagMatches( observer ) ) { - taggedObservers.push( observer ); - } + let totalArea = 0; + this.residentShapes.forEach( function( residentShape ) { + totalArea += residentShape.shape.bounds.width * residentShape.shape.bounds.height / ( self.unitSquareLength * self.unitSquareLength ); + } ); + let totalPerimeter = 0; + this.compositeShapeProperty.get().exteriorPerimeters.forEach( function( exteriorPerimeter ) { + totalPerimeter += exteriorPerimeter.length; + } ); + this.compositeShapeProperty.get().interiorPerimeters.forEach( function( interiorPerimeter ) { + totalPerimeter += interiorPerimeter.length; } ); - taggedObservers.forEach( function( taggedObserver ) { - property.unlink( taggedObserver ); + this.areaAndPerimeterProperty.set( { + area: totalArea, + perimeter: totalPerimeter } ); - }, + } + else { + // Area and perimeter readings are currently invalid. + this.areaAndPerimeterProperty.set( { + area: AreaBuilderSharedConstants.INVALID_VALUE, + perimeter: AreaBuilderSharedConstants.INVALID_VALUE + } ); + } + }, - // @private Convenience function for returning a cell or null if row or column are out of range. - getCell: function( column, row ) { - if ( column < 0 || row < 0 || column >= this.numColumns || row >= this.numRows ) { - return null; - } - return this.cells[ column ][ row ]; - }, - - // @private Function for getting the occupant of the specified cell, does bounds checking. - getCellOccupant: function( column, row ) { - const cell = this.getCell( column, row ); - return cell ? cell.occupiedBy : null; - }, - - /** - * Set or clear the occupation status of the cells. - * - * @param movableShape - * @param operation - */ - updateCellOccupation: function( movableShape, operation ) { - const xIndex = Utils.roundSymmetric( ( movableShape.destination.x - this.bounds.minX ) / this.unitSquareLength ); - const yIndex = Utils.roundSymmetric( ( movableShape.destination.y - this.bounds.minY ) / this.unitSquareLength ); - // Mark all cells occupied by this shape. - for ( let row = 0; row < movableShape.shape.bounds.height / this.unitSquareLength; row++ ) { - for ( let column = 0; column < movableShape.shape.bounds.width / this.unitSquareLength; column++ ) { - this.cells[ xIndex + column ][ yIndex + row ].occupiedBy = operation === 'add' ? movableShape : null; - } - } - }, - - // @private - updateAreaAndTotalPerimeter: function() { - if ( this.compositeShapeProperty.get().exteriorPerimeters.length <= 1 ) { - const self = this; - let totalArea = 0; - this.residentShapes.forEach( function( residentShape ) { - totalArea += residentShape.shape.bounds.width * residentShape.shape.bounds.height / ( self.unitSquareLength * self.unitSquareLength ); - } ); - let totalPerimeter = 0; - this.compositeShapeProperty.get().exteriorPerimeters.forEach( function( exteriorPerimeter ) { - totalPerimeter += exteriorPerimeter.length; - } ); - this.compositeShapeProperty.get().interiorPerimeters.forEach( function( interiorPerimeter ) { - totalPerimeter += interiorPerimeter.length; - } ); - this.areaAndPerimeterProperty.set( { - area: totalArea, - perimeter: totalPerimeter - } ); - } - else { - // Area and perimeter readings are currently invalid. - this.areaAndPerimeterProperty.set( { - area: AreaBuilderSharedConstants.INVALID_VALUE, - perimeter: AreaBuilderSharedConstants.INVALID_VALUE - } ); - } - }, - - /** - * Convenience function for finding out whether a cell is occupied that handles out of bounds case (returns false). - * @private - * @param column - * @param row - */ - isCellOccupied: function( column, row ) { - if ( column >= this.numColumns || column < 0 || row >= this.numRows || row < 0 ) { - return false; - } - else { - return this.cells[ column ][ row ].occupiedBy !== null; - } - }, - - /** - * Function that returns true if a cell is occupied or if an incoming shape is heading for it. - * @private - * @param column - * @param row - */ - isCellOccupiedNowOrSoon: function( column, row ) { - if ( this.isCellOccupied( column, row ) ) { + /** + * Convenience function for finding out whether a cell is occupied that handles out of bounds case (returns false). + * @private + * @param column + * @param row + */ + isCellOccupied: function( column, row ) { + if ( column >= this.numColumns || column < 0 || row >= this.numRows || row < 0 ) { + return false; + } + else { + return this.cells[ column ][ row ].occupiedBy !== null; + } + }, + + /** + * Function that returns true if a cell is occupied or if an incoming shape is heading for it. + * @private + * @param column + * @param row + */ + isCellOccupiedNowOrSoon: function( column, row ) { + if ( this.isCellOccupied( column, row ) ) { + return true; + } + for ( let i = 0; i < this.incomingShapes.length; i++ ) { + const targetCell = this.modelToCellVector( this.incomingShapes[ i ].destination ); + const normalizedWidth = Utils.roundSymmetric( this.incomingShapes[ i ].shape.bounds.width / this.unitSquareLength ); + const normalizedHeight = Utils.roundSymmetric( this.incomingShapes[ i ].shape.bounds.height / this.unitSquareLength ); + if ( column >= targetCell.x && column < targetCell.x + normalizedWidth && + row >= targetCell.y && row < targetCell.y + normalizedHeight ) { return true; } - for ( let i = 0; i < this.incomingShapes.length; i++ ) { - const targetCell = this.modelToCellVector( this.incomingShapes[ i ].destination ); - const normalizedWidth = Utils.roundSymmetric( this.incomingShapes[ i ].shape.bounds.width / this.unitSquareLength ); - const normalizedHeight = Utils.roundSymmetric( this.incomingShapes[ i ].shape.bounds.height / this.unitSquareLength ); - if ( column >= targetCell.x && column < targetCell.x + normalizedWidth && - row >= targetCell.y && row < targetCell.y + normalizedHeight ) { - return true; - } - } - return false; - }, - - /** - * Get the outer layer of grid points surrounding the given point. The 2nd parameter indicates how many steps away - * from the center 'shell' should be provided. - * @private - * @param point - * @param levelsRemoved - */ - getOuterSurroundingPoints: function( point, levelsRemoved ) { - const self = this; - const normalizedPoints = []; + } + return false; + }, - // Get the closest point in cell coordinates. - const normalizedStartingPoint = new Vector2( - Math.floor( ( point.x - this.bounds.minX ) / this.unitSquareLength ) - levelsRemoved, - Math.floor( ( point.y - this.bounds.minY ) / this.unitSquareLength ) - levelsRemoved - ); + /** + * Get the outer layer of grid points surrounding the given point. The 2nd parameter indicates how many steps away + * from the center 'shell' should be provided. + * @private + * @param point + * @param levelsRemoved + */ + getOuterSurroundingPoints: function( point, levelsRemoved ) { + const self = this; + const normalizedPoints = []; + + // Get the closest point in cell coordinates. + const normalizedStartingPoint = new Vector2( + Math.floor( ( point.x - this.bounds.minX ) / this.unitSquareLength ) - levelsRemoved, + Math.floor( ( point.y - this.bounds.minY ) / this.unitSquareLength ) - levelsRemoved + ); - const squareSize = ( levelsRemoved + 1 ) * 2; + const squareSize = ( levelsRemoved + 1 ) * 2; - for ( let row = 0; row < squareSize; row++ ) { - for ( let column = 0; column < squareSize; column++ ) { - if ( ( row === 0 || row === squareSize - 1 || column === 0 || column === squareSize - 1 ) && - ( column + normalizedStartingPoint.x <= this.numColumns && row + normalizedStartingPoint.y <= this.numRows ) ) { - // This is an outer point, and is valid, so include it. - normalizedPoints.push( new Vector2( column + normalizedStartingPoint.x, row + normalizedStartingPoint.y ) ); - } + for ( let row = 0; row < squareSize; row++ ) { + for ( let column = 0; column < squareSize; column++ ) { + if ( ( row === 0 || row === squareSize - 1 || column === 0 || column === squareSize - 1 ) && + ( column + normalizedStartingPoint.x <= this.numColumns && row + normalizedStartingPoint.y <= this.numRows ) ) { + // This is an outer point, and is valid, so include it. + normalizedPoints.push( new Vector2( column + normalizedStartingPoint.x, row + normalizedStartingPoint.y ) ); } } + } - const outerSurroundingPoints = []; - normalizedPoints.forEach( function( p ) { outerSurroundingPoints.push( self.cellToModelVector( p ) ); } ); - return outerSurroundingPoints; - }, - - /** - * Determine whether it is valid to place the given shape at the given position. For placement to be valid, the - * shape can't overlap with any other shape, and must share at least one side with an occupied space. - * - * @param movableShape - * @param position - * @returns {boolean} - */ - isValidToPlace: function( movableShape, position ) { - const normalizedPosition = this.modelToCellVector( position ); - const normalizedWidth = Utils.roundSymmetric( movableShape.shape.bounds.width / this.unitSquareLength ); - const normalizedHeight = Utils.roundSymmetric( movableShape.shape.bounds.height / this.unitSquareLength ); - let row; - let column; + const outerSurroundingPoints = []; + normalizedPoints.forEach( function( p ) { outerSurroundingPoints.push( self.cellToModelVector( p ) ); } ); + return outerSurroundingPoints; + }, - // Return false if the shape would go off the board if placed at this position. - if ( normalizedPosition.x < 0 || normalizedPosition.x + normalizedWidth > this.numColumns || - normalizedPosition.y < 0 || normalizedPosition.y + normalizedHeight > this.numRows ) { - return false; - } + /** + * Determine whether it is valid to place the given shape at the given position. For placement to be valid, the + * shape can't overlap with any other shape, and must share at least one side with an occupied space. + * + * @param movableShape + * @param position + * @returns {boolean} + */ + isValidToPlace: function( movableShape, position ) { + const normalizedPosition = this.modelToCellVector( position ); + const normalizedWidth = Utils.roundSymmetric( movableShape.shape.bounds.width / this.unitSquareLength ); + const normalizedHeight = Utils.roundSymmetric( movableShape.shape.bounds.height / this.unitSquareLength ); + let row; + let column; + + // Return false if the shape would go off the board if placed at this position. + if ( normalizedPosition.x < 0 || normalizedPosition.x + normalizedWidth > this.numColumns || + normalizedPosition.y < 0 || normalizedPosition.y + normalizedHeight > this.numRows ) { + return false; + } - // If there are no other shapes on the board, any position on the board is valid. - if ( this.residentShapes.length === 0 ) { - return true; - } + // If there are no other shapes on the board, any position on the board is valid. + if ( this.residentShapes.length === 0 ) { + return true; + } - // Return false if this shape overlaps any previously placed shapes. - for ( row = 0; row < normalizedHeight; row++ ) { - for ( column = 0; column < normalizedWidth; column++ ) { - if ( this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row ) ) { - return false; - } + // Return false if this shape overlaps any previously placed shapes. + for ( row = 0; row < normalizedHeight; row++ ) { + for ( column = 0; column < normalizedWidth; column++ ) { + if ( this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row ) ) { + return false; } } + } - // If this board is not set to consolidate shapes, we've done enough, and this position is valid. - if ( !this.formCompositeProperty.get() ) { - return true; - } + // If this board is not set to consolidate shapes, we've done enough, and this position is valid. + if ( !this.formCompositeProperty.get() ) { + return true; + } - // This position is only valid if the shape will share an edge with an already placed shape or an incoming shape, - // since the 'formComposite' mode is enabled. - for ( row = 0; row < normalizedHeight; row++ ) { - for ( column = 0; column < normalizedWidth; column++ ) { - if ( - this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row - 1 ) || - this.isCellOccupiedNowOrSoon( normalizedPosition.x + column - 1, normalizedPosition.y + row ) || - this.isCellOccupiedNowOrSoon( normalizedPosition.x + column + 1, normalizedPosition.y + row ) || - this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row + 1 ) - ) { - return true; - } + // This position is only valid if the shape will share an edge with an already placed shape or an incoming shape, + // since the 'formComposite' mode is enabled. + for ( row = 0; row < normalizedHeight; row++ ) { + for ( column = 0; column < normalizedWidth; column++ ) { + if ( + this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row - 1 ) || + this.isCellOccupiedNowOrSoon( normalizedPosition.x + column - 1, normalizedPosition.y + row ) || + this.isCellOccupiedNowOrSoon( normalizedPosition.x + column + 1, normalizedPosition.y + row ) || + this.isCellOccupiedNowOrSoon( normalizedPosition.x + column, normalizedPosition.y + row + 1 ) + ) { + return true; } } + } - return false; - }, - - /** - * Release all the shapes that are currently on this board and send them to their home positions. - * @public - * @param releaseMode - Controls what the shapes do after release, options are 'fade', 'animateHome', and - * 'jumpHome'. 'jumpHome' is the default. - */ - releaseAllShapes: function( releaseMode ) { - const self = this; + return false; + }, - const shapesToRelease = []; + /** + * Release all the shapes that are currently on this board and send them to their home positions. + * @public + * @param releaseMode - Controls what the shapes do after release, options are 'fade', 'animateHome', and + * 'jumpHome'. 'jumpHome' is the default. + */ + releaseAllShapes: function( releaseMode ) { + const self = this; - // Remove all listeners added to the shapes by this placement board. - this.residentShapes.forEach( function( shape ) { - self.removeTaggedObservers( shape.userControlledProperty ); - shapesToRelease.push( shape ); - } ); - this.incomingShapes.forEach( function( shape ) { - self.removeTaggedObservers( shape.animatingProperty ); - shapesToRelease.push( shape ); - } ); + const shapesToRelease = []; - // Clear out all references to shapes placed on this board. - this.residentShapes.clear(); - this.incomingShapes.length = 0; + // Remove all listeners added to the shapes by this placement board. + this.residentShapes.forEach( function( shape ) { + self.removeTaggedObservers( shape.userControlledProperty ); + shapesToRelease.push( shape ); + } ); + this.incomingShapes.forEach( function( shape ) { + self.removeTaggedObservers( shape.animatingProperty ); + shapesToRelease.push( shape ); + } ); - // Clear the cell array that tracks occupancy. - for ( let row = 0; row < this.numRows; row++ ) { - for ( let column = 0; column < this.numColumns; column++ ) { - this.cells[ column ][ row ].occupiedBy = null; - } - } + // Clear out all references to shapes placed on this board. + this.residentShapes.clear(); + this.incomingShapes.length = 0; - // Tell the shapes what to do after being released. - shapesToRelease.forEach( function( shape ) { - if ( typeof( releaseMode ) === 'undefined' || releaseMode === 'jumpHome' ) { - shape.returnToOrigin( false ); - } - else if ( releaseMode === 'animateHome' ) { - shape.returnToOrigin( true ); - } - else if ( releaseMode === 'fade' ) { - shape.fadeAway(); - } - else { - throw new Error( 'Unsupported release mode for shapes.' ); - } - } ); + // Clear the cell array that tracks occupancy. + for ( let row = 0; row < this.numRows; row++ ) { + for ( let column = 0; column < this.numColumns; column++ ) { + this.cells[ column ][ row ].occupiedBy = null; + } + } - // Update board state. - this.updateAll(); - }, - - // @public - check if a shape is resident on the board - isResidentShape: function( shape ) { - return this.residentShapes.contains( shape ); - }, - - // @private - releaseShape: function( shape ) { - assert && assert( this.isResidentShape( shape ) || this.incomingShapes.contains( shape ), 'Error: An attempt was made to release a shape that is not present.' ); - if ( this.isResidentShape( shape ) ) { - this.removeTaggedObservers( shape.userControlledProperty ); - this.removeResidentShape( shape ); + // Tell the shapes what to do after being released. + shapesToRelease.forEach( function( shape ) { + if ( typeof ( releaseMode ) === 'undefined' || releaseMode === 'jumpHome' ) { + shape.returnToOrigin( false ); + } + else if ( releaseMode === 'animateHome' ) { + shape.returnToOrigin( true ); } - else if ( this.incomingShapes.indexOf( shape ) >= 0 ) { - this.removeTaggedObservers( shape.animatingProperty ); - this.incomingShapes.splice( this.incomingShapes.indexOf( shape ), 1 ); + else if ( releaseMode === 'fade' ) { + shape.fadeAway(); } - }, - - //@private - cellToModelCoords: function( column, row ) { - return new Vector2( column * this.unitSquareLength + this.bounds.minX, row * this.unitSquareLength + this.bounds.minY ); - }, - - //@private - cellToModelVector: function( v ) { - return this.cellToModelCoords( v.x, v.y ); - }, - - //@private - modelToCellCoords: function( x, y ) { - return new Vector2( Utils.roundSymmetric( ( x - this.bounds.minX ) / this.unitSquareLength ), - Utils.roundSymmetric( ( y - this.bounds.minY ) / this.unitSquareLength ) ); - }, - - //@private - modelToCellVector: function( v ) { - return this.modelToCellCoords( v.x, v.y ); - }, - - //@private - createShapeFromPerimeterPoints: function( perimeterPoints ) { - const perimeterShape = new Shape(); + else { + throw new Error( 'Unsupported release mode for shapes.' ); + } + } ); + + // Update board state. + this.updateAll(); + }, + + // @public - check if a shape is resident on the board + isResidentShape: function( shape ) { + return this.residentShapes.contains( shape ); + }, + + // @private + releaseShape: function( shape ) { + assert && assert( this.isResidentShape( shape ) || this.incomingShapes.contains( shape ), 'Error: An attempt was made to release a shape that is not present.' ); + if ( this.isResidentShape( shape ) ) { + this.removeTaggedObservers( shape.userControlledProperty ); + this.removeResidentShape( shape ); + } + else if ( this.incomingShapes.indexOf( shape ) >= 0 ) { + this.removeTaggedObservers( shape.animatingProperty ); + this.incomingShapes.splice( this.incomingShapes.indexOf( shape ), 1 ); + } + }, + + //@private + cellToModelCoords: function( column, row ) { + return new Vector2( column * this.unitSquareLength + this.bounds.minX, row * this.unitSquareLength + this.bounds.minY ); + }, + + //@private + cellToModelVector: function( v ) { + return this.cellToModelCoords( v.x, v.y ); + }, + + //@private + modelToCellCoords: function( x, y ) { + return new Vector2( Utils.roundSymmetric( ( x - this.bounds.minX ) / this.unitSquareLength ), + Utils.roundSymmetric( ( y - this.bounds.minY ) / this.unitSquareLength ) ); + }, + + //@private + modelToCellVector: function( v ) { + return this.modelToCellCoords( v.x, v.y ); + }, + + //@private + createShapeFromPerimeterPoints: function( perimeterPoints ) { + const perimeterShape = new Shape(); + perimeterShape.moveToPoint( perimeterPoints[ 0 ] ); + for ( let i = 1; i < perimeterPoints.length; i++ ) { + perimeterShape.lineToPoint( perimeterPoints[ i ] ); + } + perimeterShape.close(); // Shouldn't be needed, but best to be sure. + return perimeterShape; + }, + + //@private + createShapeFromPerimeterList: function( perimeters ) { + const perimeterShape = new Shape(); + perimeters.forEach( function( perimeterPoints ) { perimeterShape.moveToPoint( perimeterPoints[ 0 ] ); for ( let i = 1; i < perimeterPoints.length; i++ ) { perimeterShape.lineToPoint( perimeterPoints[ i ] ); } - perimeterShape.close(); // Shouldn't be needed, but best to be sure. - return perimeterShape; - }, - - //@private - createShapeFromPerimeterList: function( perimeters ) { - const perimeterShape = new Shape(); - perimeters.forEach( function( perimeterPoints ) { - perimeterShape.moveToPoint( perimeterPoints[ 0 ] ); - for ( let i = 1; i < perimeterPoints.length; i++ ) { - perimeterShape.lineToPoint( perimeterPoints[ i ] ); - } - perimeterShape.close(); - } ); - return perimeterShape; - }, - - /** - * Marching squares algorithm for scanning the perimeter of a shape, see - * https://en.wikipedia.org/wiki/Marching_squares or search the Internet for 'Marching Squares Algorithm' for more - * information on this. - * @private - */ - scanPerimeter: function( windowStart ) { - const scanWindow = windowStart.copy(); - let scanComplete = false; - const perimeterPoints = []; - let previousMovementVector = MOVEMENT_VECTORS.up; // Init this way allows algorithm to work for interior perimeters. - while ( !scanComplete ) { - - // Scan the current four-pixel area. - const upLeftOccupied = this.isCellOccupied( scanWindow.x - 1, scanWindow.y - 1 ); - const upRightOccupied = this.isCellOccupied( scanWindow.x, scanWindow.y - 1 ); - const downLeftOccupied = this.isCellOccupied( scanWindow.x - 1, scanWindow.y ); - const downRightOccupied = this.isCellOccupied( scanWindow.x, scanWindow.y ); - - // Map the scan to the one of 16 possible states. - let marchingSquaresState = 0; - if ( upLeftOccupied ) { marchingSquaresState |= 1; } - if ( upRightOccupied ) { marchingSquaresState |= 2; } - if ( downLeftOccupied ) { marchingSquaresState |= 4; } - if ( downRightOccupied ) { marchingSquaresState |= 8; } - - assert && assert( - marchingSquaresState !== 0 && marchingSquaresState !== 15, - 'Marching squares algorithm reached invalid state.' - ); - - // Convert and add this point to the perimeter points. - perimeterPoints.push( this.cellToModelCoords( scanWindow.x, scanWindow.y ) ); - - // Move the scan window to the next position. - const movementVector = SCAN_AREA_MOVEMENT_FUNCTIONS[ marchingSquaresState ]( previousMovementVector ); - scanWindow.add( movementVector ); - previousMovementVector = movementVector; - - if ( scanWindow.equals( windowStart ) ) { - scanComplete = true; - } - } - return perimeterPoints; - }, + perimeterShape.close(); + } ); + return perimeterShape; + }, - // @private, Update the exterior and interior perimeters. - updatePerimeters: function() { - const self = this; + /** + * Marching squares algorithm for scanning the perimeter of a shape, see + * https://en.wikipedia.org/wiki/Marching_squares or search the Internet for 'Marching Squares Algorithm' for more + * information on this. + * @private + */ + scanPerimeter: function( windowStart ) { + const scanWindow = windowStart.copy(); + let scanComplete = false; + const perimeterPoints = []; + let previousMovementVector = MOVEMENT_VECTORS.up; // Init this way allows algorithm to work for interior perimeters. + while ( !scanComplete ) { + + // Scan the current four-pixel area. + const upLeftOccupied = this.isCellOccupied( scanWindow.x - 1, scanWindow.y - 1 ); + const upRightOccupied = this.isCellOccupied( scanWindow.x, scanWindow.y - 1 ); + const downLeftOccupied = this.isCellOccupied( scanWindow.x - 1, scanWindow.y ); + const downRightOccupied = this.isCellOccupied( scanWindow.x, scanWindow.y ); + + // Map the scan to the one of 16 possible states. + let marchingSquaresState = 0; + if ( upLeftOccupied ) { marchingSquaresState |= 1; } + if ( upRightOccupied ) { marchingSquaresState |= 2; } + if ( downLeftOccupied ) { marchingSquaresState |= 4; } + if ( downRightOccupied ) { marchingSquaresState |= 8; } + + assert && assert( + marchingSquaresState !== 0 && marchingSquaresState !== 15, + 'Marching squares algorithm reached invalid state.' + ); + + // Convert and add this point to the perimeter points. + perimeterPoints.push( this.cellToModelCoords( scanWindow.x, scanWindow.y ) ); + + // Move the scan window to the next position. + const movementVector = SCAN_AREA_MOVEMENT_FUNCTIONS[ marchingSquaresState ]( previousMovementVector ); + scanWindow.add( movementVector ); + previousMovementVector = movementVector; - // The perimeters can only be computed for a single consolidated shape. - if ( !this.formCompositeProperty.get() || this.residentShapes.length === 0 ) { - this.perimeter = 0; - this.compositeShapeProperty.reset(); + if ( scanWindow.equals( windowStart ) ) { + scanComplete = true; } - else { // Do the full-blown perimeter calculation - let row; - let column; - const exteriorPerimeters = []; - - // Identify each outer perimeter. There may be more than one if the user is moving a shape that was previously - // on this board, since any orphaned shapes are not released until the move is complete. - const contiguousCellGroups = this.identifyContiguousCellGroups(); - contiguousCellGroups.forEach( function( cellGroup ) { - - // Find the top left square of this group to use as a starting point. - let topLeftCell = null; - cellGroup.forEach( function( cell ) { - if ( topLeftCell === null || cell.row < topLeftCell.row || ( cell.row === topLeftCell.row && cell.column < topLeftCell.column ) ) { - topLeftCell = cell; - } - } ); + } + return perimeterPoints; + }, - // Scan the outer perimeter and add to list. - const topLeftCellOfGroup = new Vector2( topLeftCell.column, topLeftCell.row ); - exteriorPerimeters.push( self.scanPerimeter( topLeftCellOfGroup ) ); + // @private, Update the exterior and interior perimeters. + updatePerimeters: function() { + const self = this; + + // The perimeters can only be computed for a single consolidated shape. + if ( !this.formCompositeProperty.get() || this.residentShapes.length === 0 ) { + this.perimeter = 0; + this.compositeShapeProperty.reset(); + } + else { // Do the full-blown perimeter calculation + let row; + let column; + const exteriorPerimeters = []; + + // Identify each outer perimeter. There may be more than one if the user is moving a shape that was previously + // on this board, since any orphaned shapes are not released until the move is complete. + const contiguousCellGroups = this.identifyContiguousCellGroups(); + contiguousCellGroups.forEach( function( cellGroup ) { + + // Find the top left square of this group to use as a starting point. + let topLeftCell = null; + cellGroup.forEach( function( cell ) { + if ( topLeftCell === null || cell.row < topLeftCell.row || ( cell.row === topLeftCell.row && cell.column < topLeftCell.column ) ) { + topLeftCell = cell; + } } ); - // Scan for empty spaces enclosed within the outer perimeter(s). - const outlineShape = this.createShapeFromPerimeterList( exteriorPerimeters ); - let enclosedSpaces = []; - for ( row = 0; row < this.numRows; row++ ) { - for ( column = 0; column < this.numColumns; column++ ) { - if ( !this.isCellOccupied( column, row ) ) { - // This cell is empty. Test if it is within the outline perimeter. - const cellCenterInModel = this.cellToModelCoords( column, row ).addXY( this.unitSquareLength / 2, this.unitSquareLength / 2 ); - if ( outlineShape.containsPoint( cellCenterInModel ) ) { - enclosedSpaces.push( new Vector2( column, row ) ); - } + // Scan the outer perimeter and add to list. + const topLeftCellOfGroup = new Vector2( topLeftCell.column, topLeftCell.row ); + exteriorPerimeters.push( self.scanPerimeter( topLeftCellOfGroup ) ); + } ); + + // Scan for empty spaces enclosed within the outer perimeter(s). + const outlineShape = this.createShapeFromPerimeterList( exteriorPerimeters ); + let enclosedSpaces = []; + for ( row = 0; row < this.numRows; row++ ) { + for ( column = 0; column < this.numColumns; column++ ) { + if ( !this.isCellOccupied( column, row ) ) { + // This cell is empty. Test if it is within the outline perimeter. + const cellCenterInModel = this.cellToModelCoords( column, row ).addXY( this.unitSquareLength / 2, this.unitSquareLength / 2 ); + if ( outlineShape.containsPoint( cellCenterInModel ) ) { + enclosedSpaces.push( new Vector2( column, row ) ); } } } + } - // Map the internal perimeters - const interiorPerimeters = []; - while ( enclosedSpaces.length > 0 ) { - - // Locate the top left most space - var topLeftSpace = enclosedSpaces[ 0 ]; - enclosedSpaces.forEach( function( cell ) { - if ( cell.y < topLeftSpace.y || ( cell.y === topLeftSpace.y && cell.x < topLeftSpace.x ) ) { - topLeftSpace = cell; - } - } ); - - // Map the interior perimeter. - const enclosedPerimeterPoints = this.scanPerimeter( topLeftSpace ); - interiorPerimeters.push( enclosedPerimeterPoints ); - - // Identify and save all spaces not enclosed by this perimeter. - var perimeterShape = this.createShapeFromPerimeterPoints( enclosedPerimeterPoints ); - var leftoverEmptySpaces = []; - enclosedSpaces.forEach( function( enclosedSpace ) { - const positionPoint = self.cellToModelCoords( enclosedSpace.x, enclosedSpace.y ); - const centerPoint = positionPoint.plusXY( self.unitSquareLength / 2, self.unitSquareLength / 2 ); - if ( !perimeterShape.containsPoint( centerPoint ) ) { - // This space is not contained in the perimeter that was just mapped. - leftoverEmptySpaces.push( enclosedSpace ); - } - } ); + // Map the internal perimeters + const interiorPerimeters = []; + while ( enclosedSpaces.length > 0 ) { - // Set up for the next time through the loop. - enclosedSpaces = leftoverEmptySpaces; - } + // Locate the top left most space + var topLeftSpace = enclosedSpaces[ 0 ]; + enclosedSpaces.forEach( function( cell ) { + if ( cell.y < topLeftSpace.y || ( cell.y === topLeftSpace.y && cell.x < topLeftSpace.x ) ) { + topLeftSpace = cell; + } + } ); - // Update externally visible properties. Only update the perimeters if they have changed in order to minimize - // work done in the view. - if ( !( this.perimeterListsEqual( exteriorPerimeters, this.compositeShapeProperty.get().exteriorPerimeters ) && - this.perimeterListsEqual( interiorPerimeters, this.compositeShapeProperty.get().interiorPerimeters ) ) ) { - this.compositeShapeProperty.set( new PerimeterShape( exteriorPerimeters, interiorPerimeters, this.unitSquareLength, { - fillColor: this.compositeShapeFillColor, - edgeColor: this.compositeShapeEdgeColor - } ) ); - } - } - }, + // Map the interior perimeter. + const enclosedPerimeterPoints = this.scanPerimeter( topLeftSpace ); + interiorPerimeters.push( enclosedPerimeterPoints ); + + // Identify and save all spaces not enclosed by this perimeter. + var perimeterShape = this.createShapeFromPerimeterPoints( enclosedPerimeterPoints ); + var leftoverEmptySpaces = []; + enclosedSpaces.forEach( function( enclosedSpace ) { + const positionPoint = self.cellToModelCoords( enclosedSpace.x, enclosedSpace.y ); + const centerPoint = positionPoint.plusXY( self.unitSquareLength / 2, self.unitSquareLength / 2 ); + if ( !perimeterShape.containsPoint( centerPoint ) ) { + // This space is not contained in the perimeter that was just mapped. + leftoverEmptySpaces.push( enclosedSpace ); + } + } ); - // @private - perimeterPointsEqual: function( perimeter1, perimeter2 ) { - assert && assert( Array.isArray( perimeter1 ) && Array.isArray( perimeter2 ), 'Invalid parameters for perimeterPointsEqual' ); - if ( perimeter1.length !== perimeter2.length ) { - return false; + // Set up for the next time through the loop. + enclosedSpaces = leftoverEmptySpaces; } - return perimeter1.every( function( point, index ) { - return ( point.equals( perimeter2[ index ] ) ); - } ); - }, - // @private - perimeterListsEqual: function( perimeterList1, perimeterList2 ) { - assert && assert( Array.isArray( perimeterList1 ) && Array.isArray( perimeterList2 ), 'Invalid parameters for perimeterListsEqual' ); - if ( perimeterList1.length !== perimeterList2.length ) { - return false; + // Update externally visible properties. Only update the perimeters if they have changed in order to minimize + // work done in the view. + if ( !( this.perimeterListsEqual( exteriorPerimeters, this.compositeShapeProperty.get().exteriorPerimeters ) && + this.perimeterListsEqual( interiorPerimeters, this.compositeShapeProperty.get().interiorPerimeters ) ) ) { + this.compositeShapeProperty.set( new PerimeterShape( exteriorPerimeters, interiorPerimeters, this.unitSquareLength, { + fillColor: this.compositeShapeFillColor, + edgeColor: this.compositeShapeEdgeColor + } ) ); } - const self = this; - return perimeterList1.every( function( perimeterPoints, index ) { - return self.perimeterPointsEqual( perimeterPoints, perimeterList2[ index ] ); - } ); - }, - - /** - * Identify all cells that are adjacent to the provided cell and that are currently occupied by a shape. Only - * shapes that share an edge are considered to be adjacent, shapes that only touch at the corner don't count. This - * uses recursion. It also relies on a flag that must be cleared for the cells before calling this algorithm. The - * flag is done for efficiency, but this could be changed to search through the list of cells in the cell group if - * that flag method is too weird. - * - * @private - * @param startCell - * @param cellGroup - */ - identifyAdjacentOccupiedCells: function( startCell, cellGroup ) { - assert && assert( startCell.occupiedBy !== null, 'Usage error: Unoccupied cell passed to group identification.' ); - assert && assert( !startCell.cataloged, 'Usage error: Cataloged cell passed to group identification algorithm.' ); - const self = this; + } + }, - // Catalog this cell. - cellGroup.push( startCell ); - startCell.cataloged = true; + // @private + perimeterPointsEqual: function( perimeter1, perimeter2 ) { + assert && assert( Array.isArray( perimeter1 ) && Array.isArray( perimeter2 ), 'Invalid parameters for perimeterPointsEqual' ); + if ( perimeter1.length !== perimeter2.length ) { + return false; + } + return perimeter1.every( function( point, index ) { + return ( point.equals( perimeter2[ index ] ) ); + } ); + }, - // Check occupancy of each of the four adjecent cells. - Object.keys( MOVEMENT_VECTORS ).forEach( function( key ) { - const movementVector = MOVEMENT_VECTORS[ key ]; - const adjacentCell = self.getCell( startCell.column + movementVector.x, startCell.row + movementVector.y ); - if ( adjacentCell !== null && adjacentCell.occupiedBy !== null && !adjacentCell.cataloged ) { - self.identifyAdjacentOccupiedCells( adjacentCell, cellGroup ); - } - } ); - }, - - /** - * Returns an array representing all contiguous groups of occupied cells. Each group is a list of cells. - * @private - * @returns {Array} - */ - identifyContiguousCellGroups: function() { - - // Make a list of positions for all occupied cells. - let ungroupedOccupiedCells = []; - for ( let row = 0; row < this.numRows; row++ ) { - for ( let column = 0; column < this.numColumns; column++ ) { - const cell = this.cells[ column ][ row ]; - if ( cell.occupiedBy !== null ) { - ungroupedOccupiedCells.push( this.cells[ column ][ row ] ); - // Clear the flag used by the search algorithm. - cell.cataloged = false; - } - } - } + // @private + perimeterListsEqual: function( perimeterList1, perimeterList2 ) { + assert && assert( Array.isArray( perimeterList1 ) && Array.isArray( perimeterList2 ), 'Invalid parameters for perimeterListsEqual' ); + if ( perimeterList1.length !== perimeterList2.length ) { + return false; + } + const self = this; + return perimeterList1.every( function( perimeterPoints, index ) { + return self.perimeterPointsEqual( perimeterPoints, perimeterList2[ index ] ); + } ); + }, - // Identify the interconnected groups of cells. - const contiguousCellGroups = []; - while ( ungroupedOccupiedCells.length > 0 ) { - const cellGroup = []; - this.identifyAdjacentOccupiedCells( ungroupedOccupiedCells[ 0 ], cellGroup ); - contiguousCellGroups.push( cellGroup ); - ungroupedOccupiedCells = _.difference( ungroupedOccupiedCells, cellGroup ); + /** + * Identify all cells that are adjacent to the provided cell and that are currently occupied by a shape. Only + * shapes that share an edge are considered to be adjacent, shapes that only touch at the corner don't count. This + * uses recursion. It also relies on a flag that must be cleared for the cells before calling this algorithm. The + * flag is done for efficiency, but this could be changed to search through the list of cells in the cell group if + * that flag method is too weird. + * + * @private + * @param startCell + * @param cellGroup + */ + identifyAdjacentOccupiedCells: function( startCell, cellGroup ) { + assert && assert( startCell.occupiedBy !== null, 'Usage error: Unoccupied cell passed to group identification.' ); + assert && assert( !startCell.cataloged, 'Usage error: Cataloged cell passed to group identification algorithm.' ); + const self = this; + + // Catalog this cell. + cellGroup.push( startCell ); + startCell.cataloged = true; + + // Check occupancy of each of the four adjecent cells. + Object.keys( MOVEMENT_VECTORS ).forEach( function( key ) { + const movementVector = MOVEMENT_VECTORS[ key ]; + const adjacentCell = self.getCell( startCell.column + movementVector.x, startCell.row + movementVector.y ); + if ( adjacentCell !== null && adjacentCell.occupiedBy !== null && !adjacentCell.cataloged ) { + self.identifyAdjacentOccupiedCells( adjacentCell, cellGroup ); } + } ); + }, - return contiguousCellGroups; - }, - - /** - * Release any shapes that are resident on the board but that don't share at least one edge with the largest - * composite shape on the board. Such shapes are referred to as 'orphans' and, when release, they are sent back to - * the position where they were created. - */ - releaseAnyOrphans: function() { - - // Orphans can only exist when operating in the 'formComposite' mode. - if ( this.formCompositeProperty.get() ) { - const self = this; - const contiguousCellGroups = this.identifyContiguousCellGroups(); - - if ( contiguousCellGroups.length > 1 ) { - // There are orphans that should be released. Determine which ones. - let indexOfRetainedGroup = 0; - contiguousCellGroups.forEach( function( group, index ) { - if ( group.length > contiguousCellGroups[ indexOfRetainedGroup ].length ) { - indexOfRetainedGroup = index; - } - } ); - - contiguousCellGroups.forEach( function( group, groupIndex ) { - if ( groupIndex !== indexOfRetainedGroup ) { - group.forEach( function( cell ) { - const movableShape = cell.occupiedBy; - if ( movableShape !== null ) { // Need to test in case a previously released shape covered multiple cells. - self.releaseShape( movableShape ); - movableShape.returnToOrigin( true ); - } - } ); - } - } ); + /** + * Returns an array representing all contiguous groups of occupied cells. Each group is a list of cells. + * @private + * @returns {Array} + */ + identifyContiguousCellGroups: function() { + + // Make a list of positions for all occupied cells. + let ungroupedOccupiedCells = []; + for ( let row = 0; row < this.numRows; row++ ) { + for ( let column = 0; column < this.numColumns; column++ ) { + const cell = this.cells[ column ][ row ]; + if ( cell.occupiedBy !== null ) { + ungroupedOccupiedCells.push( this.cells[ column ][ row ] ); + // Clear the flag used by the search algorithm. + cell.cataloged = false; } } - }, - - /** - * Replace one of the composite shapes that currently resides on this board with a set of unit squares. This is - * generally done when a composite shape was placed on the board but we now want it treated as a bunch of smaller - * unit squares instead. - * - * @param {MovableShape} originalShape - * @param {Array} unitSquares Pieces that comprise the original shape, MUST BE CORRECTLY LOCATED - * since this method does not relocate them to the appropriate places. - */ - replaceShapeWithUnitSquares: function( originalShape, unitSquares ) { - assert && assert( this.isResidentShape( originalShape ), 'Error: Specified shape to be replaced does not appear to be present.' ); + } + + // Identify the interconnected groups of cells. + const contiguousCellGroups = []; + while ( ungroupedOccupiedCells.length > 0 ) { + const cellGroup = []; + this.identifyAdjacentOccupiedCells( ungroupedOccupiedCells[ 0 ], cellGroup ); + contiguousCellGroups.push( cellGroup ); + ungroupedOccupiedCells = _.difference( ungroupedOccupiedCells, cellGroup ); + } + + return contiguousCellGroups; + }, + + /** + * Release any shapes that are resident on the board but that don't share at least one edge with the largest + * composite shape on the board. Such shapes are referred to as 'orphans' and, when release, they are sent back to + * the position where they were created. + */ + releaseAnyOrphans: function() { + + // Orphans can only exist when operating in the 'formComposite' mode. + if ( this.formCompositeProperty.get() ) { const self = this; + const contiguousCellGroups = this.identifyContiguousCellGroups(); + + if ( contiguousCellGroups.length > 1 ) { + // There are orphans that should be released. Determine which ones. + let indexOfRetainedGroup = 0; + contiguousCellGroups.forEach( function( group, index ) { + if ( group.length > contiguousCellGroups[ indexOfRetainedGroup ].length ) { + indexOfRetainedGroup = index; + } + } ); + + contiguousCellGroups.forEach( function( group, groupIndex ) { + if ( groupIndex !== indexOfRetainedGroup ) { + group.forEach( function( cell ) { + const movableShape = cell.occupiedBy; + if ( movableShape !== null ) { // Need to test in case a previously released shape covered multiple cells. + self.releaseShape( movableShape ); + movableShape.returnToOrigin( true ); + } + } ); + } + } ); + } + } + }, - // The following add and remove operations do not use the add and remove methods in order to avoid releasing - // orphans (which could cause undesired behavior) and attribute updates (which are unnecessary). - this.residentShapes.remove( originalShape ); - this.updateCellOccupation( originalShape, 'remove' ); + /** + * Replace one of the composite shapes that currently resides on this board with a set of unit squares. This is + * generally done when a composite shape was placed on the board but we now want it treated as a bunch of smaller + * unit squares instead. + * + * @param {MovableShape} originalShape + * @param {Array} unitSquares Pieces that comprise the original shape, MUST BE CORRECTLY LOCATED + * since this method does not relocate them to the appropriate places. + */ + replaceShapeWithUnitSquares: function( originalShape, unitSquares ) { + assert && assert( this.isResidentShape( originalShape ), 'Error: Specified shape to be replaced does not appear to be present.' ); + const self = this; - unitSquares.forEach( function( movableUnitSquare ) { - self.residentShapes.push( movableUnitSquare ); + // The following add and remove operations do not use the add and remove methods in order to avoid releasing + // orphans (which could cause undesired behavior) and attribute updates (which are unnecessary). + this.residentShapes.remove( originalShape ); + this.updateCellOccupation( originalShape, 'remove' ); - // Set up a listener to remove this shape when the user grabs it. - self.addRemovalListener( movableUnitSquare ); + unitSquares.forEach( function( movableUnitSquare ) { + self.residentShapes.push( movableUnitSquare ); - // Make some state updates. - self.updateCellOccupation( movableUnitSquare, 'add' ); - } ); - }, + // Set up a listener to remove this shape when the user grabs it. + self.addRemovalListener( movableUnitSquare ); - /** - * adds a listener that will remove this shape from the board when the user grabs it - * @param {MovableShape} movableShape - * @private - */ - addRemovalListener: function( movableShape ) { + // Make some state updates. + self.updateCellOccupation( movableUnitSquare, 'add' ); + } ); + }, - const self = this; + /** + * adds a listener that will remove this shape from the board when the user grabs it + * @param {MovableShape} movableShape + * @private + */ + addRemovalListener: function( movableShape ) { - function removalListener( userControlled ) { - assert && assert( - userControlled === true, - 'should only see shapes become user controlled after being added to a placement board' - ); - self.removeResidentShape( movableShape ); - movableShape.userControlledProperty.unlink( removalListener ); - } + const self = this; - this.tagListener( removalListener ); - movableShape.userControlledProperty.lazyLink( removalListener ); - }, - - // @public, set colors used for the composite shape shown for this board - setCompositeShapeColorScheme: function( fillColor, edgeColor ) { - this.compositeShapeFillColor = fillColor; - this.compositeShapeEdgeColor = edgeColor; - }, - - // @private, Update perimeter points, placement positions, total area, and total perimeter. - updateAll: function() { - if ( !this.updatesSuspended ) { - this.updatePerimeters(); - this.updateAreaAndTotalPerimeter(); - } - }, - - /** - * This method suspends updates so that a block of squares can be added without having to all the recalculations - * for each one. This is generally done for performance reasons in cases such as depicting the solution to a - * challenge in the game. The flag is automatically cleared when the last incoming shape is added as a resident - * shape. - * @public - */ - suspendUpdatesForBlockAdd: function() { - this.updatesSuspended = true; - }, - - /** - * Set the background shape. The shape can optionally be centered horizontally and vertically when placed on the - * board. - * - * @public - * @param {PerimeterShape} perimeterShape The new background perimeterShape, or null to set no background - * perimeterShape. - * @param {boolean} centered True if the perimeterShape should be centered on the board (but still aligned with grid). - */ - setBackgroundShape: function( perimeterShape, centered ) { - if ( perimeterShape === null ) { - this.backgroundShapeProperty.reset(); + function removalListener( userControlled ) { + assert && assert( + userControlled === true, + 'should only see shapes become user controlled after being added to a placement board' + ); + self.removeResidentShape( movableShape ); + movableShape.userControlledProperty.unlink( removalListener ); + } + + this.tagListener( removalListener ); + movableShape.userControlledProperty.lazyLink( removalListener ); + }, + + // @public, set colors used for the composite shape shown for this board + setCompositeShapeColorScheme: function( fillColor, edgeColor ) { + this.compositeShapeFillColor = fillColor; + this.compositeShapeEdgeColor = edgeColor; + }, + + // @private, Update perimeter points, placement positions, total area, and total perimeter. + updateAll: function() { + if ( !this.updatesSuspended ) { + this.updatePerimeters(); + this.updateAreaAndTotalPerimeter(); + } + }, + + /** + * This method suspends updates so that a block of squares can be added without having to all the recalculations + * for each one. This is generally done for performance reasons in cases such as depicting the solution to a + * challenge in the game. The flag is automatically cleared when the last incoming shape is added as a resident + * shape. + * @public + */ + suspendUpdatesForBlockAdd: function() { + this.updatesSuspended = true; + }, + + /** + * Set the background shape. The shape can optionally be centered horizontally and vertically when placed on the + * board. + * + * @public + * @param {PerimeterShape} perimeterShape The new background perimeterShape, or null to set no background + * perimeterShape. + * @param {boolean} centered True if the perimeterShape should be centered on the board (but still aligned with grid). + */ + setBackgroundShape: function( perimeterShape, centered ) { + if ( perimeterShape === null ) { + this.backgroundShapeProperty.reset(); + } + else { + assert && assert( perimeterShape instanceof PerimeterShape, 'Background perimeterShape must be a PerimeterShape.' ); + assert && assert( perimeterShape.getWidth() % this.unitSquareLength === 0 && perimeterShape.getHeight() % this.unitSquareLength === 0, + 'Background shape width and height must be integer multiples of the unit square size.' ); + if ( centered ) { + const xOffset = this.bounds.minX + Math.floor( ( ( this.bounds.width - perimeterShape.getWidth() ) / 2 ) / this.unitSquareLength ) * this.unitSquareLength; + const yOffset = this.bounds.minY + Math.floor( ( ( this.bounds.height - perimeterShape.getHeight() ) / 2 ) / this.unitSquareLength ) * this.unitSquareLength; + this.backgroundShapeProperty.set( perimeterShape.translated( xOffset, yOffset ) ); } else { - assert && assert( perimeterShape instanceof PerimeterShape, 'Background perimeterShape must be a PerimeterShape.' ); - assert && assert( perimeterShape.getWidth() % this.unitSquareLength === 0 && perimeterShape.getHeight() % this.unitSquareLength === 0, - 'Background shape width and height must be integer multiples of the unit square size.' ); - if ( centered ) { - const xOffset = this.bounds.minX + Math.floor( ( ( this.bounds.width - perimeterShape.getWidth() ) / 2 ) / this.unitSquareLength ) * this.unitSquareLength; - const yOffset = this.bounds.minY + Math.floor( ( ( this.bounds.height - perimeterShape.getHeight() ) / 2 ) / this.unitSquareLength ) * this.unitSquareLength; - this.backgroundShapeProperty.set( perimeterShape.translated( xOffset, yOffset ) ); - } - else { - this.backgroundShapeProperty.set( perimeterShape ); - } + this.backgroundShapeProperty.set( perimeterShape ); } } - } ); -} ); + } +} ); \ No newline at end of file diff --git a/js/common/view/AreaBuilderControlPanel.js b/js/common/view/AreaBuilderControlPanel.js index b9d84e5..5a731bf 100644 --- a/js/common/view/AreaBuilderControlPanel.js +++ b/js/common/view/AreaBuilderControlPanel.js @@ -6,84 +6,81 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Checkbox = require( 'SUN/Checkbox' ); - const DimensionsIcon = require( 'AREA_BUILDER/common/view/DimensionsIcon' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Panel = require( 'SUN/Panel' ); - const Property = require( 'AXON/Property' ); - const VBox = require( 'SCENERY/nodes/VBox' ); +import Property from '../../../../axon/js/Property.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import VBox from '../../../../scenery/js/nodes/VBox.js'; +import Checkbox from '../../../../sun/js/Checkbox.js'; +import Panel from '../../../../sun/js/Panel.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import DimensionsIcon from './DimensionsIcon.js'; +import Grid from './Grid.js'; - // constants - const BACKGROUND_COLOR = AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR; - const PANEL_OPTIONS = { fill: BACKGROUND_COLOR, yMargin: 10, xMargin: 20 }; +// constants +const BACKGROUND_COLOR = AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR; +const PANEL_OPTIONS = { fill: BACKGROUND_COLOR, yMargin: 10, xMargin: 20 }; - /** - * @param showGridProperty - * @param showDimensionsProperty - * @param {Object} [options] - * @constructor - */ - function AreaBuilderControlPanel( showGridProperty, showDimensionsProperty, options ) { - Node.call( this ); +/** + * @param showGridProperty + * @param showDimensionsProperty + * @param {Object} [options] + * @constructor + */ +function AreaBuilderControlPanel( showGridProperty, showDimensionsProperty, options ) { + Node.call( this ); - // Properties that control which elements are visible and which are hidden. This constitutes the primary API. - this.gridControlVisibleProperty = new Property( true ); - this.dimensionsControlVisibleProperty = new Property( true ); + // Properties that control which elements are visible and which are hidden. This constitutes the primary API. + this.gridControlVisibleProperty = new Property( true ); + this.dimensionsControlVisibleProperty = new Property( true ); - // Create the controls and labels - const gridCheckbox = new Checkbox( - new Grid( new Bounds2( 0, 0, 40, 40 ), 10, { stroke: '#b0b0b0' } ), - showGridProperty, - { spacing: 15 } - ); - this.dimensionsIcon = new DimensionsIcon(); // @public so that the icon style can be set - const dimensionsCheckbox = new Checkbox( this.dimensionsIcon, showDimensionsProperty, { spacing: 15 } ); + // Create the controls and labels + const gridCheckbox = new Checkbox( + new Grid( new Bounds2( 0, 0, 40, 40 ), 10, { stroke: '#b0b0b0' } ), + showGridProperty, + { spacing: 15 } + ); + this.dimensionsIcon = new DimensionsIcon(); // @public so that the icon style can be set + const dimensionsCheckbox = new Checkbox( this.dimensionsIcon, showDimensionsProperty, { spacing: 15 } ); - // Create the panel. - const vBox = new VBox( { - children: [ - gridCheckbox, - dimensionsCheckbox - ], - spacing: 8, - align: 'left' - } ); - this.addChild( new Panel( vBox, PANEL_OPTIONS ) ); + // Create the panel. + const vBox = new VBox( { + children: [ + gridCheckbox, + dimensionsCheckbox + ], + spacing: 8, + align: 'left' + } ); + this.addChild( new Panel( vBox, PANEL_OPTIONS ) ); - // Add/remove the grid visibility control. - this.gridControlVisibleProperty.link( function( gridControlVisible ) { - if ( gridControlVisible && !vBox.hasChild( gridCheckbox ) ) { - vBox.insertChild( 0, gridCheckbox ); - } - else if ( !gridControlVisible && vBox.hasChild( gridCheckbox ) ) { - vBox.removeChild( gridCheckbox ); - } - } ); + // Add/remove the grid visibility control. + this.gridControlVisibleProperty.link( function( gridControlVisible ) { + if ( gridControlVisible && !vBox.hasChild( gridCheckbox ) ) { + vBox.insertChild( 0, gridCheckbox ); + } + else if ( !gridControlVisible && vBox.hasChild( gridCheckbox ) ) { + vBox.removeChild( gridCheckbox ); + } + } ); - // Add/remove the dimension visibility control. - this.dimensionsControlVisibleProperty.link( function( dimensionsControlVisible ) { - if ( dimensionsControlVisible && !vBox.hasChild( dimensionsCheckbox ) ) { - // Insert at bottom. - vBox.insertChild( vBox.getChildrenCount(), dimensionsCheckbox ); - } - else if ( !dimensionsControlVisible && vBox.hasChild( dimensionsCheckbox ) ) { - vBox.removeChild( dimensionsCheckbox ); - } - } ); + // Add/remove the dimension visibility control. + this.dimensionsControlVisibleProperty.link( function( dimensionsControlVisible ) { + if ( dimensionsControlVisible && !vBox.hasChild( dimensionsCheckbox ) ) { + // Insert at bottom. + vBox.insertChild( vBox.getChildrenCount(), dimensionsCheckbox ); + } + else if ( !dimensionsControlVisible && vBox.hasChild( dimensionsCheckbox ) ) { + vBox.removeChild( dimensionsCheckbox ); + } + } ); - this.mutate( options ); - } + this.mutate( options ); +} - areaBuilder.register( 'AreaBuilderControlPanel', AreaBuilderControlPanel ); +areaBuilder.register( 'AreaBuilderControlPanel', AreaBuilderControlPanel ); - return inherit( Node, AreaBuilderControlPanel ); -} ); \ No newline at end of file +inherit( Node, AreaBuilderControlPanel ); +export default AreaBuilderControlPanel; \ No newline at end of file diff --git a/js/common/view/AreaBuilderIconFactory.js b/js/common/view/AreaBuilderIconFactory.js index 9837083..e48a6eb 100644 --- a/js/common/view/AreaBuilderIconFactory.js +++ b/js/common/view/AreaBuilderIconFactory.js @@ -4,157 +4,149 @@ * This file contains the code used to generate the screen icons that need to be produced in code in order to look half * decent. */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Color = require( 'SCENERY/util/Color' ); - const FaceNode = require( 'SCENERY_PHET/FaceNode' ); - const GridIcon = require( 'AREA_BUILDER/common/view/GridIcon' ); - const HBox = require( 'SCENERY/nodes/HBox' ); - const LinearGradient = require( 'SCENERY/util/LinearGradient' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); - const Screen = require( 'JOIST/Screen' ); - const Shape = require( 'KITE/Shape' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const NAV_BAR_ICON_SIZE = Screen.MINIMUM_NAVBAR_ICON_SIZE; - const GRID_STROKE = null; - const SHAPE_LINE_WIDTH = 1; - const GRID_OPTIONS = { - backgroundStroke: 'black', - backgroundLineWidth: 0.5, - gridStroke: GRID_STROKE, - shapeLineWidth: SHAPE_LINE_WIDTH - }; - - function createBucketIcon( options ) { + +import Vector2 from '../../../../dot/js/Vector2.js'; +import Screen from '../../../../joist/js/Screen.js'; +import Shape from '../../../../kite/js/Shape.js'; +import merge from '../../../../phet-core/js/merge.js'; +import FaceNode from '../../../../scenery-phet/js/FaceNode.js'; +import HBox from '../../../../scenery/js/nodes/HBox.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import LinearGradient from '../../../../scenery/js/util/LinearGradient.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import GridIcon from './GridIcon.js'; + +// constants +const NAV_BAR_ICON_SIZE = Screen.MINIMUM_NAVBAR_ICON_SIZE; +const GRID_STROKE = null; +const SHAPE_LINE_WIDTH = 1; +const GRID_OPTIONS = { + backgroundStroke: 'black', + backgroundLineWidth: 0.5, + gridStroke: GRID_STROKE, + shapeLineWidth: SHAPE_LINE_WIDTH +}; + +function createBucketIcon( options ) { + const icon = new Node(); + const ellipseXRadius = NAV_BAR_ICON_SIZE.width * 0.125; + const ellipseYRadius = ellipseXRadius * 0.3; + const bucketDepth = ellipseXRadius * 0.75; + const bodyShape = new Shape().moveTo( -ellipseXRadius, 0 ).lineTo( -ellipseXRadius * 0.75, bucketDepth ).quadraticCurveTo( 0, bucketDepth + ellipseYRadius, ellipseXRadius * 0.75, bucketDepth ).lineTo( ellipseXRadius, 0 ).close(); + icon.addChild( new Path( bodyShape, { fill: 'blue', stroke: 'blue', lineWidth: 0.5 } ) ); + icon.addChild( new Path( Shape.ellipse( 0, 0, ellipseXRadius, ellipseYRadius ), { + fill: new LinearGradient( -ellipseXRadius, 0, ellipseXRadius, 0 ).addColorStop( 0, '#333333' ).addColorStop( 1, '#999999' ) + } ) ); + + icon.mutate( options ); + return icon; +} + +/** + * Static object, not meant to be instantiated. + */ +const AreaBuilderIconFactory = { + createExploreScreenNavBarIcon: function() { + + // root node const icon = new Node(); - const ellipseXRadius = NAV_BAR_ICON_SIZE.width * 0.125; - const ellipseYRadius = ellipseXRadius * 0.3; - const bucketDepth = ellipseXRadius * 0.75; - const bodyShape = new Shape().moveTo( -ellipseXRadius, 0 ). - lineTo( -ellipseXRadius * 0.75, bucketDepth ). - quadraticCurveTo( 0, bucketDepth + ellipseYRadius, ellipseXRadius * 0.75, bucketDepth ). - lineTo( ellipseXRadius, 0 ). - close(); - icon.addChild( new Path( bodyShape, { fill: 'blue', stroke: 'blue', lineWidth: 0.5 } ) ); - icon.addChild( new Path( Shape.ellipse( 0, 0, ellipseXRadius, ellipseYRadius ), { - fill: new LinearGradient( -ellipseXRadius, 0, ellipseXRadius, 0 ).addColorStop( 0, '#333333' ).addColorStop( 1, '#999999' ) + + // background + icon.addChild( new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width, NAV_BAR_ICON_SIZE.height, 0, 0, { + fill: AreaBuilderSharedConstants.BACKGROUND_COLOR } ) ); - icon.mutate( options ); + // left shape placement board and shapes + const unitSquareLength = 15; + const leftBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.GREENISH_COLOR, [ + new Vector2( 1, 1 ), new Vector2( 2, 1 ), new Vector2( 1, 2 ) + ], merge( { left: NAV_BAR_ICON_SIZE.width * 0.05, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); + icon.addChild( leftBoard ); + + // right shape placement board and shapes + const rightBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.PURPLISH_COLOR, [ + new Vector2( 1, 1 ), new Vector2( 2, 1 ) + ], merge( { right: NAV_BAR_ICON_SIZE.width * 0.95, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); + icon.addChild( rightBoard ); + + // left bucket + icon.addChild( createBucketIcon( { centerX: leftBoard.centerX, top: leftBoard.bottom + 2 } ) ); + + // right bucket + icon.addChild( createBucketIcon( { centerX: rightBoard.centerX, top: rightBoard.bottom + 2 } ) ); + + return icon; + }, + + createGameScreenNavBarIcon: function() { + + // root node + const icon = new Node(); + + // background + icon.addChild( new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width, NAV_BAR_ICON_SIZE.height, 0, 0, { + fill: AreaBuilderSharedConstants.BACKGROUND_COLOR + } ) ); + + // shape placement board and shapes + const unitSquareLength = 12; + const shapePlacementBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.GREENISH_COLOR, [ + new Vector2( 1, 1 ), new Vector2( 2, 1 ), new Vector2( 2, 2 ) + ], merge( { left: NAV_BAR_ICON_SIZE.width * 0.075, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); + icon.addChild( shapePlacementBoard ); + + // smiley face + icon.addChild( new FaceNode( NAV_BAR_ICON_SIZE.width * 0.35, { + left: shapePlacementBoard.right + 4, + centerY: shapePlacementBoard.centerY + } ) ); + + // shape carousel + const shapeCarousel = new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width * 0.5, NAV_BAR_ICON_SIZE.height * 0.25, 1, 1, { + fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, + stroke: 'black', + lineWidth: 0.5, + top: shapePlacementBoard.bottom + 8, + centerX: NAV_BAR_ICON_SIZE.width / 2 + } ); + icon.addChild( shapeCarousel ); + + // shapes on the shape carousel + const shapeFill = AreaBuilderSharedConstants.GREENISH_COLOR; + const shapeStroke = new Color( shapeFill ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); + const shapes = new HBox( { + children: [ + new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { + fill: shapeFill, + stroke: shapeStroke + } ), + new HBox( { + children: [ + new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { + fill: shapeFill, + stroke: shapeStroke + } ), + new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { + fill: shapeFill, + stroke: shapeStroke + } ) + ], spacing: 0 + } ) ], + spacing: 2, + centerX: shapeCarousel.width / 2, + centerY: shapeCarousel.height / 2 + } ); + + shapeCarousel.addChild( shapes ); + return icon; } +}; + +areaBuilder.register( 'AreaBuilderIconFactory', AreaBuilderIconFactory ); - /** - * Static object, not meant to be instantiated. - */ - const AreaBuilderIconFactory = { - createExploreScreenNavBarIcon: function() { - - // root node - const icon = new Node(); - - // background - icon.addChild( new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width, NAV_BAR_ICON_SIZE.height, 0, 0, { - fill: AreaBuilderSharedConstants.BACKGROUND_COLOR - } ) ); - - // left shape placement board and shapes - const unitSquareLength = 15; - const leftBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.GREENISH_COLOR, [ - new Vector2( 1, 1 ), new Vector2( 2, 1 ), new Vector2( 1, 2 ) - ], merge( { left: NAV_BAR_ICON_SIZE.width * 0.05, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); - icon.addChild( leftBoard ); - - // right shape placement board and shapes - const rightBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.PURPLISH_COLOR, [ - new Vector2( 1, 1 ), new Vector2( 2, 1 ) - ], merge( { right: NAV_BAR_ICON_SIZE.width * 0.95, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); - icon.addChild( rightBoard ); - - // left bucket - icon.addChild( createBucketIcon( { centerX: leftBoard.centerX, top: leftBoard.bottom + 2 } ) ); - - // right bucket - icon.addChild( createBucketIcon( { centerX: rightBoard.centerX, top: rightBoard.bottom + 2 } ) ); - - return icon; - }, - - createGameScreenNavBarIcon: function() { - - // root node - const icon = new Node(); - - // background - icon.addChild( new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width, NAV_BAR_ICON_SIZE.height, 0, 0, { - fill: AreaBuilderSharedConstants.BACKGROUND_COLOR - } ) ); - - // shape placement board and shapes - const unitSquareLength = 12; - const shapePlacementBoard = new GridIcon( 4, 4, unitSquareLength, AreaBuilderSharedConstants.GREENISH_COLOR, [ - new Vector2( 1, 1 ), new Vector2( 2, 1 ), new Vector2( 2, 2 ) - ], merge( { left: NAV_BAR_ICON_SIZE.width * 0.075, top: NAV_BAR_ICON_SIZE.height * 0.1 }, GRID_OPTIONS ) ); - icon.addChild( shapePlacementBoard ); - - // smiley face - icon.addChild( new FaceNode( NAV_BAR_ICON_SIZE.width * 0.35, { - left: shapePlacementBoard.right + 4, - centerY: shapePlacementBoard.centerY - } ) ); - - // shape carousel - const shapeCarousel = new Rectangle( 0, 0, NAV_BAR_ICON_SIZE.width * 0.5, NAV_BAR_ICON_SIZE.height * 0.25, 1, 1, { - fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, - stroke: 'black', - lineWidth: 0.5, - top: shapePlacementBoard.bottom + 8, - centerX: NAV_BAR_ICON_SIZE.width / 2 - } ); - icon.addChild( shapeCarousel ); - - // shapes on the shape carousel - const shapeFill = AreaBuilderSharedConstants.GREENISH_COLOR; - const shapeStroke = new Color( shapeFill ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); - const shapes = new HBox( { - children: [ - new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { - fill: shapeFill, - stroke: shapeStroke - } ), - new HBox( { - children: [ - new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { - fill: shapeFill, - stroke: shapeStroke - } ), - new Rectangle( 0, 0, unitSquareLength, unitSquareLength, 0, 0, { - fill: shapeFill, - stroke: shapeStroke - } ) - ], spacing: 0 - } ) ], - spacing: 2, - centerX: shapeCarousel.width / 2, - centerY: shapeCarousel.height / 2 - } ); - - shapeCarousel.addChild( shapes ); - - return icon; - } - }; - - areaBuilder.register( 'AreaBuilderIconFactory', AreaBuilderIconFactory ); - - return AreaBuilderIconFactory; -} ); \ No newline at end of file +export default AreaBuilderIconFactory; \ No newline at end of file diff --git a/js/common/view/DimensionsIcon.js b/js/common/view/DimensionsIcon.js index 0dbd036..dce2699 100644 --- a/js/common/view/DimensionsIcon.js +++ b/js/common/view/DimensionsIcon.js @@ -6,70 +6,66 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Color = require( 'SCENERY/util/Color' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); - const Text = require( 'SCENERY/nodes/Text' ); +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import Grid from './Grid.js'; - // constants - const UNIT_LENGTH = 10; // in screen coordinates - const WIDTH = 3 * UNIT_LENGTH; - const HEIGHT = 2 * UNIT_LENGTH; // in screen coordinates - const LABEL_FONT = new PhetFont( 10 ); - const DEFAULT_FILL_COLOR = AreaBuilderSharedConstants.GREENISH_COLOR; +// constants +const UNIT_LENGTH = 10; // in screen coordinates +const WIDTH = 3 * UNIT_LENGTH; +const HEIGHT = 2 * UNIT_LENGTH; // in screen coordinates +const LABEL_FONT = new PhetFont( 10 ); +const DEFAULT_FILL_COLOR = AreaBuilderSharedConstants.GREENISH_COLOR; - /** - * @param {Object} [options] - * @constructor - */ - function DimensionsIcon( options ) { - Node.call( this ); +/** + * @param {Object} [options] + * @constructor + */ +function DimensionsIcon( options ) { + Node.call( this ); - // Create the background rectangle node. - this.singleRectNode = new Rectangle( 0, 0, WIDTH, HEIGHT, 0, 0 ); - this.addChild( this.singleRectNode ); + // Create the background rectangle node. + this.singleRectNode = new Rectangle( 0, 0, WIDTH, HEIGHT, 0, 0 ); + this.addChild( this.singleRectNode ); - // Add the grid. - this.grid = new Grid( new Bounds2( 0, 0, WIDTH, HEIGHT ), UNIT_LENGTH, { stroke: '#b0b0b0', lineDash: [ 1, 2 ] } ); - this.addChild( this.grid ); + // Add the grid. + this.grid = new Grid( new Bounds2( 0, 0, WIDTH, HEIGHT ), UNIT_LENGTH, { stroke: '#b0b0b0', lineDash: [ 1, 2 ] } ); + this.addChild( this.grid ); - // Initialize the color. - this.setColor( DEFAULT_FILL_COLOR ); + // Initialize the color. + this.setColor( DEFAULT_FILL_COLOR ); - // Label the sides. - this.addChild( new Text( '2', { font: LABEL_FONT, right: -2, centerY: HEIGHT / 2 } ) ); - this.addChild( new Text( '2', { font: LABEL_FONT, left: WIDTH + 2, centerY: HEIGHT / 2 } ) ); - this.addChild( new Text( '3', { font: LABEL_FONT, centerX: WIDTH / 2, bottom: 0 } ) ); - this.addChild( new Text( '3', { font: LABEL_FONT, centerX: WIDTH / 2, top: HEIGHT } ) ); + // Label the sides. + this.addChild( new Text( '2', { font: LABEL_FONT, right: -2, centerY: HEIGHT / 2 } ) ); + this.addChild( new Text( '2', { font: LABEL_FONT, left: WIDTH + 2, centerY: HEIGHT / 2 } ) ); + this.addChild( new Text( '3', { font: LABEL_FONT, centerX: WIDTH / 2, bottom: 0 } ) ); + this.addChild( new Text( '3', { font: LABEL_FONT, centerX: WIDTH / 2, top: HEIGHT } ) ); - // Pass through any options. - this.mutate( options ); - } + // Pass through any options. + this.mutate( options ); +} - areaBuilder.register( 'DimensionsIcon', DimensionsIcon ); +areaBuilder.register( 'DimensionsIcon', DimensionsIcon ); - return inherit( Node, DimensionsIcon, { +export default inherit( Node, DimensionsIcon, { - setGridVisible: function( gridVisible ) { - assert && assert( typeof( gridVisible ) === 'boolean' ); - this.grid.visible = gridVisible; - }, + setGridVisible: function( gridVisible ) { + assert && assert( typeof ( gridVisible ) === 'boolean' ); + this.grid.visible = gridVisible; + }, - setColor: function( color ) { - this.singleRectNode.fill = color; - const strokeColor = Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); - this.singleRectNode.stroke = strokeColor; - this.grid.stroke = strokeColor; - } - } ); + setColor: function( color ) { + this.singleRectNode.fill = color; + const strokeColor = Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); + this.singleRectNode.stroke = strokeColor; + this.grid.stroke = strokeColor; + } } ); \ No newline at end of file diff --git a/js/common/view/Grid.js b/js/common/view/Grid.js index 9971672..5065949 100644 --- a/js/common/view/Grid.js +++ b/js/common/view/Grid.js @@ -6,40 +6,37 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Path = require( 'SCENERY/nodes/Path' ); - const Shape = require( 'KITE/Shape' ); - - /** - * @param {Bounds2} bounds - * @param {number} spacing - * @param {Object} [options] - * @constructor - */ - function Grid( bounds, spacing, options ) { - const gridShape = new Shape(); - - // Add the vertical lines - for ( var i = bounds.minX + spacing; i < bounds.minX + bounds.width; i += spacing ) { - gridShape.moveTo( i, bounds.minY ); - gridShape.lineTo( i, bounds.minY + bounds.height ); - } - - // Add the horizontal lines - for ( i = bounds.minY + spacing; i < bounds.minY + bounds.height; i += spacing ) { - gridShape.moveTo( bounds.minX, i ); - gridShape.lineTo( bounds.minX + bounds.width, i ); - } - - Path.call( this, gridShape, options ); + +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import areaBuilder from '../../areaBuilder.js'; + +/** + * @param {Bounds2} bounds + * @param {number} spacing + * @param {Object} [options] + * @constructor + */ +function Grid( bounds, spacing, options ) { + const gridShape = new Shape(); + + // Add the vertical lines + for ( var i = bounds.minX + spacing; i < bounds.minX + bounds.width; i += spacing ) { + gridShape.moveTo( i, bounds.minY ); + gridShape.lineTo( i, bounds.minY + bounds.height ); + } + + // Add the horizontal lines + for ( i = bounds.minY + spacing; i < bounds.minY + bounds.height; i += spacing ) { + gridShape.moveTo( bounds.minX, i ); + gridShape.lineTo( bounds.minX + bounds.width, i ); } - areaBuilder.register( 'Grid', Grid ); + Path.call( this, gridShape, options ); +} + +areaBuilder.register( 'Grid', Grid ); - return inherit( Path, Grid ); -} ); \ No newline at end of file +inherit( Path, Grid ); +export default Grid; \ No newline at end of file diff --git a/js/common/view/GridIcon.js b/js/common/view/GridIcon.js index ae44f37..5f238a5 100644 --- a/js/common/view/GridIcon.js +++ b/js/common/view/GridIcon.js @@ -6,71 +6,68 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Color = require( 'SCENERY/util/Color' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import Grid from './Grid.js'; - /** - * @param {number} columns - * @param {number} rows - * @param {number} cellLength - * @param {string} shapeFillColor - * @param {Array} occupiedCells - * @param {Object} [options] - * @constructor - */ - function GridIcon( columns, rows, cellLength, shapeFillColor, occupiedCells, options ) { +/** + * @param {number} columns + * @param {number} rows + * @param {number} cellLength + * @param {string} shapeFillColor + * @param {Array} occupiedCells + * @param {Object} [options] + * @constructor + */ +function GridIcon( columns, rows, cellLength, shapeFillColor, occupiedCells, options ) { - Node.call( this ); - const self = this; + Node.call( this ); + const self = this; - options = merge( { - // defaults - gridStroke: 'black', - gridLineWidth: 1, - backgroundStroke: null, - backgroundFill: 'white', - backgroundLineWidth: 1, - shapeStroke: new Color( shapeFillColor ).colorUtilsDarker( 0.2 ), // darkening factor empirically determined - shapeLineWidth: 1 - }, options ); + options = merge( { + // defaults + gridStroke: 'black', + gridLineWidth: 1, + backgroundStroke: null, + backgroundFill: 'white', + backgroundLineWidth: 1, + shapeStroke: new Color( shapeFillColor ).colorUtilsDarker( 0.2 ), // darkening factor empirically determined + shapeLineWidth: 1 + }, options ); - this.addChild( new Rectangle( 0, 0, columns * cellLength, rows * cellLength, 0, 0, { - fill: options.backgroundFill, - stroke: options.backgroundStroke, - lineWidth: options.backgroundLineWidth - } ) ); + this.addChild( new Rectangle( 0, 0, columns * cellLength, rows * cellLength, 0, 0, { + fill: options.backgroundFill, + stroke: options.backgroundStroke, + lineWidth: options.backgroundLineWidth + } ) ); - this.addChild( new Grid( new Bounds2( 0, 0, columns * cellLength, rows * cellLength ), cellLength, { - stroke: options.gridStroke, - lineWidth: options.gridLineWidth, - fill: options.gridFill - } ) ); + this.addChild( new Grid( new Bounds2( 0, 0, columns * cellLength, rows * cellLength ), cellLength, { + stroke: options.gridStroke, + lineWidth: options.gridLineWidth, + fill: options.gridFill + } ) ); - occupiedCells.forEach( function( occupiedCell ) { - self.addChild( new Rectangle( 0, 0, cellLength, cellLength, 0, 0, { - fill: shapeFillColor, - stroke: options.shapeStroke, - lineWidth: options.shapeLineWidth, - left: occupiedCell.x * cellLength, - top: occupiedCell.y * cellLength - } ) ); - } ); + occupiedCells.forEach( function( occupiedCell ) { + self.addChild( new Rectangle( 0, 0, cellLength, cellLength, 0, 0, { + fill: shapeFillColor, + stroke: options.shapeStroke, + lineWidth: options.shapeLineWidth, + left: occupiedCell.x * cellLength, + top: occupiedCell.y * cellLength + } ) ); + } ); - // Pass options through to the parent class. - this.mutate( options ); - } + // Pass options through to the parent class. + this.mutate( options ); +} - areaBuilder.register( 'GridIcon', GridIcon ); +areaBuilder.register( 'GridIcon', GridIcon ); - return inherit( Node, GridIcon ); -} ); \ No newline at end of file +inherit( Node, GridIcon ); +export default GridIcon; \ No newline at end of file diff --git a/js/common/view/PerimeterShapeNode.js b/js/common/view/PerimeterShapeNode.js index a800b84..c1f6551 100644 --- a/js/common/view/PerimeterShapeNode.js +++ b/js/common/view/PerimeterShapeNode.js @@ -7,247 +7,248 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Property = require( 'AXON/Property' ); - const Shape = require( 'KITE/Shape' ); - const Text = require( 'SCENERY/nodes/Text' ); - const Utils = require( 'DOT/Utils' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const DIMENSION_LABEL_FONT = new PhetFont( { size: 14 } ); - const COMPARISON_TOLERANCE = 1E-6; - - // Utility function for identifying a perimeter segment with no bends. - function identifySegment( perimeterPoints, startIndex ) { - - // Parameter checking. - if ( startIndex >= perimeterPoints.length ) { - throw new Error( 'Illegal use of function for identifying perimeter segments.' ); - } - // Set up initial portion of segment. - const segmentStartPoint = perimeterPoints[ startIndex ]; - let endIndex = ( startIndex + 1 ) % perimeterPoints.length; - let segmentEndPoint = perimeterPoints[ endIndex ]; - const previousAngle = Math.atan2( segmentEndPoint.y - segmentStartPoint.y, segmentEndPoint.x - segmentStartPoint.x ); - let segmentComplete = false; - - while ( !segmentComplete && endIndex !== 0 ) { - const candidatePoint = perimeterPoints[ ( endIndex + 1 ) % perimeterPoints.length ]; - const angleToCandidatePoint = Math.atan2( candidatePoint.y - segmentEndPoint.y, candidatePoint.x - segmentEndPoint.x ); - if ( previousAngle === angleToCandidatePoint ) { - // This point is an extension of the current segment. - segmentEndPoint = candidatePoint; - endIndex = ( endIndex + 1 ) % perimeterPoints.length; - } - else { - // This point isn't part of this segment. - segmentComplete = true; - } +import Property from '../../../../axon/js/Property.js'; +import Utils from '../../../../dot/js/Utils.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import areaBuilder from '../../areaBuilder.js'; +import Grid from './Grid.js'; + +// constants +const DIMENSION_LABEL_FONT = new PhetFont( { size: 14 } ); +const COMPARISON_TOLERANCE = 1E-6; + +// Utility function for identifying a perimeter segment with no bends. +function identifySegment( perimeterPoints, startIndex ) { + + // Parameter checking. + if ( startIndex >= perimeterPoints.length ) { + throw new Error( 'Illegal use of function for identifying perimeter segments.' ); + } + + // Set up initial portion of segment. + const segmentStartPoint = perimeterPoints[ startIndex ]; + let endIndex = ( startIndex + 1 ) % perimeterPoints.length; + let segmentEndPoint = perimeterPoints[ endIndex ]; + const previousAngle = Math.atan2( segmentEndPoint.y - segmentStartPoint.y, segmentEndPoint.x - segmentStartPoint.x ); + let segmentComplete = false; + + while ( !segmentComplete && endIndex !== 0 ) { + const candidatePoint = perimeterPoints[ ( endIndex + 1 ) % perimeterPoints.length ]; + const angleToCandidatePoint = Math.atan2( candidatePoint.y - segmentEndPoint.y, candidatePoint.x - segmentEndPoint.x ); + if ( previousAngle === angleToCandidatePoint ) { + // This point is an extension of the current segment. + segmentEndPoint = candidatePoint; + endIndex = ( endIndex + 1 ) % perimeterPoints.length; } + else { + // This point isn't part of this segment. + segmentComplete = true; + } + } - return { - startIndex: startIndex, - endIndex: endIndex - }; + return { + startIndex: startIndex, + endIndex: endIndex + }; +} + +/** + * @param {Property} perimeterShapeProperty + * @param {Bounds2} maxBounds + * @param {number} unitSquareLength + * @param {boolean} showDimensionsProperty + * @param {boolean} showGridProperty + * @param {Object} [options] + * @constructor + */ +function PerimeterShapeNode( perimeterShapeProperty, maxBounds, unitSquareLength, showDimensionsProperty, showGridProperty, options ) { + + Node.call( this ); + + const perimeterDefinesViableShapeProperty = new Property( false ); + + // Set up the shape, edge, and grid, which will be updated as the perimeter changes. The order in which these + // are added is important for proper layering. + const perimeterShapeNode = new Path( null ); + this.addChild( perimeterShapeNode ); + const grid = new Grid( maxBounds, unitSquareLength, { + lineDash: [ 0, 3, 1, 0 ], // Tweaked to work well with unit size + stroke: 'black' + } ); + this.addChild( grid ); + const perimeterNode = new Path( null, { lineWidth: 2 } ); + this.addChild( perimeterNode ); + const dimensionsLayer = new Node(); + this.addChild( dimensionsLayer ); + + // Create a pool of text nodes that will be used to portray the dimension values. This is done as a performance + // optimization, since changing text nodes is more efficient that recreating them on each update. + const textNodePool = []; + + function addDimensionLabelNode() { + const textNode = new Text( '', { + font: DIMENSION_LABEL_FONT, + centerX: maxBounds.centerX, + centerY: maxBounds.centerY + } ); + textNode.visible = false; + textNodePool.push( textNode ); + dimensionsLayer.addChild( textNode ); } - /** - * @param {Property} perimeterShapeProperty - * @param {Bounds2} maxBounds - * @param {number} unitSquareLength - * @param {boolean} showDimensionsProperty - * @param {boolean} showGridProperty - * @param {Object} [options] - * @constructor - */ - function PerimeterShapeNode( perimeterShapeProperty, maxBounds, unitSquareLength, showDimensionsProperty, showGridProperty, options ) { - - Node.call( this ); - - const perimeterDefinesViableShapeProperty = new Property( false ); - - // Set up the shape, edge, and grid, which will be updated as the perimeter changes. The order in which these - // are added is important for proper layering. - const perimeterShapeNode = new Path( null ); - this.addChild( perimeterShapeNode ); - const grid = new Grid( maxBounds, unitSquareLength, { - lineDash: [ 0, 3, 1, 0 ], // Tweaked to work well with unit size - stroke: 'black' + _.times( 16, addDimensionLabelNode ); // Initial size empirically chosen, can be adjusted if needed. + + // Define function for updating the appearance of the perimeter shape. + function update() { + let i; + + // Update the colors + assert && assert( perimeterShapeProperty.value.fillColor || perimeterShapeProperty.value.edgeColor, + 'PerimeterShape can\'t have null values for both the fill and the edge.' ); + perimeterShapeNode.fill = perimeterShapeProperty.value.fillColor; + perimeterNode.stroke = perimeterShapeProperty.value.edgeColor; + + // Define the shape of the outer perimeter. + const mainShape = new Shape(); + perimeterShapeProperty.value.exteriorPerimeters.forEach( function( exteriorPerimeters ) { + mainShape.moveToPoint( exteriorPerimeters[ 0 ] ); + for ( i = 1; i < exteriorPerimeters.length; i++ ) { + mainShape.lineToPoint( exteriorPerimeters[ i ] ); + } + mainShape.lineToPoint( exteriorPerimeters[ 0 ] ); + mainShape.close(); } ); - this.addChild( grid ); - const perimeterNode = new Path( null, { lineWidth: 2 } ); - this.addChild( perimeterNode ); - const dimensionsLayer = new Node(); - this.addChild( dimensionsLayer ); - - // Create a pool of text nodes that will be used to portray the dimension values. This is done as a performance - // optimization, since changing text nodes is more efficient that recreating them on each update. - const textNodePool = []; - - function addDimensionLabelNode() { - const textNode = new Text( '', { font: DIMENSION_LABEL_FONT, centerX: maxBounds.centerX, centerY: maxBounds.centerY } ); - textNode.visible = false; - textNodePool.push( textNode ); - dimensionsLayer.addChild( textNode ); - } - _.times( 16, addDimensionLabelNode ); // Initial size empirically chosen, can be adjusted if needed. + // Hide all dimension labels in the pool, they will be shown later if used. + textNodePool.forEach( function( textNode ) { textNode.visible = false; } ); - // Define function for updating the appearance of the perimeter shape. - function update() { - let i; + // The resulting shape will be empty if there are no points in the external perimeter, so we need to check for that. + if ( !mainShape.bounds.isEmpty() ) { - // Update the colors - assert && assert( perimeterShapeProperty.value.fillColor || perimeterShapeProperty.value.edgeColor, - 'PerimeterShape can\'t have null values for both the fill and the edge.' ); - perimeterShapeNode.fill = perimeterShapeProperty.value.fillColor; - perimeterNode.stroke = perimeterShapeProperty.value.edgeColor; + // Make sure the shape fits within its specified bounds. + assert && assert( maxBounds.containsBounds( mainShape.bounds ) ); - // Define the shape of the outer perimeter. - const mainShape = new Shape(); - perimeterShapeProperty.value.exteriorPerimeters.forEach( function( exteriorPerimeters ) { - mainShape.moveToPoint( exteriorPerimeters[ 0 ] ); - for ( i = 1; i < exteriorPerimeters.length; i++ ) { - mainShape.lineToPoint( exteriorPerimeters[ i ] ); + // Turn on visibility of the perimeter and the interior fill. + perimeterShapeNode.visible = true; + perimeterNode.visible = true; + + // Handling any interior perimeters, a.k.a. holes, in the shape. + perimeterShapeProperty.value.interiorPerimeters.forEach( function( interiorPerimeter ) { + mainShape.moveToPoint( interiorPerimeter[ 0 ] ); + for ( i = 1; i < interiorPerimeter.length; i++ ) { + mainShape.lineToPoint( interiorPerimeter[ i ] ); } - mainShape.lineToPoint( exteriorPerimeters[ 0 ] ); + mainShape.lineToPoint( interiorPerimeter[ 0 ] ); mainShape.close(); } ); - // Hide all dimension labels in the pool, they will be shown later if used. - textNodePool.forEach( function( textNode ) { textNode.visible = false; } ); - - // The resulting shape will be empty if there are no points in the external perimeter, so we need to check for that. - if ( !mainShape.bounds.isEmpty() ) { + perimeterShapeNode.setShape( mainShape ); + perimeterNode.setShape( mainShape ); - // Make sure the shape fits within its specified bounds. - assert && assert( maxBounds.containsBounds( mainShape.bounds ) ); + grid.clipArea = mainShape; - // Turn on visibility of the perimeter and the interior fill. - perimeterShapeNode.visible = true; - perimeterNode.visible = true; + // Add the dimension labels for the perimeters, but only if there is only 1 exterior perimeter (multiple + // interior perimeters if fine). + if ( perimeterShapeProperty.value.exteriorPerimeters.length === 1 ) { - // Handling any interior perimeters, a.k.a. holes, in the shape. + // Create a list of the perimeters to be labeled. + const perimetersToLabel = []; + perimetersToLabel.push( perimeterShapeProperty.value.exteriorPerimeters[ 0 ] ); perimeterShapeProperty.value.interiorPerimeters.forEach( function( interiorPerimeter ) { - mainShape.moveToPoint( interiorPerimeter[ 0 ] ); - for ( i = 1; i < interiorPerimeter.length; i++ ) { - mainShape.lineToPoint( interiorPerimeter[ i ] ); - } - mainShape.lineToPoint( interiorPerimeter[ 0 ] ); - mainShape.close(); + perimetersToLabel.push( interiorPerimeter ); } ); - perimeterShapeNode.setShape( mainShape ); - perimeterNode.setShape( mainShape ); - - grid.clipArea = mainShape; - - // Add the dimension labels for the perimeters, but only if there is only 1 exterior perimeter (multiple - // interior perimeters if fine). - if ( perimeterShapeProperty.value.exteriorPerimeters.length === 1 ) { - - // Create a list of the perimeters to be labeled. - const perimetersToLabel = []; - perimetersToLabel.push( perimeterShapeProperty.value.exteriorPerimeters[ 0 ] ); - perimeterShapeProperty.value.interiorPerimeters.forEach( function( interiorPerimeter ) { - perimetersToLabel.push( interiorPerimeter ); - } ); - - // Identify the segments in each of the perimeters, exterior and interior, to be labeled. - const segmentLabelsInfo = []; - perimetersToLabel.forEach( function( perimeterToLabel ) { - let segment = { startIndex: 0, endIndex: 0 }; - do { - segment = identifySegment( perimeterToLabel, segment.endIndex ); - // Only put labels on segments that have integer lengths. - const segmentLabelInfo = { - unitLength: perimeterToLabel[ segment.startIndex ].distance( perimeterToLabel[ segment.endIndex ] ) / unitSquareLength, - position: new Vector2( ( perimeterToLabel[ segment.startIndex ].x + perimeterToLabel[ segment.endIndex ].x ) / 2, - ( perimeterToLabel[ segment.startIndex ].y + perimeterToLabel[ segment.endIndex ].y ) / 2 ), - edgeAngle: Math.atan2( perimeterToLabel[ segment.endIndex ].y - perimeterToLabel[ segment.startIndex ].y, - perimeterToLabel[ segment.endIndex ].x - perimeterToLabel[ segment.startIndex ].x - ) - }; - - // Only include the labels that are integer values. - if ( Math.abs( Utils.roundSymmetric( segmentLabelInfo.unitLength ) - segmentLabelInfo.unitLength ) < COMPARISON_TOLERANCE ) { - segmentLabelInfo.unitLength = Utils.roundSymmetric( segmentLabelInfo.unitLength ); - segmentLabelsInfo.push( segmentLabelInfo ); - } - } while ( segment.endIndex !== 0 ); - } ); - - // Make sure that there are enough labels in the pool. - if ( segmentLabelsInfo.length > textNodePool.length ) { - _.times( segmentLabelsInfo.length - textNodePool.length, addDimensionLabelNode ); - } - - // Get labels from the pool and place them on each segment, just outside of the shape. - segmentLabelsInfo.forEach( function( segmentLabelInfo, segmentIndex ) { - const dimensionLabel = textNodePool[ segmentIndex ]; - dimensionLabel.visible = true; - dimensionLabel.text = segmentLabelInfo.unitLength; - const labelPositionOffset = new Vector2( 0, 0 ); - // TODO: At the time of this writing there is an issue with Shape.containsPoint() that can make - // containment testing unreliable if there is an edge on the same line as the containment test. As a - // workaround, the containment test offset is tweaked a little below. Once this issue is fixed, the - // label offset itself can be used for the test. See https://github.com/phetsims/kite/issues/3. - let containmentTestOffset; - if ( segmentLabelInfo.edgeAngle === 0 || segmentLabelInfo.edgeAngle === Math.PI ) { - // Label is on horizontal edge, so use height to determine offset. - labelPositionOffset.setXY( 0, dimensionLabel.height / 2 ); - containmentTestOffset = labelPositionOffset.plusXY( 1, 0 ); - } - else { // NOTE: Angled edges are not currently supported. - // Label is on a vertical edge - labelPositionOffset.setXY( dimensionLabel.width * 0.8, 0 ); - containmentTestOffset = labelPositionOffset.plusXY( 0, 1 ); - } - if ( mainShape.containsPoint( segmentLabelInfo.position.plus( containmentTestOffset ) ) ) { - // Flip the offset vector to keep the label outside of the shape. - labelPositionOffset.rotate( Math.PI ); + // Identify the segments in each of the perimeters, exterior and interior, to be labeled. + const segmentLabelsInfo = []; + perimetersToLabel.forEach( function( perimeterToLabel ) { + let segment = { startIndex: 0, endIndex: 0 }; + do { + segment = identifySegment( perimeterToLabel, segment.endIndex ); + // Only put labels on segments that have integer lengths. + const segmentLabelInfo = { + unitLength: perimeterToLabel[ segment.startIndex ].distance( perimeterToLabel[ segment.endIndex ] ) / unitSquareLength, + position: new Vector2( ( perimeterToLabel[ segment.startIndex ].x + perimeterToLabel[ segment.endIndex ].x ) / 2, + ( perimeterToLabel[ segment.startIndex ].y + perimeterToLabel[ segment.endIndex ].y ) / 2 ), + edgeAngle: Math.atan2( perimeterToLabel[ segment.endIndex ].y - perimeterToLabel[ segment.startIndex ].y, + perimeterToLabel[ segment.endIndex ].x - perimeterToLabel[ segment.startIndex ].x + ) + }; + + // Only include the labels that are integer values. + if ( Math.abs( Utils.roundSymmetric( segmentLabelInfo.unitLength ) - segmentLabelInfo.unitLength ) < COMPARISON_TOLERANCE ) { + segmentLabelInfo.unitLength = Utils.roundSymmetric( segmentLabelInfo.unitLength ); + segmentLabelsInfo.push( segmentLabelInfo ); } - dimensionLabel.center = segmentLabelInfo.position.plus( labelPositionOffset ); - } ); + } while ( segment.endIndex !== 0 ); + } ); + + // Make sure that there are enough labels in the pool. + if ( segmentLabelsInfo.length > textNodePool.length ) { + _.times( segmentLabelsInfo.length - textNodePool.length, addDimensionLabelNode ); } - perimeterDefinesViableShapeProperty.value = true; - } - else { - perimeterShapeNode.visible = false; - perimeterNode.visible = false; - perimeterDefinesViableShapeProperty.value = false; + + // Get labels from the pool and place them on each segment, just outside of the shape. + segmentLabelsInfo.forEach( function( segmentLabelInfo, segmentIndex ) { + const dimensionLabel = textNodePool[ segmentIndex ]; + dimensionLabel.visible = true; + dimensionLabel.text = segmentLabelInfo.unitLength; + const labelPositionOffset = new Vector2( 0, 0 ); + // TODO: At the time of this writing there is an issue with Shape.containsPoint() that can make + // containment testing unreliable if there is an edge on the same line as the containment test. As a + // workaround, the containment test offset is tweaked a little below. Once this issue is fixed, the + // label offset itself can be used for the test. See https://github.com/phetsims/kite/issues/3. + let containmentTestOffset; + if ( segmentLabelInfo.edgeAngle === 0 || segmentLabelInfo.edgeAngle === Math.PI ) { + // Label is on horizontal edge, so use height to determine offset. + labelPositionOffset.setXY( 0, dimensionLabel.height / 2 ); + containmentTestOffset = labelPositionOffset.plusXY( 1, 0 ); + } + else { // NOTE: Angled edges are not currently supported. + // Label is on a vertical edge + labelPositionOffset.setXY( dimensionLabel.width * 0.8, 0 ); + containmentTestOffset = labelPositionOffset.plusXY( 0, 1 ); + } + if ( mainShape.containsPoint( segmentLabelInfo.position.plus( containmentTestOffset ) ) ) { + // Flip the offset vector to keep the label outside of the shape. + labelPositionOffset.rotate( Math.PI ); + } + dimensionLabel.center = segmentLabelInfo.position.plus( labelPositionOffset ); + } ); } + perimeterDefinesViableShapeProperty.value = true; + } + else { + perimeterShapeNode.visible = false; + perimeterNode.visible = false; + perimeterDefinesViableShapeProperty.value = false; } + } - // Control visibility of the dimension indicators. - showDimensionsProperty.linkAttribute( dimensionsLayer, 'visible' ); + // Control visibility of the dimension indicators. + showDimensionsProperty.linkAttribute( dimensionsLayer, 'visible' ); - // Control visibility of the grid. - Property.multilink( [ showGridProperty, perimeterDefinesViableShapeProperty ], function( showGrid, perimeterDefinesViableShape ) { - grid.visible = showGrid && perimeterDefinesViableShape; - } ); + // Control visibility of the grid. + Property.multilink( [ showGridProperty, perimeterDefinesViableShapeProperty ], function( showGrid, perimeterDefinesViableShape ) { + grid.visible = showGrid && perimeterDefinesViableShape; + } ); - // Update the shape, grid, and dimensions if the perimeter shape itself changes. - perimeterShapeProperty.link( function() { - update(); - } ); + // Update the shape, grid, and dimensions if the perimeter shape itself changes. + perimeterShapeProperty.link( function() { + update(); + } ); - // Pass options through to parent class. - this.mutate( options ); - } + // Pass options through to parent class. + this.mutate( options ); +} - areaBuilder.register( 'PerimeterShapeNode', PerimeterShapeNode ); +areaBuilder.register( 'PerimeterShapeNode', PerimeterShapeNode ); - return inherit( Node, PerimeterShapeNode ); -} ); \ No newline at end of file +inherit( Node, PerimeterShapeNode ); +export default PerimeterShapeNode; \ No newline at end of file diff --git a/js/common/view/ShapeCreatorNode.js b/js/common/view/ShapeCreatorNode.js index fc8812a..7f51d98 100644 --- a/js/common/view/ShapeCreatorNode.js +++ b/js/common/view/ShapeCreatorNode.js @@ -5,178 +5,175 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Color = require( 'SCENERY/util/Color' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const MovableDragHandler = require( 'SCENERY_PHET/input/MovableDragHandler' ); - const MovableShape = require( 'AREA_BUILDER/common/model/MovableShape' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const Property = require( 'AXON/Property' ); - const ScreenView = require( 'JOIST/ScreenView' ); - const Vector2 = require( 'DOT/Vector2' ); - const Vector2Property = require( 'DOT/Vector2Property' ); - - // constants - const BORDER_LINE_WIDTH = 1; - - /** - * @param {Shape} shape - * @param {string || Color} color - * @param {function} addShapeToModel - A function for adding the created shape to the model - * @param {Object} [options] - * @constructor - */ - function ShapeCreatorNode( shape, color, addShapeToModel, options ) { - assert && assert( shape.bounds.minX === 0 && shape.bounds.minY === 0, 'Error: Shape is expected to be located at 0, 0' ); - Node.call( this, { cursor: 'pointer' } ); - const self = this; - - options = merge( { - - // Spacing of the grid, if any, that should be shown on the creator node. Null indicates no grid. - gridSpacing: null, - - // Max number of shapes that can be created by this node. - creationLimit: Number.POSITIVE_INFINITY, - - // Drag bounds for the created shapes. - shapeDragBounds: Bounds2.EVERYTHING, - - // This is a node that is or will be somewhere up the scene graph tree from this ShapeCreatorNode, doesn't move, - // and whose parent has the coordinate frame needed to do the appropriate transformations when the a drag takes - // place on this ShapeCreatorNode. This is needed in cases where the ShapeCreatorNode can be moved while a drag - // of a created node is still in progress. This can occur when the ShapeCreatorNode is placed on a carousel and - // the sim is being used in a multi-touch environment. See https://github.com/phetsims/area-builder/issues/95 for - // more information. - nonMovingAncestor: null - }, options ); - - // parameter check - if ( options.creationLimit < Number.POSITIVE_INFINITY && - ( shape.bounds.width !== AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH || - shape.bounds.height !== AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH ) ) { - // The ability to set a creation limit ONLY works for unit squares. The reason for this is that non-unit shapes - // are generally decomposed into unit squares when added to the placement board, so it's hard to track when they - // get returned to their origin. It would be possible to do this, but the requirements of the sim at the time of - // this writing make it unnecessary. So, if you're hitting this exception, the code may need to be revamped to - // support creation limits for shapes that are not unit squares. - throw new Error( 'Creation limit is only supported for unit squares.' ); - } - // Create the node that the user will click upon to add a model element to the view. - const representation = new Path( shape, { - fill: color, - stroke: Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), - lineWidth: BORDER_LINE_WIDTH, - lineJoin: 'round' - } ); - this.addChild( representation ); - - // Add grid if specified. - if ( options.gridSpacing ) { - const gridNode = new Grid( representation.bounds.dilated( -BORDER_LINE_WIDTH ), options.gridSpacing, { - lineDash: [ 0, 3, 1, 0 ], - stroke: 'black' - } ); - this.addChild( gridNode ); - } +import Property from '../../../../axon/js/Property.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Vector2Property from '../../../../dot/js/Vector2Property.js'; +import ScreenView from '../../../../joist/js/ScreenView.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import MovableDragHandler from '../../../../scenery-phet/js/input/MovableDragHandler.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import MovableShape from '../model/MovableShape.js'; +import Grid from './Grid.js'; + +// constants +const BORDER_LINE_WIDTH = 1; - const createdCountProperty = new Property( 0 ); // Used to track the number of shapes created and not returned. +/** + * @param {Shape} shape + * @param {string || Color} color + * @param {function} addShapeToModel - A function for adding the created shape to the model + * @param {Object} [options] + * @constructor + */ +function ShapeCreatorNode( shape, color, addShapeToModel, options ) { + assert && assert( shape.bounds.minX === 0 && shape.bounds.minY === 0, 'Error: Shape is expected to be located at 0, 0' ); + Node.call( this, { cursor: 'pointer' } ); + const self = this; + + options = merge( { + + // Spacing of the grid, if any, that should be shown on the creator node. Null indicates no grid. + gridSpacing: null, + + // Max number of shapes that can be created by this node. + creationLimit: Number.POSITIVE_INFINITY, + + // Drag bounds for the created shapes. + shapeDragBounds: Bounds2.EVERYTHING, + + // This is a node that is or will be somewhere up the scene graph tree from this ShapeCreatorNode, doesn't move, + // and whose parent has the coordinate frame needed to do the appropriate transformations when the a drag takes + // place on this ShapeCreatorNode. This is needed in cases where the ShapeCreatorNode can be moved while a drag + // of a created node is still in progress. This can occur when the ShapeCreatorNode is placed on a carousel and + // the sim is being used in a multi-touch environment. See https://github.com/phetsims/area-builder/issues/95 for + // more information. + nonMovingAncestor: null + }, options ); + + // parameter check + if ( options.creationLimit < Number.POSITIVE_INFINITY && + ( shape.bounds.width !== AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH || + shape.bounds.height !== AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH ) ) { + // The ability to set a creation limit ONLY works for unit squares. The reason for this is that non-unit shapes + // are generally decomposed into unit squares when added to the placement board, so it's hard to track when they + // get returned to their origin. It would be possible to do this, but the requirements of the sim at the time of + // this writing make it unnecessary. So, if you're hitting this exception, the code may need to be revamped to + // support creation limits for shapes that are not unit squares. + throw new Error( 'Creation limit is only supported for unit squares.' ); + } - // If the created count exceeds the max, make this node invisible (which also makes it unusable). - createdCountProperty.link( function( numCreated ) { - self.visible = numCreated < options.creationLimit; + // Create the node that the user will click upon to add a model element to the view. + const representation = new Path( shape, { + fill: color, + stroke: Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), + lineWidth: BORDER_LINE_WIDTH, + lineJoin: 'round' + } ); + this.addChild( representation ); + + // Add grid if specified. + if ( options.gridSpacing ) { + const gridNode = new Grid( representation.bounds.dilated( -BORDER_LINE_WIDTH ), options.gridSpacing, { + lineDash: [ 0, 3, 1, 0 ], + stroke: 'black' } ); + this.addChild( gridNode ); + } - // variables used by the drag handler - let parentScreenView = null; // needed for coordinate transforms - let movableShape = null; - const shapePositionProperty = new Vector2Property( Vector2.ZERO ); + const createdCountProperty = new Property( 0 ); // Used to track the number of shapes created and not returned. - // Link the internal position property to the movable shape. - shapePositionProperty.link( function( position ){ - if ( movableShape !== null ){ - movableShape.positionProperty.set( position ); - } - } ); + // If the created count exceeds the max, make this node invisible (which also makes it unusable). + createdCountProperty.link( function( numCreated ) { + self.visible = numCreated < options.creationLimit; + } ); + + // variables used by the drag handler + let parentScreenView = null; // needed for coordinate transforms + let movableShape = null; + const shapePositionProperty = new Vector2Property( Vector2.ZERO ); - // Adjust the drag bounds to compensate for the shape that that the entire shape will stay in bounds. - const shapeDragBounds = options.shapeDragBounds.copy(); - shapeDragBounds.setMaxX( shapeDragBounds.maxX - shape.bounds.width ); - shapeDragBounds.setMaxY( shapeDragBounds.maxY - shape.bounds.height ); + // Link the internal position property to the movable shape. + shapePositionProperty.link( function( position ) { + if ( movableShape !== null ) { + movableShape.positionProperty.set( position ); + } + } ); - // Add the listener that will allow the user to click on this and create a new shape, then position it in the model. - this.addInputListener( new MovableDragHandler( shapePositionProperty, { + // Adjust the drag bounds to compensate for the shape that that the entire shape will stay in bounds. + const shapeDragBounds = options.shapeDragBounds.copy(); + shapeDragBounds.setMaxX( shapeDragBounds.maxX - shape.bounds.width ); + shapeDragBounds.setMaxY( shapeDragBounds.maxY - shape.bounds.height ); - dragBounds: shapeDragBounds, - targetNode: options.nonMovingAncestor, + // Add the listener that will allow the user to click on this and create a new shape, then position it in the model. + this.addInputListener( new MovableDragHandler( shapePositionProperty, { - // Allow moving a finger (touch) across this node to interact with it - allowTouchSnag: true, + dragBounds: shapeDragBounds, + targetNode: options.nonMovingAncestor, - startDrag: function( event, trail ) { - if ( !parentScreenView ) { + // Allow moving a finger (touch) across this node to interact with it + allowTouchSnag: true, - // Find the parent screen view by moving up the scene graph. - let testNode = self; - while ( testNode !== null ) { - if ( testNode instanceof ScreenView ) { - parentScreenView = testNode; - break; - } - testNode = testNode.parents[ 0 ]; // move up the scene graph by one level - } - assert && assert( parentScreenView, 'unable to find parent screen view' ); - } + startDrag: function( event, trail ) { + if ( !parentScreenView ) { - // Determine the initial position of the new element as a function of the event position and this node's bounds. - const upperLeftCornerGlobal = self.parentToGlobalPoint( self.leftTop ); - const initialPositionOffset = upperLeftCornerGlobal.minus( event.pointer.point ); - const initialPosition = parentScreenView.globalToLocalPoint( event.pointer.point.plus( initialPositionOffset ) ); - shapePositionProperty.value = initialPosition; - - // Create and add the new model element. - movableShape = new MovableShape( shape, color, initialPosition ); - movableShape.userControlledProperty.set( true ); - addShapeToModel( movableShape ); - - // If the creation count is limited, adjust the value and monitor the created shape for if/when it is returned. - if ( options.creationLimit < Number.POSITIVE_INFINITY ) { - // Use an IIFE to keep a reference of the movable shape in a closure. - ( function() { - createdCountProperty.value++; - const localRefToMovableShape = movableShape; - localRefToMovableShape.returnedToOriginEmitter.addListener( function returnedToOriginListener() { - if ( !localRefToMovableShape.userControlledProperty.get() ) { - // The shape has been returned to its origin. - createdCountProperty.value--; - localRefToMovableShape.returnedToOriginEmitter.removeListener( returnedToOriginListener ); - } - } ); - } )(); + // Find the parent screen view by moving up the scene graph. + let testNode = self; + while ( testNode !== null ) { + if ( testNode instanceof ScreenView ) { + parentScreenView = testNode; + break; + } + testNode = testNode.parents[ 0 ]; // move up the scene graph by one level } - }, + assert && assert( parentScreenView, 'unable to find parent screen view' ); + } - endDrag: function( event, trail ) { - movableShape.userControlledProperty.set( false ); - movableShape = null; + // Determine the initial position of the new element as a function of the event position and this node's bounds. + const upperLeftCornerGlobal = self.parentToGlobalPoint( self.leftTop ); + const initialPositionOffset = upperLeftCornerGlobal.minus( event.pointer.point ); + const initialPosition = parentScreenView.globalToLocalPoint( event.pointer.point.plus( initialPositionOffset ) ); + shapePositionProperty.value = initialPosition; + + // Create and add the new model element. + movableShape = new MovableShape( shape, color, initialPosition ); + movableShape.userControlledProperty.set( true ); + addShapeToModel( movableShape ); + + // If the creation count is limited, adjust the value and monitor the created shape for if/when it is returned. + if ( options.creationLimit < Number.POSITIVE_INFINITY ) { + // Use an IIFE to keep a reference of the movable shape in a closure. + ( function() { + createdCountProperty.value++; + const localRefToMovableShape = movableShape; + localRefToMovableShape.returnedToOriginEmitter.addListener( function returnedToOriginListener() { + if ( !localRefToMovableShape.userControlledProperty.get() ) { + // The shape has been returned to its origin. + createdCountProperty.value--; + localRefToMovableShape.returnedToOriginEmitter.removeListener( returnedToOriginListener ); + } + } ); + } )(); } - } ) ); + }, - // Pass options through to parent. - this.mutate( options ); - } + endDrag: function( event, trail ) { + movableShape.userControlledProperty.set( false ); + movableShape = null; + } + } ) ); + + // Pass options through to parent. + this.mutate( options ); +} - areaBuilder.register( 'ShapeCreatorNode', ShapeCreatorNode ); +areaBuilder.register( 'ShapeCreatorNode', ShapeCreatorNode ); - return inherit( Node, ShapeCreatorNode ); -} ); \ No newline at end of file +inherit( Node, ShapeCreatorNode ); +export default ShapeCreatorNode; \ No newline at end of file diff --git a/js/common/view/ShapeNode.js b/js/common/view/ShapeNode.js index 2f3238d..d9f80ea 100644 --- a/js/common/view/ShapeNode.js +++ b/js/common/view/ShapeNode.js @@ -5,164 +5,161 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const Color = require( 'SCENERY/util/Color' ); - const DerivedProperty = require( 'AXON/DerivedProperty' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const MovableDragHandler = require( 'SCENERY_PHET/input/MovableDragHandler' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const SHADOW_COLOR = 'rgba( 50, 50, 50, 0.5 )'; - const SHADOW_OFFSET = new Vector2( 5, 5 ); - const OPACITY_OF_TRANSLUCENT_SHAPES = 0.65; // Value empirically determined. - const UNIT_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; - const BORDER_LINE_WIDTH = 1; - - /** - * @param {MovableShape} movableShape - * @param {Bounds2} dragBounds - * @constructor - */ - function ShapeNode( movableShape, dragBounds ) { - Node.call( this, { cursor: 'pointer' } ); - const self = this; - this.color = movableShape.color; // @public - - // Set up the mouse and touch areas for this node so that this can still be grabbed when invisible. - this.touchArea = movableShape.shape; - this.mouseArea = movableShape.shape; - - // Set up a root node whose visibility and opacity will be manipulated below. - const rootNode = new Node(); - this.addChild( rootNode ); - - // Create the shadow - const shadow = new Path( movableShape.shape, { - fill: SHADOW_COLOR, - leftTop: SHADOW_OFFSET - } ); - rootNode.addChild( shadow ); - - // Create the primary representation - const representation = new Path( movableShape.shape, { - fill: movableShape.color, - stroke: Color.toColor( movableShape.color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), - lineWidth: 1, - lineJoin: 'round' - } ); - rootNode.addChild( representation ); - // Add the grid - representation.addChild( new Grid( representation.bounds.dilated( -BORDER_LINE_WIDTH ), UNIT_LENGTH, { - lineDash: [ 0, 3, 1, 0 ], - stroke: 'black' - } ) ); +import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import MovableDragHandler from '../../../../scenery-phet/js/input/MovableDragHandler.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../AreaBuilderSharedConstants.js'; +import Grid from './Grid.js'; + +// constants +const SHADOW_COLOR = 'rgba( 50, 50, 50, 0.5 )'; +const SHADOW_OFFSET = new Vector2( 5, 5 ); +const OPACITY_OF_TRANSLUCENT_SHAPES = 0.65; // Value empirically determined. +const UNIT_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; +const BORDER_LINE_WIDTH = 1; - // Move this node as the model representation moves - movableShape.positionProperty.link( function( position ) { - self.leftTop = position; +/** + * @param {MovableShape} movableShape + * @param {Bounds2} dragBounds + * @constructor + */ +function ShapeNode( movableShape, dragBounds ) { + Node.call( this, { cursor: 'pointer' } ); + const self = this; + this.color = movableShape.color; // @public + + // Set up the mouse and touch areas for this node so that this can still be grabbed when invisible. + this.touchArea = movableShape.shape; + this.mouseArea = movableShape.shape; + + // Set up a root node whose visibility and opacity will be manipulated below. + const rootNode = new Node(); + this.addChild( rootNode ); + + // Create the shadow + const shadow = new Path( movableShape.shape, { + fill: SHADOW_COLOR, + leftTop: SHADOW_OFFSET + } ); + rootNode.addChild( shadow ); + + // Create the primary representation + const representation = new Path( movableShape.shape, { + fill: movableShape.color, + stroke: Color.toColor( movableShape.color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), + lineWidth: 1, + lineJoin: 'round' + } ); + rootNode.addChild( representation ); + + // Add the grid + representation.addChild( new Grid( representation.bounds.dilated( -BORDER_LINE_WIDTH ), UNIT_LENGTH, { + lineDash: [ 0, 3, 1, 0 ], + stroke: 'black' + } ) ); + + // Move this node as the model representation moves + movableShape.positionProperty.link( function( position ) { + self.leftTop = position; + } ); + + // Because a composite shape is often used to depict the overall shape when a shape is on the placement board, this + // element may become invisible unless it is user controlled, animating, or fading. + const visibleProperty = new DerivedProperty( [ + movableShape.userControlledProperty, + movableShape.animatingProperty, + movableShape.fadeProportionProperty, + movableShape.invisibleWhenStillProperty ], + function( userControlled, animating, fadeProportion, invisibleWhenStill ) { + return ( userControlled || animating || fadeProportion > 0 || !invisibleWhenStill ); } ); - // Because a composite shape is often used to depict the overall shape when a shape is on the placement board, this - // element may become invisible unless it is user controlled, animating, or fading. - const visibleProperty = new DerivedProperty( [ - movableShape.userControlledProperty, - movableShape.animatingProperty, - movableShape.fadeProportionProperty, - movableShape.invisibleWhenStillProperty ], - function( userControlled, animating, fadeProportion, invisibleWhenStill ) { - return ( userControlled || animating || fadeProportion > 0 || !invisibleWhenStill ); - } ); - - // Opacity is also a derived property, range is 0 to 1. - const opacityProperty = new DerivedProperty( [ - movableShape.userControlledProperty, - movableShape.animatingProperty, - movableShape.fadeProportionProperty ], - function( userControlled, animating, fadeProportion ) { - if ( userControlled || animating ) { - // The shape is either being dragged by the user or is moving to a position, so should be fully opaque. - return 1; - } - else if ( fadeProportion > 0 ) { - // The shape is fading away. - return 1 - fadeProportion; - } - else { - // The shape is not controlled by the user, animated, or fading, so it is most likely placed on the board. - // If it is visible, it will be translucent, since some of the games use shapes in this state to place over - // other shapes for comparative purposes. - return OPACITY_OF_TRANSLUCENT_SHAPES; - } + // Opacity is also a derived property, range is 0 to 1. + const opacityProperty = new DerivedProperty( [ + movableShape.userControlledProperty, + movableShape.animatingProperty, + movableShape.fadeProportionProperty ], + function( userControlled, animating, fadeProportion ) { + if ( userControlled || animating ) { + // The shape is either being dragged by the user or is moving to a position, so should be fully opaque. + return 1; + } + else if ( fadeProportion > 0 ) { + // The shape is fading away. + return 1 - fadeProportion; } - ); + else { + // The shape is not controlled by the user, animated, or fading, so it is most likely placed on the board. + // If it is visible, it will be translucent, since some of the games use shapes in this state to place over + // other shapes for comparative purposes. + return OPACITY_OF_TRANSLUCENT_SHAPES; + } + } + ); - opacityProperty.link( function( opacity ) { - rootNode.opacity = opacity; - } ); + opacityProperty.link( function( opacity ) { + rootNode.opacity = opacity; + } ); - visibleProperty.link( function( visible ) { - rootNode.visible = visible; - } ); + visibleProperty.link( function( visible ) { + rootNode.visible = visible; + } ); - const shadowVisibilityProperty = new DerivedProperty( - [ movableShape.userControlledProperty, movableShape.animatingProperty ], - function( userControlled, animating ) { - return ( userControlled || animating ); - } ); + const shadowVisibilityProperty = new DerivedProperty( + [ movableShape.userControlledProperty, movableShape.animatingProperty ], + function( userControlled, animating ) { + return ( userControlled || animating ); + } ); - shadowVisibilityProperty.linkAttribute( shadow, 'visible' ); + shadowVisibilityProperty.linkAttribute( shadow, 'visible' ); - function updatePickability(){ - // To avoid certain complications, this node should not be pickable if it is animating or fading. - self.pickable = !movableShape.animatingProperty.get() && movableShape.fadeProportionProperty.get() === 0; - } + function updatePickability() { + // To avoid certain complications, this node should not be pickable if it is animating or fading. + self.pickable = !movableShape.animatingProperty.get() && movableShape.fadeProportionProperty.get() === 0; + } - movableShape.animatingProperty.link( function() { - updatePickability(); - } ); + movableShape.animatingProperty.link( function() { + updatePickability(); + } ); - movableShape.fadeProportionProperty.link( function( fadeProportion ) { - updatePickability(); - } ); + movableShape.fadeProportionProperty.link( function( fadeProportion ) { + updatePickability(); + } ); - // Adjust the drag bounds to compensate for the shape that that the entire shape will stay in bounds. - const shapeDragBounds = new Bounds2( - dragBounds.minX, - dragBounds.minY, - dragBounds.maxX - movableShape.shape.bounds.width, - dragBounds.maxY - movableShape.shape.bounds.height - ); + // Adjust the drag bounds to compensate for the shape that that the entire shape will stay in bounds. + const shapeDragBounds = new Bounds2( + dragBounds.minX, + dragBounds.minY, + dragBounds.maxX - movableShape.shape.bounds.width, + dragBounds.maxY - movableShape.shape.bounds.height + ); - // Add the listener that will allow the user to drag the shape around. - this.addInputListener( new MovableDragHandler( movableShape.positionProperty, { + // Add the listener that will allow the user to drag the shape around. + this.addInputListener( new MovableDragHandler( movableShape.positionProperty, { - dragBounds: shapeDragBounds, + dragBounds: shapeDragBounds, - // Allow moving a finger (touch) across a node to pick it up. - allowTouchSnag: true, + // Allow moving a finger (touch) across a node to pick it up. + allowTouchSnag: true, - startDrag: function( event, trail ) { - movableShape.userControlledProperty.set( true ); - }, + startDrag: function( event, trail ) { + movableShape.userControlledProperty.set( true ); + }, - endDrag: function( event, trail ) { - movableShape.userControlledProperty.set( false ); - } - } ) ); - } + endDrag: function( event, trail ) { + movableShape.userControlledProperty.set( false ); + } + } ) ); +} - areaBuilder.register( 'ShapeNode', ShapeNode ); +areaBuilder.register( 'ShapeNode', ShapeNode ); - return inherit( Node, ShapeNode ); -} ); +inherit( Node, ShapeNode ); +export default ShapeNode; \ No newline at end of file diff --git a/js/common/view/ShapePlacementBoardNode.js b/js/common/view/ShapePlacementBoardNode.js index 419666a..0d4683c 100644 --- a/js/common/view/ShapePlacementBoardNode.js +++ b/js/common/view/ShapePlacementBoardNode.js @@ -6,82 +6,79 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const Grid = require( 'AREA_BUILDER/common/view/Grid' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const PerimeterShapeNode = require( 'AREA_BUILDER/common/view/PerimeterShapeNode' ); - const Property = require( 'AXON/Property' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); +import Property from '../../../../axon/js/Property.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import areaBuilder from '../../areaBuilder.js'; +import Grid from './Grid.js'; +import PerimeterShapeNode from './PerimeterShapeNode.js'; - /** - * @param {ShapePlacementBoard} shapePlacementBoard - * @constructor - */ - function ShapePlacementBoardNode( shapePlacementBoard ) { - Node.call( this ); +/** + * @param {ShapePlacementBoard} shapePlacementBoard + * @constructor + */ +function ShapePlacementBoardNode( shapePlacementBoard ) { + Node.call( this ); - // Create and add the board itself. - const board = Rectangle.bounds( shapePlacementBoard.bounds, { fill: 'white', stroke: 'black' } ); - this.addChild( board ); + // Create and add the board itself. + const board = Rectangle.bounds( shapePlacementBoard.bounds, { fill: 'white', stroke: 'black' } ); + this.addChild( board ); - // Create and add the grid - const grid = new Grid( shapePlacementBoard.bounds, shapePlacementBoard.unitSquareLength, { stroke: '#C0C0C0' } ); - this.addChild( grid ); + // Create and add the grid + const grid = new Grid( shapePlacementBoard.bounds, shapePlacementBoard.unitSquareLength, { stroke: '#C0C0C0' } ); + this.addChild( grid ); - // Track and update the grid visibility - shapePlacementBoard.showGridProperty.linkAttribute( grid, 'visible' ); + // Track and update the grid visibility + shapePlacementBoard.showGridProperty.linkAttribute( grid, 'visible' ); - // Monitor the background shape and add/remove/update it as it changes. - this.backgroundShape = new PerimeterShapeNode( - shapePlacementBoard.backgroundShapeProperty, - shapePlacementBoard.bounds, - shapePlacementBoard.unitSquareLength, - shapePlacementBoard.showDimensionsProperty, - shapePlacementBoard.showGridOnBackgroundShapeProperty - ); - this.addChild( this.backgroundShape ); + // Monitor the background shape and add/remove/update it as it changes. + this.backgroundShape = new PerimeterShapeNode( + shapePlacementBoard.backgroundShapeProperty, + shapePlacementBoard.bounds, + shapePlacementBoard.unitSquareLength, + shapePlacementBoard.showDimensionsProperty, + shapePlacementBoard.showGridOnBackgroundShapeProperty + ); + this.addChild( this.backgroundShape ); - // Monitor the shapes added by the user to the board and create an equivalent shape with no edges for each. This - // may seem a little odd - why hide the shapes that the user placed and depict them with essentially the same - // thing minus the edge stroke? The reason is that this makes layering and control of visual modes much easier. - const shapesLayer = new Node(); - this.addChild( shapesLayer ); - shapePlacementBoard.residentShapes.addItemAddedListener( function( addedShape ) { - if ( shapePlacementBoard.formCompositeProperty.get() ) { - // Add a representation of the shape. - const representation = new Path( addedShape.shape, { - fill: addedShape.color, - left: addedShape.positionProperty.get().x, - top: addedShape.positionProperty.get().y - } ); - shapesLayer.addChild( representation ); + // Monitor the shapes added by the user to the board and create an equivalent shape with no edges for each. This + // may seem a little odd - why hide the shapes that the user placed and depict them with essentially the same + // thing minus the edge stroke? The reason is that this makes layering and control of visual modes much easier. + const shapesLayer = new Node(); + this.addChild( shapesLayer ); + shapePlacementBoard.residentShapes.addItemAddedListener( function( addedShape ) { + if ( shapePlacementBoard.formCompositeProperty.get() ) { + // Add a representation of the shape. + const representation = new Path( addedShape.shape, { + fill: addedShape.color, + left: addedShape.positionProperty.get().x, + top: addedShape.positionProperty.get().y + } ); + shapesLayer.addChild( representation ); - shapePlacementBoard.residentShapes.addItemRemovedListener( function removalListener( removedShape ) { - if ( removedShape === addedShape ) { - shapesLayer.removeChild( representation ); - shapePlacementBoard.residentShapes.removeItemRemovedListener( removalListener ); - } - } ); - } - } ); + shapePlacementBoard.residentShapes.addItemRemovedListener( function removalListener( removedShape ) { + if ( removedShape === addedShape ) { + shapesLayer.removeChild( representation ); + shapePlacementBoard.residentShapes.removeItemRemovedListener( removalListener ); + } + } ); + } + } ); - // Add the perimeter shape, which depicts the exterior and interior perimeters formed by the placed shapes. - this.addChild( new PerimeterShapeNode( - shapePlacementBoard.compositeShapeProperty, - shapePlacementBoard.bounds, - shapePlacementBoard.unitSquareLength, - shapePlacementBoard.showDimensionsProperty, - new Property( true ) // grid on shape - always shown for the composite shape - ) ); - } + // Add the perimeter shape, which depicts the exterior and interior perimeters formed by the placed shapes. + this.addChild( new PerimeterShapeNode( + shapePlacementBoard.compositeShapeProperty, + shapePlacementBoard.bounds, + shapePlacementBoard.unitSquareLength, + shapePlacementBoard.showDimensionsProperty, + new Property( true ) // grid on shape - always shown for the composite shape + ) ); +} - areaBuilder.register( 'ShapePlacementBoardNode', ShapePlacementBoardNode ); +areaBuilder.register( 'ShapePlacementBoardNode', ShapePlacementBoardNode ); - return inherit( Node, ShapePlacementBoardNode ); -} ); \ No newline at end of file +inherit( Node, ShapePlacementBoardNode ); +export default ShapePlacementBoardNode; \ No newline at end of file diff --git a/js/explore/AreaBuilderExploreScreen.js b/js/explore/AreaBuilderExploreScreen.js index 46df50f..cd803ee 100644 --- a/js/explore/AreaBuilderExploreScreen.js +++ b/js/explore/AreaBuilderExploreScreen.js @@ -5,46 +5,42 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderExploreModel = require( 'AREA_BUILDER/explore/model/AreaBuilderExploreModel' ); - const AreaBuilderExploreView = require( 'AREA_BUILDER/explore/view/AreaBuilderExploreView' ); - const AreaBuilderIconFactory = require( 'AREA_BUILDER/common/view/AreaBuilderIconFactory' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Image = require( 'SCENERY/nodes/Image' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Property = require( 'AXON/Property' ); - const Screen = require( 'JOIST/Screen' ); - - // strings - const exploreString = require( 'string!AREA_BUILDER/explore' ); - - // images - const exploreIcon = require( 'image!AREA_BUILDER/explore-icon.png' ); - - /** - * @constructor - */ - function AreaBuilderExploreScreen( tandem ) { - - const options = { - name: exploreString, - backgroundColorProperty: new Property( AreaBuilderSharedConstants.BACKGROUND_COLOR ), - homeScreenIcon: new Image( exploreIcon ), - navigationBarIcon: AreaBuilderIconFactory.createExploreScreenNavBarIcon(), - tandem: tandem - }; - - Screen.call( this, - function() { return new AreaBuilderExploreModel(); }, - function( model ) { return new AreaBuilderExploreView( model ); }, - options ); - } - - areaBuilder.register( 'AreaBuilderExploreScreen', AreaBuilderExploreScreen ); - - return inherit( Screen, AreaBuilderExploreScreen ); -} ); \ No newline at end of file + +import Property from '../../../axon/js/Property.js'; +import Screen from '../../../joist/js/Screen.js'; +import inherit from '../../../phet-core/js/inherit.js'; +import Image from '../../../scenery/js/nodes/Image.js'; +import exploreIcon from '../../images/explore-icon_png.js'; +import areaBuilderStrings from '../area-builder-strings.js'; +import areaBuilder from '../areaBuilder.js'; +import AreaBuilderSharedConstants from '../common/AreaBuilderSharedConstants.js'; +import AreaBuilderIconFactory from '../common/view/AreaBuilderIconFactory.js'; +import AreaBuilderExploreModel from './model/AreaBuilderExploreModel.js'; +import AreaBuilderExploreView from './view/AreaBuilderExploreView.js'; + +const exploreString = areaBuilderStrings.explore; + + +/** + * @constructor + */ +function AreaBuilderExploreScreen( tandem ) { + + const options = { + name: exploreString, + backgroundColorProperty: new Property( AreaBuilderSharedConstants.BACKGROUND_COLOR ), + homeScreenIcon: new Image( exploreIcon ), + navigationBarIcon: AreaBuilderIconFactory.createExploreScreenNavBarIcon(), + tandem: tandem + }; + + Screen.call( this, + function() { return new AreaBuilderExploreModel(); }, + function( model ) { return new AreaBuilderExploreView( model ); }, + options ); +} + +areaBuilder.register( 'AreaBuilderExploreScreen', AreaBuilderExploreScreen ); + +inherit( Screen, AreaBuilderExploreScreen ); +export default AreaBuilderExploreScreen; \ No newline at end of file diff --git a/js/explore/model/AreaBuilderExploreModel.js b/js/explore/model/AreaBuilderExploreModel.js index ae80628..d1f0a6d 100644 --- a/js/explore/model/AreaBuilderExploreModel.js +++ b/js/explore/model/AreaBuilderExploreModel.js @@ -5,188 +5,184 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bucket = require( 'PHETCOMMON/model/Bucket' ); - const Dimension2 = require( 'DOT/Dimension2' ); - const inherit = require( 'PHET_CORE/inherit' ); - const MovableShape = require( 'AREA_BUILDER/common/model/MovableShape' ); - const ObservableArray = require( 'AXON/ObservableArray' ); - const Property = require( 'AXON/Property' ); - const Shape = require( 'KITE/Shape' ); - const ShapePlacementBoard = require( 'AREA_BUILDER/common/model/ShapePlacementBoard' ); - const StringProperty = require( 'AXON/StringProperty' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; - const UNIT_SQUARE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); - const SMALL_BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 9, UNIT_SQUARE_LENGTH * 8 ); - const LARGE_BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 19, UNIT_SQUARE_LENGTH * 8 ); - const PLAY_AREA_WIDTH = AreaBuilderSharedConstants.LAYOUT_BOUNDS.width; - const SPACE_BETWEEN_PLACEMENT_BOARDS = UNIT_SQUARE_LENGTH; - const BOARD_Y_POS = 70; // Empirically determined from looking at the layout - const BUCKET_SIZE = new Dimension2( 90, 45 ); - const BOARD_TO_BUCKET_Y_SPACING = 45; - /** - * @constructor - */ - function AreaBuilderExploreModel() { - - this.showShapeBoardGridsProperty = new Property( true ); // @public - this.showDimensionsProperty = new Property( true ); // @public - this.boardDisplayModeProperty = new StringProperty( 'single' ); // @public, value values are 'single' and 'dual' - - this.movableShapes = new ObservableArray(); // @public - this.unitSquareLength = UNIT_SQUARE_LENGTH; // @public, @final - - // Create the shape placement boards. Each boardDisplayMode has its own set of boards and buckets so that state can - // be preserved when switching modes. - this.leftShapePlacementBoard = new ShapePlacementBoard( - SMALL_BOARD_SIZE, - UNIT_SQUARE_LENGTH, - new Vector2( PLAY_AREA_WIDTH / 2 - SPACE_BETWEEN_PLACEMENT_BOARDS / 2 - SMALL_BOARD_SIZE.width, BOARD_Y_POS ), - AreaBuilderSharedConstants.GREENISH_COLOR, - this.showShapeBoardGridsProperty, - this.showDimensionsProperty - ); // @public - this.rightShapePlacementBoard = new ShapePlacementBoard( - SMALL_BOARD_SIZE, - UNIT_SQUARE_LENGTH, - new Vector2( PLAY_AREA_WIDTH / 2 + SPACE_BETWEEN_PLACEMENT_BOARDS / 2, BOARD_Y_POS ), - AreaBuilderSharedConstants.PURPLISH_COLOR, - this.showShapeBoardGridsProperty, - this.showDimensionsProperty - ); // @public - this.singleShapePlacementBoard = new ShapePlacementBoard( - LARGE_BOARD_SIZE, - UNIT_SQUARE_LENGTH, - new Vector2( PLAY_AREA_WIDTH / 2 - LARGE_BOARD_SIZE.width / 2, BOARD_Y_POS ), - AreaBuilderSharedConstants.ORANGISH_COLOR, - this.showShapeBoardGridsProperty, - this.showDimensionsProperty - ); // @public - - // @private, for convenience. - this.shapePlacementBoards = [ this.leftShapePlacementBoard, this.rightShapePlacementBoard, this.singleShapePlacementBoard ]; - - // Create the buckets that will hold the shapes. - const bucketYPos = this.leftShapePlacementBoard.bounds.minY + SMALL_BOARD_SIZE.height + BOARD_TO_BUCKET_Y_SPACING; - this.leftBucket = new Bucket( { - position: new Vector2( this.leftShapePlacementBoard.bounds.minX + SMALL_BOARD_SIZE.width * 0.7, bucketYPos ), - baseColor: '#000080', - size: BUCKET_SIZE, - invertY: true - } ); - this.rightBucket = new Bucket( { - position: new Vector2( this.rightShapePlacementBoard.bounds.minX + SMALL_BOARD_SIZE.width * 0.3, bucketYPos ), - baseColor: '#000080', - size: BUCKET_SIZE, - invertY: true - } ); - this.singleModeBucket = new Bucket( { - position: new Vector2( this.singleShapePlacementBoard.bounds.minX + LARGE_BOARD_SIZE.width / 2, bucketYPos ), - baseColor: '#000080', - size: BUCKET_SIZE, - invertY: true - } ); - } +import ObservableArray from '../../../../axon/js/ObservableArray.js'; +import Property from '../../../../axon/js/Property.js'; +import StringProperty from '../../../../axon/js/StringProperty.js'; +import Dimension2 from '../../../../dot/js/Dimension2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Bucket from '../../../../phetcommon/js/model/Bucket.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import MovableShape from '../../common/model/MovableShape.js'; +import ShapePlacementBoard from '../../common/model/ShapePlacementBoard.js'; + +// constants +const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; +const UNIT_SQUARE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); +const SMALL_BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 9, UNIT_SQUARE_LENGTH * 8 ); +const LARGE_BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 19, UNIT_SQUARE_LENGTH * 8 ); +const PLAY_AREA_WIDTH = AreaBuilderSharedConstants.LAYOUT_BOUNDS.width; +const SPACE_BETWEEN_PLACEMENT_BOARDS = UNIT_SQUARE_LENGTH; +const BOARD_Y_POS = 70; // Empirically determined from looking at the layout +const BUCKET_SIZE = new Dimension2( 90, 45 ); +const BOARD_TO_BUCKET_Y_SPACING = 45; - areaBuilder.register( 'AreaBuilderExploreModel', AreaBuilderExploreModel ); +/** + * @constructor + */ +function AreaBuilderExploreModel() { + + this.showShapeBoardGridsProperty = new Property( true ); // @public + this.showDimensionsProperty = new Property( true ); // @public + this.boardDisplayModeProperty = new StringProperty( 'single' ); // @public, value values are 'single' and 'dual' + + this.movableShapes = new ObservableArray(); // @public + this.unitSquareLength = UNIT_SQUARE_LENGTH; // @public, @final + + // Create the shape placement boards. Each boardDisplayMode has its own set of boards and buckets so that state can + // be preserved when switching modes. + this.leftShapePlacementBoard = new ShapePlacementBoard( + SMALL_BOARD_SIZE, + UNIT_SQUARE_LENGTH, + new Vector2( PLAY_AREA_WIDTH / 2 - SPACE_BETWEEN_PLACEMENT_BOARDS / 2 - SMALL_BOARD_SIZE.width, BOARD_Y_POS ), + AreaBuilderSharedConstants.GREENISH_COLOR, + this.showShapeBoardGridsProperty, + this.showDimensionsProperty + ); // @public + this.rightShapePlacementBoard = new ShapePlacementBoard( + SMALL_BOARD_SIZE, + UNIT_SQUARE_LENGTH, + new Vector2( PLAY_AREA_WIDTH / 2 + SPACE_BETWEEN_PLACEMENT_BOARDS / 2, BOARD_Y_POS ), + AreaBuilderSharedConstants.PURPLISH_COLOR, + this.showShapeBoardGridsProperty, + this.showDimensionsProperty + ); // @public + this.singleShapePlacementBoard = new ShapePlacementBoard( + LARGE_BOARD_SIZE, + UNIT_SQUARE_LENGTH, + new Vector2( PLAY_AREA_WIDTH / 2 - LARGE_BOARD_SIZE.width / 2, BOARD_Y_POS ), + AreaBuilderSharedConstants.ORANGISH_COLOR, + this.showShapeBoardGridsProperty, + this.showDimensionsProperty + ); // @public + + // @private, for convenience. + this.shapePlacementBoards = [ this.leftShapePlacementBoard, this.rightShapePlacementBoard, this.singleShapePlacementBoard ]; + + // Create the buckets that will hold the shapes. + const bucketYPos = this.leftShapePlacementBoard.bounds.minY + SMALL_BOARD_SIZE.height + BOARD_TO_BUCKET_Y_SPACING; + this.leftBucket = new Bucket( { + position: new Vector2( this.leftShapePlacementBoard.bounds.minX + SMALL_BOARD_SIZE.width * 0.7, bucketYPos ), + baseColor: '#000080', + size: BUCKET_SIZE, + invertY: true + } ); + this.rightBucket = new Bucket( { + position: new Vector2( this.rightShapePlacementBoard.bounds.minX + SMALL_BOARD_SIZE.width * 0.3, bucketYPos ), + baseColor: '#000080', + size: BUCKET_SIZE, + invertY: true + } ); + this.singleModeBucket = new Bucket( { + position: new Vector2( this.singleShapePlacementBoard.bounds.minX + LARGE_BOARD_SIZE.width / 2, bucketYPos ), + baseColor: '#000080', + size: BUCKET_SIZE, + invertY: true + } ); +} + +areaBuilder.register( 'AreaBuilderExploreModel', AreaBuilderExploreModel ); + +export default inherit( Object, AreaBuilderExploreModel, { - return inherit( Object, AreaBuilderExploreModel, { + step: function( dt ) { + this.movableShapes.forEach( function( movableShape ) { movableShape.step( dt ); } ); + }, - step: function( dt ) { - this.movableShapes.forEach( function( movableShape ) { movableShape.step( dt ); } ); - }, + placeShape: function( movableShape ) { + let shapePlaced = false; + for ( let i = 0; i < this.shapePlacementBoards.length && !shapePlaced; i++ ) { + shapePlaced = this.shapePlacementBoards[ i ].placeShape( movableShape ); + } + if ( !shapePlaced ) { + movableShape.returnToOrigin( true ); + } + }, - placeShape: function( movableShape ) { - let shapePlaced = false; - for ( let i = 0; i < this.shapePlacementBoards.length && !shapePlaced; i++ ) { - shapePlaced = this.shapePlacementBoards[ i ].placeShape( movableShape ); + /** + * Function for adding new movable shapes to this model when the user creates them, generally by clicking on some + * some sort of creator node. + * @public + * @param movableShape + */ + addUserCreatedMovableShape: function( movableShape ) { + const self = this; + this.movableShapes.push( movableShape ); + movableShape.userControlledProperty.link( function( userControlled ) { + if ( !userControlled ) { + self.placeShape( movableShape ); } - if ( !shapePlaced ) { - movableShape.returnToOrigin( true ); + } ); + + // The shape will be removed from the model if and when it returns to its origination point. This is how a shape + // can be 'put back' into the bucket. + movableShape.returnedToOriginEmitter.addListener( function() { + if ( !movableShape.userControlledProperty.get() ) { + // The shape has been returned to the bucket. + self.movableShapes.remove( movableShape ); } - }, - - /** - * Function for adding new movable shapes to this model when the user creates them, generally by clicking on some - * some sort of creator node. - * @public - * @param movableShape - */ - addUserCreatedMovableShape: function( movableShape ) { - const self = this; - this.movableShapes.push( movableShape ); - movableShape.userControlledProperty.link( function( userControlled ) { - if ( !userControlled ) { - self.placeShape( movableShape ); - } - } ); + } ); - // The shape will be removed from the model if and when it returns to its origination point. This is how a shape - // can be 'put back' into the bucket. - movableShape.returnedToOriginEmitter.addListener( function() { - if ( !movableShape.userControlledProperty.get() ) { - // The shape has been returned to the bucket. - self.movableShapes.remove( movableShape ); - } - } ); + // Another point at which the shape is removed is if it fades away. + movableShape.fadeProportionProperty.link( function fadeHandler( fadeProportion ) { + if ( fadeProportion === 1 ) { + self.movableShapes.remove( movableShape ); + movableShape.fadeProportionProperty.unlink( fadeHandler ); + } + } ); + }, - // Another point at which the shape is removed is if it fades away. - movableShape.fadeProportionProperty.link( function fadeHandler( fadeProportion ) { - if ( fadeProportion === 1 ) { - self.movableShapes.remove( movableShape ); - movableShape.fadeProportionProperty.unlink( fadeHandler ); - } - } ); - }, - - /** - * fill the boards with unit squares, useful for debugging, not used in general operation of the sim - */ - fillBoards: function() { - const self = this; - this.shapePlacementBoards.forEach( function( board ) { - const numRows = board.bounds.height / UNIT_SQUARE_LENGTH; - const numColumns = board.bounds.width / UNIT_SQUARE_LENGTH; - let movableShape; - let shapeOrigin; - if ( board === self.leftShapePlacementBoard ){ - shapeOrigin = self.leftBucket.position; - } - else if ( board === self.rightShapePlacementBoard ){ - shapeOrigin = self.rightBucket.position; - } - else{ - shapeOrigin = self.singleModeBucket.position; - } - _.times( numColumns, function( columnIndex ) { - _.times( numRows, function( rowIndex ) { - movableShape = new MovableShape( UNIT_SQUARE_SHAPE, board.colorHandled, shapeOrigin ); - movableShape.positionProperty.set( new Vector2( - board.bounds.minX + columnIndex * UNIT_SQUARE_LENGTH, - board.bounds.minY + rowIndex * UNIT_SQUARE_LENGTH - ) ); - self.addUserCreatedMovableShape( movableShape ); - } ); + /** + * fill the boards with unit squares, useful for debugging, not used in general operation of the sim + */ + fillBoards: function() { + const self = this; + this.shapePlacementBoards.forEach( function( board ) { + const numRows = board.bounds.height / UNIT_SQUARE_LENGTH; + const numColumns = board.bounds.width / UNIT_SQUARE_LENGTH; + let movableShape; + let shapeOrigin; + if ( board === self.leftShapePlacementBoard ) { + shapeOrigin = self.leftBucket.position; + } + else if ( board === self.rightShapePlacementBoard ) { + shapeOrigin = self.rightBucket.position; + } + else { + shapeOrigin = self.singleModeBucket.position; + } + _.times( numColumns, function( columnIndex ) { + _.times( numRows, function( rowIndex ) { + movableShape = new MovableShape( UNIT_SQUARE_SHAPE, board.colorHandled, shapeOrigin ); + movableShape.positionProperty.set( new Vector2( + board.bounds.minX + columnIndex * UNIT_SQUARE_LENGTH, + board.bounds.minY + rowIndex * UNIT_SQUARE_LENGTH + ) ); + self.addUserCreatedMovableShape( movableShape ); } ); } ); - }, - - // Resets all model elements - reset: function() { - this.showShapeBoardGridsProperty.reset(); - this.showDimensionsProperty.reset(); - this.boardDisplayModeProperty.reset(); - this.shapePlacementBoards.forEach( function( board ) { board.releaseAllShapes( 'jumpHome' ); } ); - this.movableShapes.clear(); - } - } ); -} ); + } ); + }, + + // Resets all model elements + reset: function() { + this.showShapeBoardGridsProperty.reset(); + this.showDimensionsProperty.reset(); + this.boardDisplayModeProperty.reset(); + this.shapePlacementBoards.forEach( function( board ) { board.releaseAllShapes( 'jumpHome' ); } ); + this.movableShapes.clear(); + } +} ); \ No newline at end of file diff --git a/js/explore/view/AreaAndPerimeterDisplay.js b/js/explore/view/AreaAndPerimeterDisplay.js index 8a4c9ec..04d61f6 100644 --- a/js/explore/view/AreaAndPerimeterDisplay.js +++ b/js/explore/view/AreaAndPerimeterDisplay.js @@ -5,96 +5,92 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const AccordionBox = require( 'SUN/AccordionBox' ); - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Property = require( 'AXON/Property' ); - const Text = require( 'SCENERY/nodes/Text' ); +import Property from '../../../../axon/js/Property.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import AccordionBox from '../../../../sun/js/AccordionBox.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; - // strings - const areaString = require( 'string!AREA_BUILDER/area' ); - const perimeterString = require( 'string!AREA_BUILDER/perimeter' ); - const valuesString = require( 'string!AREA_BUILDER/values' ); +const areaString = areaBuilderStrings.area; +const perimeterString = areaBuilderStrings.perimeter; +const valuesString = areaBuilderStrings.values; - // constants - const DISPLAY_FONT = new PhetFont( 14 ); - const MAX_CONTENT_WIDTH = 200; // empirically determined, supports translation - const MAX_TITLE_WIDTH = 190; // empirically determined, supports translation +// constants +const DISPLAY_FONT = new PhetFont( 14 ); +const MAX_CONTENT_WIDTH = 200; // empirically determined, supports translation +const MAX_TITLE_WIDTH = 190; // empirically determined, supports translation - /** - * @param {Property} areaAndPerimeterProperty - An object containing values for area and perimeter - * @param {Color} areaTextColor - * @param {Color} perimeterTextColor - * @param {Object} [options] - * @constructor - */ - function AreaAndPerimeterDisplay( areaAndPerimeterProperty, areaTextColor, perimeterTextColor, options ) { - - options = merge( { - maxWidth: Number.POSITIVE_INFINITY - }, options ); - - const contentNode = new Node(); - const areaCaption = new Text( areaString, { font: DISPLAY_FONT } ); - const perimeterCaption = new Text( perimeterString, { font: DISPLAY_FONT } ); - const tempTwoDigitString = new Text( '999', { font: DISPLAY_FONT } ); - const areaReadout = new Text( '', { font: DISPLAY_FONT, fill: areaTextColor } ); - const perimeterReadout = new Text( '', { font: DISPLAY_FONT, fill: perimeterTextColor } ); +/** + * @param {Property} areaAndPerimeterProperty - An object containing values for area and perimeter + * @param {Color} areaTextColor + * @param {Color} perimeterTextColor + * @param {Object} [options] + * @constructor + */ +function AreaAndPerimeterDisplay( areaAndPerimeterProperty, areaTextColor, perimeterTextColor, options ) { - contentNode.addChild( areaCaption ); - perimeterCaption.left = areaCaption.left; - perimeterCaption.top = areaCaption.bottom + 5; - contentNode.addChild( perimeterCaption ); - contentNode.addChild( areaReadout ); - contentNode.addChild( perimeterReadout ); - const readoutsRightEdge = Math.max( perimeterCaption.right, areaCaption.right ) + 8 + tempTwoDigitString.width; + options = merge( { + maxWidth: Number.POSITIVE_INFINITY + }, options ); - areaAndPerimeterProperty.link( function( areaAndPerimeter ) { - areaReadout.text = areaAndPerimeter.area; - areaReadout.bottom = areaCaption.bottom; - areaReadout.right = readoutsRightEdge; - perimeterReadout.text = areaAndPerimeter.perimeter; - perimeterReadout.bottom = perimeterCaption.bottom; - perimeterReadout.right = readoutsRightEdge; - } ); + const contentNode = new Node(); + const areaCaption = new Text( areaString, { font: DISPLAY_FONT } ); + const perimeterCaption = new Text( perimeterString, { font: DISPLAY_FONT } ); + const tempTwoDigitString = new Text( '999', { font: DISPLAY_FONT } ); + const areaReadout = new Text( '', { font: DISPLAY_FONT, fill: areaTextColor } ); + const perimeterReadout = new Text( '', { font: DISPLAY_FONT, fill: perimeterTextColor } ); - // in support of translation, scale the content node if it's too big - if ( contentNode.width > MAX_CONTENT_WIDTH ){ - contentNode.scale( MAX_CONTENT_WIDTH / contentNode.width ); - } + contentNode.addChild( areaCaption ); + perimeterCaption.left = areaCaption.left; + perimeterCaption.top = areaCaption.bottom + 5; + contentNode.addChild( perimeterCaption ); + contentNode.addChild( areaReadout ); + contentNode.addChild( perimeterReadout ); + const readoutsRightEdge = Math.max( perimeterCaption.right, areaCaption.right ) + 8 + tempTwoDigitString.width; - this.expandedProperty = new Property( true ); - AccordionBox.call( this, contentNode, { - cornerRadius: 3, - titleNode: new Text( valuesString, { font: DISPLAY_FONT, maxWidth: MAX_TITLE_WIDTH } ), - titleAlignX: 'left', - contentAlign: 'left', - fill: 'white', - showTitleWhenExpanded: false, - contentXMargin: 8, - contentYMargin: 4, - expandedProperty: this.expandedProperty, - expandCollapseButtonOptions: { - touchAreaXDilation: 10, - touchAreaYDilation: 10 - } - } ); + areaAndPerimeterProperty.link( function( areaAndPerimeter ) { + areaReadout.text = areaAndPerimeter.area; + areaReadout.bottom = areaCaption.bottom; + areaReadout.right = readoutsRightEdge; + perimeterReadout.text = areaAndPerimeter.perimeter; + perimeterReadout.bottom = perimeterCaption.bottom; + perimeterReadout.right = readoutsRightEdge; + } ); - this.mutate( options ); + // in support of translation, scale the content node if it's too big + if ( contentNode.width > MAX_CONTENT_WIDTH ) { + contentNode.scale( MAX_CONTENT_WIDTH / contentNode.width ); } - areaBuilder.register( 'AreaAndPerimeterDisplay', AreaAndPerimeterDisplay ); - - return inherit( AccordionBox, AreaAndPerimeterDisplay, { - reset: function() { - this.expandedProperty.reset(); + this.expandedProperty = new Property( true ); + AccordionBox.call( this, contentNode, { + cornerRadius: 3, + titleNode: new Text( valuesString, { font: DISPLAY_FONT, maxWidth: MAX_TITLE_WIDTH } ), + titleAlignX: 'left', + contentAlign: 'left', + fill: 'white', + showTitleWhenExpanded: false, + contentXMargin: 8, + contentYMargin: 4, + expandedProperty: this.expandedProperty, + expandCollapseButtonOptions: { + touchAreaXDilation: 10, + touchAreaYDilation: 10 } } ); + + this.mutate( options ); +} + +areaBuilder.register( 'AreaAndPerimeterDisplay', AreaAndPerimeterDisplay ); + +export default inherit( AccordionBox, AreaAndPerimeterDisplay, { + reset: function() { + this.expandedProperty.reset(); + } } ); \ No newline at end of file diff --git a/js/explore/view/AreaBuilderExploreView.js b/js/explore/view/AreaBuilderExploreView.js index 8f77fa5..1ba3534 100644 --- a/js/explore/view/AreaBuilderExploreView.js +++ b/js/explore/view/AreaBuilderExploreView.js @@ -5,99 +5,99 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderControlPanel = require( 'AREA_BUILDER/common/view/AreaBuilderControlPanel' ); - const AreaBuilderQueryParameters = require( 'AREA_BUILDER/common/AreaBuilderQueryParameters' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const BoardDisplayModePanel = require( 'AREA_BUILDER/explore/view/BoardDisplayModePanel' ); - const ExploreNode = require( 'AREA_BUILDER/explore/view/ExploreNode' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' ); - const ScreenView = require( 'JOIST/ScreenView' ); - - // constants - const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; - - /** - * @param {AreaBuilderExploreModel} model - * @constructor - */ - function AreaBuilderExploreView( model ) { - - ScreenView.call( this, { layoutBounds: AreaBuilderSharedConstants.LAYOUT_BOUNDS } ); - - // Create the layers where the shapes will be placed. The shapes are maintained in separate layers so that they - // are over all of the shape placement boards in the z-order. - const movableShapesLayer = new Node( { layerSplit: true } ); // Force the moving shape into a separate layer for improved performance. - const singleBoardShapesLayer = new Node(); - movableShapesLayer.addChild( singleBoardShapesLayer ); - const dualBoardShapesLayer = new Node(); - movableShapesLayer.addChild( dualBoardShapesLayer ); - - // Create the composite nodes that contain the shape placement board, the readout, the bucket, the shape creator - // nodes, and the eraser button. - const centerExploreNode = new ExploreNode( model.singleShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), - model.movableShapes, model.singleModeBucket, { shapesLayer: singleBoardShapesLayer, shapeDragBounds: this.layoutBounds } ); - this.addChild( centerExploreNode ); - const leftExploreNode = new ExploreNode( model.leftShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), - model.movableShapes, model.leftBucket, { shapesLayer: dualBoardShapesLayer, shapeDragBounds: this.layoutBounds } ); - this.addChild( leftExploreNode ); - const rightExploreNode = new ExploreNode( model.rightShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), - model.movableShapes, model.rightBucket, { shapesLayer: dualBoardShapesLayer, shapeDragBounds: this.layoutBounds } ); - this.addChild( rightExploreNode ); - - // Control which board(s), bucket(s), and shapes are visible. - model.boardDisplayModeProperty.link( function( boardDisplayMode ) { - centerExploreNode.visible = boardDisplayMode === 'single'; - singleBoardShapesLayer.pickable = boardDisplayMode === 'single'; - leftExploreNode.visible = boardDisplayMode === 'dual'; - rightExploreNode.visible = boardDisplayMode === 'dual'; - dualBoardShapesLayer.pickable = boardDisplayMode === 'dual'; - } ); - // Create and add the panel that contains the ABSwitch. - const switchPanel = new BoardDisplayModePanel( model.boardDisplayModeProperty ); - this.addChild( switchPanel ); - - // Create and add the common control panel. - const controlPanel = new AreaBuilderControlPanel( model.showShapeBoardGridsProperty, model.showDimensionsProperty ); - this.addChild( controlPanel ); - - // Add the reset button. - this.addChild( new ResetAllButton( { - radius: AreaBuilderSharedConstants.RESET_BUTTON_RADIUS, - right: this.layoutBounds.width - AreaBuilderSharedConstants.CONTROLS_INSET, - bottom: this.layoutBounds.height - AreaBuilderSharedConstants.CONTROLS_INSET, - listener: function() { - centerExploreNode.reset(); - leftExploreNode.reset(); - rightExploreNode.reset(); - model.reset(); - } - } ) ); - - // Add the layers where the movable shapes reside. - this.addChild( movableShapesLayer ); - - // Perform final layout adjustments - const centerBoardBounds = model.singleShapePlacementBoard.bounds; - controlPanel.top = centerBoardBounds.maxY + SPACE_AROUND_SHAPE_PLACEMENT_BOARD; - controlPanel.left = centerBoardBounds.minX; - switchPanel.top = centerBoardBounds.maxY + SPACE_AROUND_SHAPE_PLACEMENT_BOARD; - switchPanel.right = centerBoardBounds.maxX; - - // If the appropriate query parameter is set, fill the boards. This is useful for debugging. - if ( AreaBuilderQueryParameters.prefillBoards ){ - model.fillBoards(); +import ScreenView from '../../../../joist/js/ScreenView.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import ResetAllButton from '../../../../scenery-phet/js/buttons/ResetAllButton.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderQueryParameters from '../../common/AreaBuilderQueryParameters.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import AreaBuilderControlPanel from '../../common/view/AreaBuilderControlPanel.js'; +import BoardDisplayModePanel from './BoardDisplayModePanel.js'; +import ExploreNode from './ExploreNode.js'; + +// constants +const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; + +/** + * @param {AreaBuilderExploreModel} model + * @constructor + */ +function AreaBuilderExploreView( model ) { + + ScreenView.call( this, { layoutBounds: AreaBuilderSharedConstants.LAYOUT_BOUNDS } ); + + // Create the layers where the shapes will be placed. The shapes are maintained in separate layers so that they + // are over all of the shape placement boards in the z-order. + const movableShapesLayer = new Node( { layerSplit: true } ); // Force the moving shape into a separate layer for improved performance. + const singleBoardShapesLayer = new Node(); + movableShapesLayer.addChild( singleBoardShapesLayer ); + const dualBoardShapesLayer = new Node(); + movableShapesLayer.addChild( dualBoardShapesLayer ); + + // Create the composite nodes that contain the shape placement board, the readout, the bucket, the shape creator + // nodes, and the eraser button. + const centerExploreNode = new ExploreNode( model.singleShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), + model.movableShapes, model.singleModeBucket, { + shapesLayer: singleBoardShapesLayer, + shapeDragBounds: this.layoutBounds + } ); + this.addChild( centerExploreNode ); + const leftExploreNode = new ExploreNode( model.leftShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), + model.movableShapes, model.leftBucket, { shapesLayer: dualBoardShapesLayer, shapeDragBounds: this.layoutBounds } ); + this.addChild( leftExploreNode ); + const rightExploreNode = new ExploreNode( model.rightShapePlacementBoard, model.addUserCreatedMovableShape.bind( model ), + model.movableShapes, model.rightBucket, { shapesLayer: dualBoardShapesLayer, shapeDragBounds: this.layoutBounds } ); + this.addChild( rightExploreNode ); + + // Control which board(s), bucket(s), and shapes are visible. + model.boardDisplayModeProperty.link( function( boardDisplayMode ) { + centerExploreNode.visible = boardDisplayMode === 'single'; + singleBoardShapesLayer.pickable = boardDisplayMode === 'single'; + leftExploreNode.visible = boardDisplayMode === 'dual'; + rightExploreNode.visible = boardDisplayMode === 'dual'; + dualBoardShapesLayer.pickable = boardDisplayMode === 'dual'; + } ); + + // Create and add the panel that contains the ABSwitch. + const switchPanel = new BoardDisplayModePanel( model.boardDisplayModeProperty ); + this.addChild( switchPanel ); + + // Create and add the common control panel. + const controlPanel = new AreaBuilderControlPanel( model.showShapeBoardGridsProperty, model.showDimensionsProperty ); + this.addChild( controlPanel ); + + // Add the reset button. + this.addChild( new ResetAllButton( { + radius: AreaBuilderSharedConstants.RESET_BUTTON_RADIUS, + right: this.layoutBounds.width - AreaBuilderSharedConstants.CONTROLS_INSET, + bottom: this.layoutBounds.height - AreaBuilderSharedConstants.CONTROLS_INSET, + listener: function() { + centerExploreNode.reset(); + leftExploreNode.reset(); + rightExploreNode.reset(); + model.reset(); } + } ) ); + + // Add the layers where the movable shapes reside. + this.addChild( movableShapesLayer ); + + // Perform final layout adjustments + const centerBoardBounds = model.singleShapePlacementBoard.bounds; + controlPanel.top = centerBoardBounds.maxY + SPACE_AROUND_SHAPE_PLACEMENT_BOARD; + controlPanel.left = centerBoardBounds.minX; + switchPanel.top = centerBoardBounds.maxY + SPACE_AROUND_SHAPE_PLACEMENT_BOARD; + switchPanel.right = centerBoardBounds.maxX; + + // If the appropriate query parameter is set, fill the boards. This is useful for debugging. + if ( AreaBuilderQueryParameters.prefillBoards ) { + model.fillBoards(); } +} - areaBuilder.register( 'AreaBuilderExploreView', AreaBuilderExploreView ); +areaBuilder.register( 'AreaBuilderExploreView', AreaBuilderExploreView ); - return inherit( ScreenView, AreaBuilderExploreView ); -} ); \ No newline at end of file +inherit( ScreenView, AreaBuilderExploreView ); +export default AreaBuilderExploreView; \ No newline at end of file diff --git a/js/explore/view/BoardDisplayModePanel.js b/js/explore/view/BoardDisplayModePanel.js index 4e9d764..0a78609 100644 --- a/js/explore/view/BoardDisplayModePanel.js +++ b/js/explore/view/BoardDisplayModePanel.js @@ -3,85 +3,82 @@ /** * Panel that contains a switch that is used to switch between the two exploration modes. */ -define( require => { - 'use strict'; - // modules - const ABSwitch = require( 'SUN/ABSwitch' ); - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Color = require( 'SCENERY/util/Color' ); - const Dimension2 = require( 'DOT/Dimension2' ); - const HBox = require( 'SCENERY/nodes/HBox' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Panel = require( 'SUN/Panel' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); - const VBox = require( 'SCENERY/nodes/VBox' ); - const Vector2 = require( 'DOT/Vector2' ); +import Dimension2 from '../../../../dot/js/Dimension2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import HBox from '../../../../scenery/js/nodes/HBox.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import VBox from '../../../../scenery/js/nodes/VBox.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import ABSwitch from '../../../../sun/js/ABSwitch.js'; +import Panel from '../../../../sun/js/Panel.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; - // utility function for creating the icons used on this panel - function createIcon( color, rectangleLength, rectanglePositions ) { - const edgeColor = Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); - const content = new Node(); - rectanglePositions.forEach( function( position ) { - content.addChild( new Rectangle( 0, 0, rectangleLength, rectangleLength, 0, 0, { - fill: color, - stroke: edgeColor, - left: position.x * rectangleLength, - top: position.y * rectangleLength - } ) ); - } ); - return new Panel( content, { fill: 'white', stroke: 'black', cornerRadius: 0, backgroundPickable: true } ); - } +// utility function for creating the icons used on this panel +function createIcon( color, rectangleLength, rectanglePositions ) { + const edgeColor = Color.toColor( color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); + const content = new Node(); + rectanglePositions.forEach( function( position ) { + content.addChild( new Rectangle( 0, 0, rectangleLength, rectangleLength, 0, 0, { + fill: color, + stroke: edgeColor, + left: position.x * rectangleLength, + top: position.y * rectangleLength + } ) ); + } ); + return new Panel( content, { fill: 'white', stroke: 'black', cornerRadius: 0, backgroundPickable: true } ); +} - /** - * - * @constructor - */ - function BoardDisplayModePanel( boardDisplayModeProperty ) { +/** + * + * @constructor + */ +function BoardDisplayModePanel( boardDisplayModeProperty ) { - const singleBoardIcon = createIcon( AreaBuilderSharedConstants.ORANGISH_COLOR, 6, [ - new Vector2( 0, 1 ), - new Vector2( 1, 0 ), - new Vector2( 1, 1 ) - ] ); + const singleBoardIcon = createIcon( AreaBuilderSharedConstants.ORANGISH_COLOR, 6, [ + new Vector2( 0, 1 ), + new Vector2( 1, 0 ), + new Vector2( 1, 1 ) + ] ); - const dualBoardIcon = new HBox( { - children: [ - createIcon( AreaBuilderSharedConstants.GREENISH_COLOR, 6, [ - new Vector2( 0, 0 ), - new Vector2( 1, 0 ), - new Vector2( 1, 1 ) - ] ), - createIcon( AreaBuilderSharedConstants.PURPLISH_COLOR, 6, [ - new Vector2( 0, 0 ), - new Vector2( 0, 1 ), - new Vector2( 1, 0 ), - new Vector2( 1, 1 ) - ] ) - ], - spacing: 3 - } - ); + const dualBoardIcon = new HBox( { + children: [ + createIcon( AreaBuilderSharedConstants.GREENISH_COLOR, 6, [ + new Vector2( 0, 0 ), + new Vector2( 1, 0 ), + new Vector2( 1, 1 ) + ] ), + createIcon( AreaBuilderSharedConstants.PURPLISH_COLOR, 6, [ + new Vector2( 0, 0 ), + new Vector2( 0, 1 ), + new Vector2( 1, 0 ), + new Vector2( 1, 1 ) + ] ) + ], + spacing: 3 + } + ); - Panel.call( this, - new VBox( { - children: [ - new ABSwitch( boardDisplayModeProperty, 'single', singleBoardIcon, 'dual', dualBoardIcon, { - toggleSwitchOptions: { - size: new Dimension2( 36, 18 ), - thumbTouchAreaXDilation: 5, - thumbTouchAreaYDilation: 5 - } - } ) - ], - spacing: 10 // Empirically determined - } ), { fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, cornerRadius: 4 } - ); - } + Panel.call( this, + new VBox( { + children: [ + new ABSwitch( boardDisplayModeProperty, 'single', singleBoardIcon, 'dual', dualBoardIcon, { + toggleSwitchOptions: { + size: new Dimension2( 36, 18 ), + thumbTouchAreaXDilation: 5, + thumbTouchAreaYDilation: 5 + } + } ) + ], + spacing: 10 // Empirically determined + } ), { fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, cornerRadius: 4 } + ); +} - areaBuilder.register( 'BoardDisplayModePanel', BoardDisplayModePanel ); +areaBuilder.register( 'BoardDisplayModePanel', BoardDisplayModePanel ); - return inherit( Panel, BoardDisplayModePanel ); -} ); \ No newline at end of file +inherit( Panel, BoardDisplayModePanel ); +export default BoardDisplayModePanel; \ No newline at end of file diff --git a/js/explore/view/ExploreNode.js b/js/explore/view/ExploreNode.js index e7b3aa0..4909e3a 100644 --- a/js/explore/view/ExploreNode.js +++ b/js/explore/view/ExploreNode.js @@ -6,160 +6,156 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const AreaAndPerimeterDisplay = require( 'AREA_BUILDER/explore/view/AreaAndPerimeterDisplay' ); - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Bounds2 = require( 'DOT/Bounds2' ); - const BucketFront = require( 'SCENERY_PHET/bucket/BucketFront' ); - const BucketHole = require( 'SCENERY_PHET/bucket/BucketHole' ); - const Color = require( 'SCENERY/util/Color' ); - const EraserButton = require( 'SCENERY_PHET/buttons/EraserButton' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Shape = require( 'KITE/Shape' ); - const ShapeCreatorNode = require( 'AREA_BUILDER/common/view/ShapeCreatorNode' ); - const ShapeNode = require( 'AREA_BUILDER/common/view/ShapeNode' ); - const ShapePlacementBoardNode = require( 'AREA_BUILDER/common/view/ShapePlacementBoardNode' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; - const IDENTITY_TRANSFORM = ModelViewTransform2.createIdentity(); - const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; - const UNIT_RECTANGLE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); - const SHAPE_CREATOR_OFFSET_POSITIONS = [ - // Offsets used for initial position of shape, relative to bucket hole center. Empirically determined. - new Vector2( -20 - UNIT_SQUARE_LENGTH / 2, 0 - UNIT_SQUARE_LENGTH / 2 ), - new Vector2( -10 - UNIT_SQUARE_LENGTH / 2, -2 - UNIT_SQUARE_LENGTH / 2 ), - new Vector2( 9 - UNIT_SQUARE_LENGTH / 2, 1 - UNIT_SQUARE_LENGTH / 2 ), - new Vector2( 18 - UNIT_SQUARE_LENGTH / 2, 3 - UNIT_SQUARE_LENGTH / 2 ), - new Vector2( 3 - UNIT_SQUARE_LENGTH / 2, 5 - UNIT_SQUARE_LENGTH / 2 ) - ]; - - /** - * @param {ShapePlacementBoard} shapePlacementBoard - * @param {function} addShapeToModel - Function for adding a newly created shape to the model. - * @param {ObservableArray} movableShapeList - The array that tracks the movable shapes. - * @param {Bucket} bucket - Model of the bucket that is to be portrayed - * @param {Object} [options] - * @constructor - */ - function ExploreNode( shapePlacementBoard, addShapeToModel, movableShapeList, bucket, options ) { - - options = merge( { - - // drag bounds for the shapes that can go on the board - shapeDragBounds: Bounds2.EVERYTHING, - - // An optional layer (scenery node) on which movable shapes will be placed. Passing it in allows it to be - // created outside this node, which supports some layering which is otherwise not possible. - shapesLayer: null - - }, options ); - - // Verify that the shape placement board is set up to handle a specific color, rather than all colors, since other - // code below depends on this. - assert && assert( shapePlacementBoard.colorHandled !== '*' ); - const shapeColor = Color.toColor( shapePlacementBoard.colorHandled ); - - Node.call( this ); - - // Create the nodes that will be used to layer things visually. - const backLayer = new Node(); - this.addChild( backLayer ); - let movableShapesLayer; - if ( !options.shapesLayer ) { - movableShapesLayer = new Node( { layerSplit: true } ); // Force the moving shape into a separate layer for performance reasons. - this.addChild( movableShapesLayer ); - } - else { - // Assume that this layer was added to the scene graph elsewhere, and doesn't need to be added here. - movableShapesLayer = options.shapesLayer; - } - const bucketFrontLayer = new Node(); - this.addChild( bucketFrontLayer ); - const singleBoardControlsLayer = new Node(); - this.addChild( singleBoardControlsLayer ); - - // Add the node that represents the shape placement board. This is positioned based on this model position, and - // all other nodes (such as the bucket) are positioned relative to this. - const shapePlacementBoardNode = new ShapePlacementBoardNode( shapePlacementBoard ); - backLayer.addChild( shapePlacementBoardNode ); - - // Add the area and perimeter display - this.areaAndPerimeterDisplay = new AreaAndPerimeterDisplay( - shapePlacementBoard.areaAndPerimeterProperty, - shapeColor, - shapeColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), - { - centerX: shapePlacementBoardNode.centerX, - bottom: shapePlacementBoardNode.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD - } - ); - this.addChild( this.areaAndPerimeterDisplay ); - - // Add the bucket view elements - const bucketFront = new BucketFront( bucket, IDENTITY_TRANSFORM ); - bucketFrontLayer.addChild( bucketFront ); - const bucketHole = new BucketHole( bucket, IDENTITY_TRANSFORM ); - backLayer.addChild( bucketHole ); - - // Add the shape creator nodes. These must be added after the bucket hole for proper layering. - SHAPE_CREATOR_OFFSET_POSITIONS.forEach( function( offset ) { - backLayer.addChild( new ShapeCreatorNode( UNIT_RECTANGLE_SHAPE, shapeColor, addShapeToModel, { - left: bucketHole.centerX + offset.x, - top: bucketHole.centerY + offset.y, - shapeDragBounds: options.shapeDragBounds - } ) ); - } ); - - // Add the button that allows the board to be cleared of all shapes. - this.addChild( new EraserButton( { - right: bucketFront.right - 3, - top: bucketFront.bottom + 5, - touchAreaXDilation: 5, - touchAreaYDilation: 5, - listener: function() { shapePlacementBoard.releaseAllShapes( 'fade' ); } - } ) ); - // Handle the comings and goings of movable shapes. - movableShapeList.addItemAddedListener( function( addedShape ) { - - if ( addedShape.color.equals( shapeColor ) ) { - - // Create and add the view representation for this shape. - const shapeNode = new ShapeNode( addedShape, options.shapeDragBounds ); - movableShapesLayer.addChild( shapeNode ); - - // Move the shape to the front of this layer when grabbed by the user. - addedShape.userControlledProperty.link( function( userControlled ) { - if ( userControlled ) { - shapeNode.moveToFront(); - } - } ); - - // Add the removal listener for if and when this shape is removed from the model. - movableShapeList.addItemRemovedListener( function removalListener( removedShape ) { - if ( removedShape === addedShape ) { - movableShapesLayer.removeChild( shapeNode ); - movableShapeList.removeItemRemovedListener( removalListener ); - } - } ); - } - } ); - } +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js'; +import BucketFront from '../../../../scenery-phet/js/bucket/BucketFront.js'; +import BucketHole from '../../../../scenery-phet/js/bucket/BucketHole.js'; +import EraserButton from '../../../../scenery-phet/js/buttons/EraserButton.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import ShapeCreatorNode from '../../common/view/ShapeCreatorNode.js'; +import ShapeNode from '../../common/view/ShapeNode.js'; +import ShapePlacementBoardNode from '../../common/view/ShapePlacementBoardNode.js'; +import AreaAndPerimeterDisplay from './AreaAndPerimeterDisplay.js'; + +// constants +const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; +const IDENTITY_TRANSFORM = ModelViewTransform2.createIdentity(); +const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; +const UNIT_RECTANGLE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); +const SHAPE_CREATOR_OFFSET_POSITIONS = [ + // Offsets used for initial position of shape, relative to bucket hole center. Empirically determined. + new Vector2( -20 - UNIT_SQUARE_LENGTH / 2, 0 - UNIT_SQUARE_LENGTH / 2 ), + new Vector2( -10 - UNIT_SQUARE_LENGTH / 2, -2 - UNIT_SQUARE_LENGTH / 2 ), + new Vector2( 9 - UNIT_SQUARE_LENGTH / 2, 1 - UNIT_SQUARE_LENGTH / 2 ), + new Vector2( 18 - UNIT_SQUARE_LENGTH / 2, 3 - UNIT_SQUARE_LENGTH / 2 ), + new Vector2( 3 - UNIT_SQUARE_LENGTH / 2, 5 - UNIT_SQUARE_LENGTH / 2 ) +]; + +/** + * @param {ShapePlacementBoard} shapePlacementBoard + * @param {function} addShapeToModel - Function for adding a newly created shape to the model. + * @param {ObservableArray} movableShapeList - The array that tracks the movable shapes. + * @param {Bucket} bucket - Model of the bucket that is to be portrayed + * @param {Object} [options] + * @constructor + */ +function ExploreNode( shapePlacementBoard, addShapeToModel, movableShapeList, bucket, options ) { + + options = merge( { + + // drag bounds for the shapes that can go on the board + shapeDragBounds: Bounds2.EVERYTHING, + + // An optional layer (scenery node) on which movable shapes will be placed. Passing it in allows it to be + // created outside this node, which supports some layering which is otherwise not possible. + shapesLayer: null + + }, options ); - areaBuilder.register( 'ExploreNode', ExploreNode ); + // Verify that the shape placement board is set up to handle a specific color, rather than all colors, since other + // code below depends on this. + assert && assert( shapePlacementBoard.colorHandled !== '*' ); + const shapeColor = Color.toColor( shapePlacementBoard.colorHandled ); - return inherit( Node, ExploreNode, { - reset: function() { - this.areaAndPerimeterDisplay.reset(); + Node.call( this ); + + // Create the nodes that will be used to layer things visually. + const backLayer = new Node(); + this.addChild( backLayer ); + let movableShapesLayer; + if ( !options.shapesLayer ) { + movableShapesLayer = new Node( { layerSplit: true } ); // Force the moving shape into a separate layer for performance reasons. + this.addChild( movableShapesLayer ); + } + else { + // Assume that this layer was added to the scene graph elsewhere, and doesn't need to be added here. + movableShapesLayer = options.shapesLayer; + } + const bucketFrontLayer = new Node(); + this.addChild( bucketFrontLayer ); + const singleBoardControlsLayer = new Node(); + this.addChild( singleBoardControlsLayer ); + + // Add the node that represents the shape placement board. This is positioned based on this model position, and + // all other nodes (such as the bucket) are positioned relative to this. + const shapePlacementBoardNode = new ShapePlacementBoardNode( shapePlacementBoard ); + backLayer.addChild( shapePlacementBoardNode ); + + // Add the area and perimeter display + this.areaAndPerimeterDisplay = new AreaAndPerimeterDisplay( + shapePlacementBoard.areaAndPerimeterProperty, + shapeColor, + shapeColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ), + { + centerX: shapePlacementBoardNode.centerX, + bottom: shapePlacementBoardNode.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD + } + ); + this.addChild( this.areaAndPerimeterDisplay ); + + // Add the bucket view elements + const bucketFront = new BucketFront( bucket, IDENTITY_TRANSFORM ); + bucketFrontLayer.addChild( bucketFront ); + const bucketHole = new BucketHole( bucket, IDENTITY_TRANSFORM ); + backLayer.addChild( bucketHole ); + + // Add the shape creator nodes. These must be added after the bucket hole for proper layering. + SHAPE_CREATOR_OFFSET_POSITIONS.forEach( function( offset ) { + backLayer.addChild( new ShapeCreatorNode( UNIT_RECTANGLE_SHAPE, shapeColor, addShapeToModel, { + left: bucketHole.centerX + offset.x, + top: bucketHole.centerY + offset.y, + shapeDragBounds: options.shapeDragBounds + } ) ); + } ); + + // Add the button that allows the board to be cleared of all shapes. + this.addChild( new EraserButton( { + right: bucketFront.right - 3, + top: bucketFront.bottom + 5, + touchAreaXDilation: 5, + touchAreaYDilation: 5, + listener: function() { shapePlacementBoard.releaseAllShapes( 'fade' ); } + } ) ); + + // Handle the comings and goings of movable shapes. + movableShapeList.addItemAddedListener( function( addedShape ) { + + if ( addedShape.color.equals( shapeColor ) ) { + + // Create and add the view representation for this shape. + const shapeNode = new ShapeNode( addedShape, options.shapeDragBounds ); + movableShapesLayer.addChild( shapeNode ); + + // Move the shape to the front of this layer when grabbed by the user. + addedShape.userControlledProperty.link( function( userControlled ) { + if ( userControlled ) { + shapeNode.moveToFront(); + } + } ); + + // Add the removal listener for if and when this shape is removed from the model. + movableShapeList.addItemRemovedListener( function removalListener( removedShape ) { + if ( removedShape === addedShape ) { + movableShapesLayer.removeChild( shapeNode ); + movableShapeList.removeItemRemovedListener( removalListener ); + } + } ); } } ); -} ); +} + +areaBuilder.register( 'ExploreNode', ExploreNode ); + +export default inherit( Node, ExploreNode, { + reset: function() { + this.areaAndPerimeterDisplay.reset(); + } +} ); \ No newline at end of file diff --git a/js/game/AreaBuilderGameScreen.js b/js/game/AreaBuilderGameScreen.js index af4c2a6..669b48b 100644 --- a/js/game/AreaBuilderGameScreen.js +++ b/js/game/AreaBuilderGameScreen.js @@ -5,45 +5,41 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderChallengeFactory = require( 'AREA_BUILDER/game/model/AreaBuilderChallengeFactory' ); - const AreaBuilderGameModel = require( 'AREA_BUILDER/game/model/AreaBuilderGameModel' ); - const AreaBuilderGameView = require( 'AREA_BUILDER/game/view/AreaBuilderGameView' ); - const AreaBuilderIconFactory = require( 'AREA_BUILDER/common/view/AreaBuilderIconFactory' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Image = require( 'SCENERY/nodes/Image' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Property = require( 'AXON/Property' ); - const QuizGameModel = require( 'AREA_BUILDER/game/model/QuizGameModel' ); - const Screen = require( 'JOIST/Screen' ); - - // strings - const gameString = require( 'string!AREA_BUILDER/game' ); - - // images - const gameIcon = require( 'image!AREA_BUILDER/game-icon.png' ); - - function AreaBuilderGameScreen( tandem ) { - - const options = { - name: gameString, - backgroundColorProperty: new Property( AreaBuilderSharedConstants.BACKGROUND_COLOR ), - homeScreenIcon: new Image( gameIcon ), - navigationBarIcon: AreaBuilderIconFactory.createGameScreenNavBarIcon(), - tandem: tandem - }; - - Screen.call( this, - function() { return new QuizGameModel( new AreaBuilderChallengeFactory(), new AreaBuilderGameModel() ); }, - function( model ) { return new AreaBuilderGameView( model ); }, - options ); - } - - areaBuilder.register( 'AreaBuilderGameScreen', AreaBuilderGameScreen ); - - return inherit( Screen, AreaBuilderGameScreen ); -} ); \ No newline at end of file + +import Property from '../../../axon/js/Property.js'; +import Screen from '../../../joist/js/Screen.js'; +import inherit from '../../../phet-core/js/inherit.js'; +import Image from '../../../scenery/js/nodes/Image.js'; +import gameIcon from '../../images/game-icon_png.js'; +import areaBuilderStrings from '../area-builder-strings.js'; +import areaBuilder from '../areaBuilder.js'; +import AreaBuilderSharedConstants from '../common/AreaBuilderSharedConstants.js'; +import AreaBuilderIconFactory from '../common/view/AreaBuilderIconFactory.js'; +import AreaBuilderChallengeFactory from './model/AreaBuilderChallengeFactory.js'; +import AreaBuilderGameModel from './model/AreaBuilderGameModel.js'; +import QuizGameModel from './model/QuizGameModel.js'; +import AreaBuilderGameView from './view/AreaBuilderGameView.js'; + +const gameString = areaBuilderStrings.game; + + +function AreaBuilderGameScreen( tandem ) { + + const options = { + name: gameString, + backgroundColorProperty: new Property( AreaBuilderSharedConstants.BACKGROUND_COLOR ), + homeScreenIcon: new Image( gameIcon ), + navigationBarIcon: AreaBuilderIconFactory.createGameScreenNavBarIcon(), + tandem: tandem + }; + + Screen.call( this, + function() { return new QuizGameModel( new AreaBuilderChallengeFactory(), new AreaBuilderGameModel() ); }, + function( model ) { return new AreaBuilderGameView( model ); }, + options ); +} + +areaBuilder.register( 'AreaBuilderGameScreen', AreaBuilderGameScreen ); + +inherit( Screen, AreaBuilderGameScreen ); +export default AreaBuilderGameScreen; \ No newline at end of file diff --git a/js/game/model/AreaBuilderChallengeFactory.js b/js/game/model/AreaBuilderChallengeFactory.js index b2c37e6..37ce023 100644 --- a/js/game/model/AreaBuilderChallengeFactory.js +++ b/js/game/model/AreaBuilderChallengeFactory.js @@ -5,985 +5,981 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderGameChallenge = require( 'AREA_BUILDER/game/model/AreaBuilderGameChallenge' ); - const AreaBuilderGameModel = require( 'AREA_BUILDER/game/model/AreaBuilderGameModel' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Color = require( 'SCENERY/util/Color' ); - const Fraction = require( 'PHETCOMMON/model/Fraction' ); - const inherit = require( 'PHET_CORE/inherit' ); - const PerimeterShape = require( 'AREA_BUILDER/common/model/PerimeterShape' ); - const Shape = require( 'KITE/Shape' ); - const Utils = require( 'DOT/Utils' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; // In screen coords - - function AreaBuilderChallengeFactory() { - - const random = phet.joist.random; - - // Basic shapes used in the 'creator kits'. - const UNIT_SQUARE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) - .lineTo( 0, UNIT_SQUARE_LENGTH ) - .close(); - const HORIZONTAL_DOUBLE_SQUARE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH * 2, 0 ) - .lineTo( UNIT_SQUARE_LENGTH * 2, UNIT_SQUARE_LENGTH ) - .lineTo( 0, UNIT_SQUARE_LENGTH ) - .close(); - const VERTICAL_DOUBLE_SQUARE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH * 2 ) - .lineTo( 0, UNIT_SQUARE_LENGTH * 2 ) - .close(); - const QUAD_SQUARE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH * 2, 0 ) - .lineTo( UNIT_SQUARE_LENGTH * 2, UNIT_SQUARE_LENGTH * 2 ) - .lineTo( 0, UNIT_SQUARE_LENGTH * 2 ) - .close(); - const RIGHT_BOTTOM_TRIANGLE_SHAPE = new Shape() - .moveTo( UNIT_SQUARE_LENGTH, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) - .lineTo( 0, UNIT_SQUARE_LENGTH ) - .lineTo( UNIT_SQUARE_LENGTH, 0 ) - .close(); - const LEFT_BOTTOM_TRIANGLE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) - .lineTo( 0, UNIT_SQUARE_LENGTH ) - .lineTo( 0, 0 ) - .close(); - const RIGHT_TOP_TRIANGLE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) - .lineTo( 0, 0 ) - .close(); - const LEFT_TOP_TRIANGLE_SHAPE = new Shape() - .moveTo( 0, 0 ) - .lineTo( UNIT_SQUARE_LENGTH, 0 ) - .lineTo( 0, UNIT_SQUARE_LENGTH ) - .lineTo( 0, 0 ) - .close(); - - // Shape kit with a set of basic shapes and a default color. - const BASIC_RECTANGLES_SHAPE_KIT = [ - { - shape: UNIT_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, - { - shape: HORIZONTAL_DOUBLE_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, - { - shape: VERTICAL_DOUBLE_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, - { - shape: QUAD_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - } - ]; - const RECTANGLES_AND_TRIANGLES_SHAPE_KIT = [ - { - shape: HORIZONTAL_DOUBLE_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, - { - shape: UNIT_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, +import Utils from '../../../../dot/js/Utils.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Fraction from '../../../../phetcommon/js/model/Fraction.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import PerimeterShape from '../../common/model/PerimeterShape.js'; +import AreaBuilderGameChallenge from './AreaBuilderGameChallenge.js'; +import AreaBuilderGameModel from './AreaBuilderGameModel.js'; + +// constants +const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; // In screen coords + +function AreaBuilderChallengeFactory() { + + const random = phet.joist.random; + + // Basic shapes used in the 'creator kits'. + const UNIT_SQUARE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) + .lineTo( 0, UNIT_SQUARE_LENGTH ) + .close(); + const HORIZONTAL_DOUBLE_SQUARE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH * 2, 0 ) + .lineTo( UNIT_SQUARE_LENGTH * 2, UNIT_SQUARE_LENGTH ) + .lineTo( 0, UNIT_SQUARE_LENGTH ) + .close(); + const VERTICAL_DOUBLE_SQUARE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH * 2 ) + .lineTo( 0, UNIT_SQUARE_LENGTH * 2 ) + .close(); + const QUAD_SQUARE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH * 2, 0 ) + .lineTo( UNIT_SQUARE_LENGTH * 2, UNIT_SQUARE_LENGTH * 2 ) + .lineTo( 0, UNIT_SQUARE_LENGTH * 2 ) + .close(); + const RIGHT_BOTTOM_TRIANGLE_SHAPE = new Shape() + .moveTo( UNIT_SQUARE_LENGTH, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) + .lineTo( 0, UNIT_SQUARE_LENGTH ) + .lineTo( UNIT_SQUARE_LENGTH, 0 ) + .close(); + const LEFT_BOTTOM_TRIANGLE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) + .lineTo( 0, UNIT_SQUARE_LENGTH ) + .lineTo( 0, 0 ) + .close(); + const RIGHT_TOP_TRIANGLE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ) + .lineTo( 0, 0 ) + .close(); + const LEFT_TOP_TRIANGLE_SHAPE = new Shape() + .moveTo( 0, 0 ) + .lineTo( UNIT_SQUARE_LENGTH, 0 ) + .lineTo( 0, UNIT_SQUARE_LENGTH ) + .lineTo( 0, 0 ) + .close(); + + // Shape kit with a set of basic shapes and a default color. + const BASIC_RECTANGLES_SHAPE_KIT = [ + { + shape: UNIT_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: HORIZONTAL_DOUBLE_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: VERTICAL_DOUBLE_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: QUAD_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + } + ]; + + const RECTANGLES_AND_TRIANGLES_SHAPE_KIT = [ + { + shape: HORIZONTAL_DOUBLE_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: UNIT_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: VERTICAL_DOUBLE_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: LEFT_BOTTOM_TRIANGLE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: LEFT_TOP_TRIANGLE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: RIGHT_BOTTOM_TRIANGLE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + }, + { + shape: RIGHT_TOP_TRIANGLE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + } + ]; + + // Color chooser for selecting randomized colors for 'find the area' challenges. + const FIND_THE_AREA_COLOR_CHOOSER = { + colorList: random.shuffle( [ + new Color( AreaBuilderSharedConstants.PALE_BLUE_COLOR ), + new Color( AreaBuilderSharedConstants.PINKISH_COLOR ), + new Color( AreaBuilderSharedConstants.PURPLISH_COLOR ), + new Color( AreaBuilderSharedConstants.ORANGISH_COLOR ), + new Color( AreaBuilderSharedConstants.DARK_GREEN_COLOR ) + ] ), + index: 0, + nextColor: function() { + if ( this.index >= this.colorList.length ) { + // Time to shuffle the color list. Make sure that when we do, the color that was at the end of the previous + // list isn't at the beginning of this one, or we'll get two of the same colors in a row. + const lastColor = this.colorList[ this.colorList.length - 1 ]; + do { + this.colorList = random.shuffle( this.colorList ); + } while ( this.colorList[ 0 ] === lastColor ); + + // Reset the index. + this.index = 0; + } + return this.colorList[ this.index++ ]; + } + }; + + // Color chooser for selecting randomized colors for 'build it' style challenges. + const BUILD_IT_COLOR_CHOOSER = { + colorList: random.shuffle( [ + new Color( AreaBuilderSharedConstants.GREENISH_COLOR ), + new Color( AreaBuilderSharedConstants.PINKISH_COLOR ), + new Color( AreaBuilderSharedConstants.ORANGISH_COLOR ), + new Color( AreaBuilderSharedConstants.PALE_BLUE_COLOR ) + ] ), + index: 0, + nextColor: function() { + if ( this.index >= this.colorList.length ) { + // Time to shuffle the color list. Make sure that when we do, the color that was at the end of the previous + // list isn't at the beginning of this one, or we'll get two of the same colors in a row. + const lastColor = this.colorList[ this.colorList.length - 1 ]; + do { + this.colorList = random.shuffle( this.colorList ); + } while ( this.colorList[ 0 ] === lastColor ); + + // Reset the index. + this.index = 0; + } + return this.colorList[ this.index++ ]; + } + }; + + // Color pair chooser, used for selecting randomized colors for two tone 'build it' challenges. + const COLOR_PAIR_CHOOSER = { + colorPairList: random.shuffle( [ { - shape: VERTICAL_DOUBLE_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR + color1: AreaBuilderSharedConstants.GREENISH_COLOR, + color2: AreaBuilderSharedConstants.DARK_GREEN_COLOR }, { - shape: LEFT_BOTTOM_TRIANGLE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR + color1: AreaBuilderSharedConstants.PURPLISH_COLOR, + color2: AreaBuilderSharedConstants.DARK_PURPLE_COLOR }, { - shape: LEFT_TOP_TRIANGLE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR + color1: AreaBuilderSharedConstants.PALE_BLUE_COLOR, + color2: AreaBuilderSharedConstants.DARK_BLUE_COLOR }, { - shape: RIGHT_BOTTOM_TRIANGLE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - }, - { - shape: RIGHT_TOP_TRIANGLE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR + color1: AreaBuilderSharedConstants.PINKISH_COLOR, + color2: AreaBuilderSharedConstants.PURPLE_PINK_COLOR } - ]; + ] ), + index: 0, + nextColorPair: function() { + if ( this.index >= this.colorPairList.length ) { + // Time to shuffle the list. + const lastColorPair = this.colorPairList[ this.colorPairList.length - 1 ]; + do { + this.colorPairList = random.shuffle( this.colorPairList ); + } while ( this.colorPairList[ 0 ] === lastColorPair ); - // Color chooser for selecting randomized colors for 'find the area' challenges. - const FIND_THE_AREA_COLOR_CHOOSER = { - colorList: random.shuffle( [ - new Color( AreaBuilderSharedConstants.PALE_BLUE_COLOR ), - new Color( AreaBuilderSharedConstants.PINKISH_COLOR ), - new Color( AreaBuilderSharedConstants.PURPLISH_COLOR ), - new Color( AreaBuilderSharedConstants.ORANGISH_COLOR ), - new Color( AreaBuilderSharedConstants.DARK_GREEN_COLOR ) - ] ), - index: 0, - nextColor: function() { - if ( this.index >= this.colorList.length ) { - // Time to shuffle the color list. Make sure that when we do, the color that was at the end of the previous - // list isn't at the beginning of this one, or we'll get two of the same colors in a row. - const lastColor = this.colorList[ this.colorList.length - 1 ]; - do { - this.colorList = random.shuffle( this.colorList ); - } while ( this.colorList[ 0 ] === lastColor ); - - // Reset the index. - this.index = 0; - } - return this.colorList[ this.index++ ]; - } - }; - - // Color chooser for selecting randomized colors for 'build it' style challenges. - const BUILD_IT_COLOR_CHOOSER = { - colorList: random.shuffle( [ - new Color( AreaBuilderSharedConstants.GREENISH_COLOR ), - new Color( AreaBuilderSharedConstants.PINKISH_COLOR ), - new Color( AreaBuilderSharedConstants.ORANGISH_COLOR ), - new Color( AreaBuilderSharedConstants.PALE_BLUE_COLOR ) - ] ), - index: 0, - nextColor: function() { - if ( this.index >= this.colorList.length ) { - // Time to shuffle the color list. Make sure that when we do, the color that was at the end of the previous - // list isn't at the beginning of this one, or we'll get two of the same colors in a row. - const lastColor = this.colorList[ this.colorList.length - 1 ]; - do { - this.colorList = random.shuffle( this.colorList ); - } while ( this.colorList[ 0 ] === lastColor ); - - // Reset the index. - this.index = 0; - } - return this.colorList[ this.index++ ]; - } - }; - - // Color pair chooser, used for selecting randomized colors for two tone 'build it' challenges. - const COLOR_PAIR_CHOOSER = { - colorPairList: random.shuffle( [ - { - color1: AreaBuilderSharedConstants.GREENISH_COLOR, - color2: AreaBuilderSharedConstants.DARK_GREEN_COLOR - }, - { - color1: AreaBuilderSharedConstants.PURPLISH_COLOR, - color2: AreaBuilderSharedConstants.DARK_PURPLE_COLOR - }, - { - color1: AreaBuilderSharedConstants.PALE_BLUE_COLOR, - color2: AreaBuilderSharedConstants.DARK_BLUE_COLOR - }, - { - color1: AreaBuilderSharedConstants.PINKISH_COLOR, - color2: AreaBuilderSharedConstants.PURPLE_PINK_COLOR - } - ] ), - index: 0, - nextColorPair: function() { - if ( this.index >= this.colorPairList.length ) { - // Time to shuffle the list. - const lastColorPair = this.colorPairList[ this.colorPairList.length - 1 ]; - do { - this.colorPairList = random.shuffle( this.colorPairList ); - } while ( this.colorPairList[ 0 ] === lastColorPair ); - - // Reset the index. - this.index = 0; - } - return this.colorPairList[ this.index++ ]; + // Reset the index. + this.index = 0; } - }; + return this.colorPairList[ this.index++ ]; + } + }; - // -------------- private functions --------------------------- + // -------------- private functions --------------------------- - // Select a random element from an array - function randomElement( array ) { - return array[ Math.floor( random.nextDouble() * array.length ) ]; - } + // Select a random element from an array + function randomElement( array ) { + return array[ Math.floor( random.nextDouble() * array.length ) ]; + } - // Create a solution spec (a.k.a. an example solution) that represents a rectangle with the specified origin and size. - function createMonochromeRectangularSolutionSpec( x, y, width, height, color ) { - const solutionSpec = []; - for ( let column = 0; column < width; column++ ) { - for ( let row = 0; row < height; row++ ) { - solutionSpec.push( { - cellColumn: column + x, - cellRow: row + y, - color: color - } ); - } + // Create a solution spec (a.k.a. an example solution) that represents a rectangle with the specified origin and size. + function createMonochromeRectangularSolutionSpec( x, y, width, height, color ) { + const solutionSpec = []; + for ( let column = 0; column < width; column++ ) { + for ( let row = 0; row < height; row++ ) { + solutionSpec.push( { + cellColumn: column + x, + cellRow: row + y, + color: color + } ); } - return solutionSpec; } + return solutionSpec; + } - // Create a solution spec (a.k.a. an example solution) for a two-tone challenge - function createTwoColorRectangularSolutionSpec( x, y, width, height, color1, color2, color1proportion ) { - const solutionSpec = []; - for ( let row = 0; row < height; row++ ) { - for ( let column = 0; column < width; column++ ) { - solutionSpec.push( { - cellColumn: column + x, - cellRow: row + y, - color: ( row * width + column ) / ( width * height ) < color1proportion ? color1 : color2 - } ); - } + // Create a solution spec (a.k.a. an example solution) for a two-tone challenge + function createTwoColorRectangularSolutionSpec( x, y, width, height, color1, color2, color1proportion ) { + const solutionSpec = []; + for ( let row = 0; row < height; row++ ) { + for ( let column = 0; column < width; column++ ) { + solutionSpec.push( { + cellColumn: column + x, + cellRow: row + y, + color: ( row * width + column ) / ( width * height ) < color1proportion ? color1 : color2 + } ); } - return solutionSpec; - } - - // Function for creating a 'shape kit' of the basic shapes of the specified color. - function createBasicRectanglesShapeKit( color ) { - const kit = []; - BASIC_RECTANGLES_SHAPE_KIT.forEach( function( kitElement ) { - kit.push( { shape: kitElement.shape, color: color } ); - } ); - return kit; - } - - function createTwoToneRectangleBuildKit( color1, color2 ) { - const kit = []; - BASIC_RECTANGLES_SHAPE_KIT.forEach( function( kitElement ) { - const color1Element = { - shape: kitElement.shape, - color: color1 - }; - kit.push( color1Element ); - const color2Element = { - shape: kitElement.shape, - color: color2 - }; - kit.push( color2Element ); - } ); - return kit; - } - - function flipPerimeterPointsHorizontally( perimeterPointList ) { - const reflectedPoints = []; - let minX = Number.POSITIVE_INFINITY; - let maxX = Number.NEGATIVE_INFINITY; - perimeterPointList.forEach( function( point ) { - minX = Math.min( point.x, minX ); - maxX = Math.max( point.x, maxX ); - } ); - perimeterPointList.forEach( function( point ) { - reflectedPoints.push( new Vector2( -1 * ( point.x - minX - maxX ), point.y ) ); - } ); - return reflectedPoints; - } - - function flipPerimeterPointsVertically( perimeterPointList ) { - const reflectedPoints = []; - let minY = Number.POSITIVE_INFINITY; - let maxY = Number.NEGATIVE_INFINITY; - perimeterPointList.forEach( function( point ) { - minY = Math.min( point.y, minY ); - maxY = Math.max( point.y, maxY ); - } ); - perimeterPointList.forEach( function( point ) { - reflectedPoints.push( new Vector2( point.x, -1 * ( point.y - minY - maxY ) ) ); - } ); - return reflectedPoints; - } - - function createRectangularPerimeterShape( x, y, width, height, fillColor ) { - return new PerimeterShape( - // Exterior perimeters - [ - [ - new Vector2( x, y ), - new Vector2( x + width, y ), - new Vector2( x + width, y + height ), - new Vector2( x, y + height ) - ] - ], - - // Interior perimeters - [], - - // Unit size - UNIT_SQUARE_LENGTH, - - // color - { - fillColor: fillColor, - edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } - ); } + return solutionSpec; + } + + // Function for creating a 'shape kit' of the basic shapes of the specified color. + function createBasicRectanglesShapeKit( color ) { + const kit = []; + BASIC_RECTANGLES_SHAPE_KIT.forEach( function( kitElement ) { + kit.push( { shape: kitElement.shape, color: color } ); + } ); + return kit; + } - function createLShapedPerimeterShape( x, y, width, height, missingCorner, widthMissing, heightMissing, fillColor ) { - assert && assert( width > widthMissing && height > heightMissing, 'Invalid parameters' ); + function createTwoToneRectangleBuildKit( color1, color2 ) { + const kit = []; + BASIC_RECTANGLES_SHAPE_KIT.forEach( function( kitElement ) { + const color1Element = { + shape: kitElement.shape, + color: color1 + }; + kit.push( color1Element ); + const color2Element = { + shape: kitElement.shape, + color: color2 + }; + kit.push( color2Element ); + } ); + return kit; + } - let perimeterPoints = [ - new Vector2( x + widthMissing, y ), - new Vector2( x + width, y ), - new Vector2( x + width, y + height ), - new Vector2( x, y + height ), - new Vector2( x, y + heightMissing ), - new Vector2( x + widthMissing, y + heightMissing ) - ]; + function flipPerimeterPointsHorizontally( perimeterPointList ) { + const reflectedPoints = []; + let minX = Number.POSITIVE_INFINITY; + let maxX = Number.NEGATIVE_INFINITY; + perimeterPointList.forEach( function( point ) { + minX = Math.min( point.x, minX ); + maxX = Math.max( point.x, maxX ); + } ); + perimeterPointList.forEach( function( point ) { + reflectedPoints.push( new Vector2( -1 * ( point.x - minX - maxX ), point.y ) ); + } ); + return reflectedPoints; + } - if ( missingCorner === 'rightTop' || missingCorner === 'rightBottom' ) { - perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); - } - if ( missingCorner === 'leftBottom' || missingCorner === 'rightBottom' ) { - perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); - } + function flipPerimeterPointsVertically( perimeterPointList ) { + const reflectedPoints = []; + let minY = Number.POSITIVE_INFINITY; + let maxY = Number.NEGATIVE_INFINITY; + perimeterPointList.forEach( function( point ) { + minY = Math.min( point.y, minY ); + maxY = Math.max( point.y, maxY ); + } ); + perimeterPointList.forEach( function( point ) { + reflectedPoints.push( new Vector2( point.x, -1 * ( point.y - minY - maxY ) ) ); + } ); + return reflectedPoints; + } - return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { - fillColor: fillColor, - edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } - ); - } - - // Create a perimeter shape with a cutout in the top, bottom, left, or right side. - function createUShapedPerimeterShape( x, y, width, height, sideWithCutout, cutoutWidth, cutoutHeight, cutoutOffset, fillColor ) { - let perimeterPoints = [ new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ) ]; - - if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { - perimeterPoints[ 0 ].setXY( x, y ); - perimeterPoints[ 1 ].setXY( x + width, y ); - perimeterPoints[ 2 ].setXY( x + width, y + height ); - perimeterPoints[ 3 ].setXY( x, y + height ); - perimeterPoints[ 4 ].setXY( x, y + cutoutOffset + cutoutHeight ); - perimeterPoints[ 5 ].setXY( x + cutoutWidth, y + cutoutOffset + cutoutHeight ); - perimeterPoints[ 6 ].setXY( x + cutoutWidth, y + cutoutOffset ); - perimeterPoints[ 7 ].setXY( x, y + cutoutOffset ); - if ( sideWithCutout === 'right' ) { - perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); - } - } - else { - perimeterPoints[ 0 ].setXY( x, y ); - perimeterPoints[ 1 ].setXY( x + cutoutOffset, y ); - perimeterPoints[ 2 ].setXY( x + cutoutOffset, y + cutoutHeight ); - perimeterPoints[ 3 ].setXY( x + cutoutOffset + cutoutWidth, y + cutoutHeight ); - perimeterPoints[ 4 ].setXY( x + cutoutOffset + cutoutWidth, y ); - perimeterPoints[ 5 ].setXY( x + width, y ); - perimeterPoints[ 6 ].setXY( x + width, y + height ); - perimeterPoints[ 7 ].setXY( x, y + height ); - if ( sideWithCutout === 'bottom' ) { - perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); - } - } + function createRectangularPerimeterShape( x, y, width, height, fillColor ) { + return new PerimeterShape( + // Exterior perimeters + [ + [ + new Vector2( x, y ), + new Vector2( x + width, y ), + new Vector2( x + width, y + height ), + new Vector2( x, y + height ) + ] + ], + + // Interior perimeters + [], - return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { + // Unit size + UNIT_SQUARE_LENGTH, + + // color + { fillColor: fillColor, edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } ); - } - - function createPerimeterShapeWithHole( x, y, width, height, holeWidth, holeHeight, holeXOffset, holeYOffset, fillColor ) { - const exteriorPerimeterPoints = [ - new Vector2( x, y ), - new Vector2( x + width, y ), - new Vector2( x + width, y + height ), - new Vector2( x, y + height ) - ]; - const interiorPerimeterPoints = [ - // Have to draw hole in opposite direction for it to appear. - new Vector2( x + holeXOffset, y + holeYOffset ), - new Vector2( x + holeXOffset, y + holeYOffset + holeHeight ), - new Vector2( x + holeXOffset + holeWidth, y + holeYOffset + holeHeight ), - new Vector2( x + holeXOffset + holeWidth, y + holeYOffset ) - ]; - - return new PerimeterShape( [ exteriorPerimeterPoints ], [ interiorPerimeterPoints ], UNIT_SQUARE_LENGTH, { + } + ); + } + + function createLShapedPerimeterShape( x, y, width, height, missingCorner, widthMissing, heightMissing, fillColor ) { + assert && assert( width > widthMissing && height > heightMissing, 'Invalid parameters' ); + + let perimeterPoints = [ + new Vector2( x + widthMissing, y ), + new Vector2( x + width, y ), + new Vector2( x + width, y + height ), + new Vector2( x, y + height ), + new Vector2( x, y + heightMissing ), + new Vector2( x + widthMissing, y + heightMissing ) + ]; + + if ( missingCorner === 'rightTop' || missingCorner === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); + } + if ( missingCorner === 'leftBottom' || missingCorner === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); + } + + return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { fillColor: fillColor, edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } ); - } + } + ); + } - function createPerimeterShapeSlantedHypotenuseRightIsoscelesTriangle( x, y, edgeLength, cornerPosition, fillColor ) { - let perimeterPoints = [ new Vector2( x, y ), new Vector2( x + edgeLength, y ), new Vector2( x, y + edgeLength ) ]; - if ( cornerPosition === 'rightTop' || cornerPosition === 'rightBottom' ) { + // Create a perimeter shape with a cutout in the top, bottom, left, or right side. + function createUShapedPerimeterShape( x, y, width, height, sideWithCutout, cutoutWidth, cutoutHeight, cutoutOffset, fillColor ) { + let perimeterPoints = [ new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ), new Vector2( 0, 0 ) ]; + + if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { + perimeterPoints[ 0 ].setXY( x, y ); + perimeterPoints[ 1 ].setXY( x + width, y ); + perimeterPoints[ 2 ].setXY( x + width, y + height ); + perimeterPoints[ 3 ].setXY( x, y + height ); + perimeterPoints[ 4 ].setXY( x, y + cutoutOffset + cutoutHeight ); + perimeterPoints[ 5 ].setXY( x + cutoutWidth, y + cutoutOffset + cutoutHeight ); + perimeterPoints[ 6 ].setXY( x + cutoutWidth, y + cutoutOffset ); + perimeterPoints[ 7 ].setXY( x, y + cutoutOffset ); + if ( sideWithCutout === 'right' ) { perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); } - if ( cornerPosition === 'leftBottom' || cornerPosition === 'rightBottom' ) { + } + else { + perimeterPoints[ 0 ].setXY( x, y ); + perimeterPoints[ 1 ].setXY( x + cutoutOffset, y ); + perimeterPoints[ 2 ].setXY( x + cutoutOffset, y + cutoutHeight ); + perimeterPoints[ 3 ].setXY( x + cutoutOffset + cutoutWidth, y + cutoutHeight ); + perimeterPoints[ 4 ].setXY( x + cutoutOffset + cutoutWidth, y ); + perimeterPoints[ 5 ].setXY( x + width, y ); + perimeterPoints[ 6 ].setXY( x + width, y + height ); + perimeterPoints[ 7 ].setXY( x, y + height ); + if ( sideWithCutout === 'bottom' ) { perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); } + } - return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { - fillColor: fillColor, - edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } ); + return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { + fillColor: fillColor, + edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) + } ); + } + + function createPerimeterShapeWithHole( x, y, width, height, holeWidth, holeHeight, holeXOffset, holeYOffset, fillColor ) { + const exteriorPerimeterPoints = [ + new Vector2( x, y ), + new Vector2( x + width, y ), + new Vector2( x + width, y + height ), + new Vector2( x, y + height ) + ]; + const interiorPerimeterPoints = [ + // Have to draw hole in opposite direction for it to appear. + new Vector2( x + holeXOffset, y + holeYOffset ), + new Vector2( x + holeXOffset, y + holeYOffset + holeHeight ), + new Vector2( x + holeXOffset + holeWidth, y + holeYOffset + holeHeight ), + new Vector2( x + holeXOffset + holeWidth, y + holeYOffset ) + ]; + + return new PerimeterShape( [ exteriorPerimeterPoints ], [ interiorPerimeterPoints ], UNIT_SQUARE_LENGTH, { + fillColor: fillColor, + edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) + } ); + } + + function createPerimeterShapeSlantedHypotenuseRightIsoscelesTriangle( x, y, edgeLength, cornerPosition, fillColor ) { + let perimeterPoints = [ new Vector2( x, y ), new Vector2( x + edgeLength, y ), new Vector2( x, y + edgeLength ) ]; + if ( cornerPosition === 'rightTop' || cornerPosition === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); + } + if ( cornerPosition === 'leftBottom' || cornerPosition === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); } - function createPerimeterShapeLevelHypotenuseRightIsoscelesTriangle( x, y, hypotenuseLength, cornerPosition, fillColor ) { - let perimeterPoints; - if ( cornerPosition === 'centerTop' || cornerPosition === 'centerBottom' ) { - perimeterPoints = [ new Vector2( x, y ), new Vector2( x + hypotenuseLength, y ), - new Vector2( x + hypotenuseLength / 2, y + hypotenuseLength / 2 ) ]; - if ( cornerPosition === 'centerBottom' ) { - perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); - } - } - else { - perimeterPoints = [ new Vector2( x, y ), new Vector2( x, y + hypotenuseLength ), - new Vector2( x + hypotenuseLength / 2, y + hypotenuseLength / 2 ) ]; - if ( cornerPosition === 'centerLeft' ) { - perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); - } - } + return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { + fillColor: fillColor, + edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) + } ); + } - // Reflect as appropriate to create the specified orientation. - if ( cornerPosition === 'centerTop' || cornerPosition === 'rightBottom' ) { - perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); - } - if ( cornerPosition === 'leftBottom' || cornerPosition === 'rightBottom' ) { + function createPerimeterShapeLevelHypotenuseRightIsoscelesTriangle( x, y, hypotenuseLength, cornerPosition, fillColor ) { + let perimeterPoints; + if ( cornerPosition === 'centerTop' || cornerPosition === 'centerBottom' ) { + perimeterPoints = [ new Vector2( x, y ), new Vector2( x + hypotenuseLength, y ), + new Vector2( x + hypotenuseLength / 2, y + hypotenuseLength / 2 ) ]; + if ( cornerPosition === 'centerBottom' ) { perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); } + } + else { + perimeterPoints = [ new Vector2( x, y ), new Vector2( x, y + hypotenuseLength ), + new Vector2( x + hypotenuseLength / 2, y + hypotenuseLength / 2 ) ]; + if ( cornerPosition === 'centerLeft' ) { + perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); + } + } - return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { - fillColor: fillColor, - edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } ); + // Reflect as appropriate to create the specified orientation. + if ( cornerPosition === 'centerTop' || cornerPosition === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); + } + if ( cornerPosition === 'leftBottom' || cornerPosition === 'rightBottom' ) { + perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); } - function createShapeWithDiagonalAndMissingCorner( x, y, width, height, diagonalPosition, diagonalSquareLength, cutWidth, cutHeight, fillColor ) { - assert && assert( width - diagonalSquareLength >= cutWidth && height - diagonalSquareLength >= cutHeight, 'Invalid parameters' ); + return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { + fillColor: fillColor, + edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) + } ); + } - let perimeterPoints = []; - // Draw shape with diagonal in lower right corner, starting in upper right corner. - perimeterPoints.push( new Vector2( x + width, y ) ); - perimeterPoints.push( new Vector2( x + width, y + height - diagonalSquareLength ) ); - perimeterPoints.push( new Vector2( x + width - diagonalSquareLength, y + height ) ); - perimeterPoints.push( new Vector2( x, y + height ) ); - perimeterPoints.push( new Vector2( x, y + cutHeight ) ); - perimeterPoints.push( new Vector2( x + cutWidth, y + cutHeight ) ); - perimeterPoints.push( new Vector2( x + cutWidth, y ) ); + function createShapeWithDiagonalAndMissingCorner( x, y, width, height, diagonalPosition, diagonalSquareLength, cutWidth, cutHeight, fillColor ) { + assert && assert( width - diagonalSquareLength >= cutWidth && height - diagonalSquareLength >= cutHeight, 'Invalid parameters' ); - // Reflect shape as needed to meet the specified orientation. - if ( diagonalPosition === 'leftTop' || diagonalPosition === 'leftBottom' ) { - perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); - } - if ( diagonalPosition === 'rightTop' || diagonalPosition === 'leftTop' ) { - perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); - } + let perimeterPoints = []; + // Draw shape with diagonal in lower right corner, starting in upper right corner. + perimeterPoints.push( new Vector2( x + width, y ) ); + perimeterPoints.push( new Vector2( x + width, y + height - diagonalSquareLength ) ); + perimeterPoints.push( new Vector2( x + width - diagonalSquareLength, y + height ) ); + perimeterPoints.push( new Vector2( x, y + height ) ); + perimeterPoints.push( new Vector2( x, y + cutHeight ) ); + perimeterPoints.push( new Vector2( x + cutWidth, y + cutHeight ) ); + perimeterPoints.push( new Vector2( x + cutWidth, y ) ); - return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { - fillColor: fillColor, - edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) - } ); - } - - // Return a value that indicates whether two challenges are similar, used when generating challenges that are - // distinct enough to keep the game interesting. - function isChallengeSimilar( challenge1, challenge2 ) { - if ( challenge1.buildSpec && challenge2.buildSpec ) { - if ( challenge1.buildSpec.proportions && challenge2.buildSpec.proportions ) { - if ( challenge1.buildSpec.proportions.color1Proportion.denominator === challenge2.buildSpec.proportions.color1Proportion.denominator ) { - if ( challenge1.buildSpec.perimeter && challenge2.buildSpec.perimeter || !challenge1.buildSpec.perimeter && !challenge2.buildSpec.perimeter ) { - return true; - } - } - } - else if ( !challenge1.buildSpec.proportions && !challenge1.buildSpec.proportions ) { - if ( challenge1.buildSpec.area === challenge2.buildSpec.area ) { + // Reflect shape as needed to meet the specified orientation. + if ( diagonalPosition === 'leftTop' || diagonalPosition === 'leftBottom' ) { + perimeterPoints = flipPerimeterPointsHorizontally( perimeterPoints ); + } + if ( diagonalPosition === 'rightTop' || diagonalPosition === 'leftTop' ) { + perimeterPoints = flipPerimeterPointsVertically( perimeterPoints ); + } + + return new PerimeterShape( [ perimeterPoints ], [], UNIT_SQUARE_LENGTH, { + fillColor: fillColor, + edgeColor: fillColor.colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ) + } ); + } + + // Return a value that indicates whether two challenges are similar, used when generating challenges that are + // distinct enough to keep the game interesting. + function isChallengeSimilar( challenge1, challenge2 ) { + if ( challenge1.buildSpec && challenge2.buildSpec ) { + if ( challenge1.buildSpec.proportions && challenge2.buildSpec.proportions ) { + if ( challenge1.buildSpec.proportions.color1Proportion.denominator === challenge2.buildSpec.proportions.color1Proportion.denominator ) { + if ( challenge1.buildSpec.perimeter && challenge2.buildSpec.perimeter || !challenge1.buildSpec.perimeter && !challenge2.buildSpec.perimeter ) { return true; } } } - else { - if ( challenge1.backgroundShape && challenge2.backgroundShape ) { - if ( challenge1.backgroundShape.unitArea === challenge2.backgroundShape.unitArea ) { - return true; - } + else if ( !challenge1.buildSpec.proportions && !challenge1.buildSpec.proportions ) { + if ( challenge1.buildSpec.area === challenge2.buildSpec.area ) { + return true; } } - - // If we got to here, the challenges are not similar. - return false; } - - // Test the challenge against the history of recently generated challenges to see if it is unique. - function isChallengeUnique( challenge ) { - let challengeIsUnique = true; - for ( let i = 0; i < challengeHistory.length; i++ ) { - if ( isChallengeSimilar( challenge, challengeHistory[ i ] ) ) { - challengeIsUnique = false; - break; + else { + if ( challenge1.backgroundShape && challenge2.backgroundShape ) { + if ( challenge1.backgroundShape.unitArea === challenge2.backgroundShape.unitArea ) { + return true; } } - return challengeIsUnique; } - function generateBuildAreaChallenge() { + // If we got to here, the challenges are not similar. + return false; + } - // Create a unique challenge - const width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); - let height = 0; - while ( width * height < 8 || width * height > 36 ) { - height = random.nextIntBetween( 0, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - } - const color = BUILD_IT_COLOR_CHOOSER.nextColor(); - const exampleSolution = createMonochromeRectangularSolutionSpec( - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), - width, - height, - color - ); - const challenge = AreaBuilderGameChallenge.createBuildAreaChallenge( width * height, createBasicRectanglesShapeKit( color ), exampleSolution ); - return challenge; - } - - /** - * Generate a 'build it' area+perimeter challenge that consists of two connected rectangles. See the design spec - * for details. - */ - function generateTwoRectangleBuildAreaAndPerimeterChallenge() { - - // Create first rectangle dimensions - const width1 = random.nextIntBetween( 2, 6 ); - let height1; - do { - height1 = random.nextIntBetween( 1, 4 ); - } while ( width1 % 2 === height1 % 2 ); - - // Create second rectangle dimensions - let width2 = 0; - do { - width2 = random.nextIntBetween( 1, 6 ); - } while ( width1 + width2 > AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); - let height2; - do { - height2 = random.nextIntBetween( 1, 6 ); - } while ( width2 % 2 === height2 % 2 || height1 + height2 > AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - - // Choose the amount of overlap - const overlap = random.nextIntBetween( 1, Math.min( width1, width2 ) - 1 ); - - const left = Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - ( width1 + width2 - overlap ) ) / 2 ); - const top = Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - ( height1 + height2 ) ) / 2 ); - - // Create a solution spec by merging specs for each of the rectangles together. - const color = BUILD_IT_COLOR_CHOOSER.nextColor(); - const solutionSpec = createMonochromeRectangularSolutionSpec( left, top, width1, height1, color ).concat( - createMonochromeRectangularSolutionSpec( left + width1 - overlap, top + height1, width2, height2, color ) ); - - return ( AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge( width1 * height1 + width2 * height2, - 2 * width1 + 2 * height1 + 2 * width2 + 2 * height2 - 2 * overlap, createBasicRectanglesShapeKit( color ), solutionSpec ) ); - } - - function generateBuildAreaAndPerimeterChallenge() { - - let width; - let height; - - // Width can be any value from 3 to 8 excluding 7, see design doc. - do { - width = random.nextIntBetween( 3, 8 ); - } while ( width === 0 || width === 7 ); - - // Choose the height based on the total area. - do { - height = random.nextIntBetween( 3, 8 ); - } while ( width * height < 12 || width * height > 36 || height === 7 || height > AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - - const color = BUILD_IT_COLOR_CHOOSER.nextColor(); - - const exampleSolution = createMonochromeRectangularSolutionSpec( - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), - width, - height, - color - ); - return AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge( width * height, - 2 * width + 2 * height, createBasicRectanglesShapeKit( color ), exampleSolution ); - } - - function generateRectangularFindAreaChallenge() { - let width; - let height; - do { - width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); - height = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); - } while ( width * height < 16 || width * height > 36 ); - const perimeterShape = createRectangularPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); - } - - function generateLShapedFindAreaChallenge() { - let width; - let height; - do { - width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); - height = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); - } while ( width * height < 16 || width * height > 36 ); - const missingWidth = random.nextIntBetween( 1, width - 1 ); - const missingHeight = random.nextIntBetween( 1, height - 1 ); - const missingCorner = randomElement( [ 'leftTop', 'rightTop', 'leftBottom', 'rightBottom' ] ); - const perimeterShape = createLShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - missingCorner, missingWidth * UNIT_SQUARE_LENGTH, missingHeight * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); - } - - function generateUShapedFindAreaChallenge() { - let width; - let height; - do { - width = random.nextIntBetween( 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); - height = random.nextIntBetween( 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - } while ( width * height < 16 || width * height > 36 ); - const sideWithCutout = randomElement( [ 'left', 'right', 'top', 'bottom' ] ); - let cutoutWidth; - let cutoutHeight; - let cutoutOffset; - if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { - cutoutWidth = random.nextIntBetween( 2, width - 1 ); - cutoutHeight = random.nextIntBetween( 1, height - 2 ); - cutoutOffset = random.nextIntBetween( 1, height - cutoutHeight - 1 ); - } - else { - cutoutWidth = random.nextIntBetween( 1, width - 2 ); - cutoutHeight = random.nextIntBetween( 2, height - 1 ); - cutoutOffset = random.nextIntBetween( 1, width - cutoutWidth - 1 ); - } - const perimeterShape = createUShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - sideWithCutout, cutoutWidth * UNIT_SQUARE_LENGTH, cutoutHeight * UNIT_SQUARE_LENGTH, - cutoutOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); - } - - function generateOShapedFindAreaChallenge() { - let width; - let height; - do { - width = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); - height = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - } while ( width * height < 16 || width * height > 36 ); - const holeWidth = random.nextIntBetween( 1, width - 2 ); - const holeHeight = random.nextIntBetween( 1, height - 2 ); - const holeXOffset = random.nextIntBetween( 1, width - holeWidth - 1 ); - const holeYOffset = random.nextIntBetween( 1, height - holeHeight - 1 ); - const perimeterShape = createPerimeterShapeWithHole( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - holeWidth * UNIT_SQUARE_LENGTH, holeHeight * UNIT_SQUARE_LENGTH, holeXOffset * UNIT_SQUARE_LENGTH, - holeYOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); - } - - function generateIsoscelesRightTriangleSlantedHypotenuseFindAreaChallenge() { - const cornerPosition = randomElement( [ 'leftTop', 'rightTop', 'rightBottom', 'leftBottom' ] ); - let edgeLength = 0; - do { - edgeLength = random.nextIntBetween( 4, Math.min( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2, - AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ) ); - } while ( edgeLength % 2 !== 0 ); - const perimeterShape = createPerimeterShapeSlantedHypotenuseRightIsoscelesTriangle( 0, 0, - edgeLength * UNIT_SQUARE_LENGTH, cornerPosition, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); - } - - function generateIsoscelesRightTriangleLevelHypotenuseFindAreaChallenge() { - const cornerPosition = randomElement( [ 'centerTop', 'rightCenter', 'centerBottom', 'leftCenter' ] ); - let hypotenuseLength = 0; - let maxHypotenuse; - if ( cornerPosition === 'centerTop' || cornerPosition === 'centerBottom' ) { - maxHypotenuse = AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4; - } - else { - maxHypotenuse = AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2; - } - do { - hypotenuseLength = random.nextIntBetween( 2, maxHypotenuse ); - } while ( hypotenuseLength % 2 !== 0 ); - const perimeterShape = createPerimeterShapeLevelHypotenuseRightIsoscelesTriangle( 0, 0, - hypotenuseLength * UNIT_SQUARE_LENGTH, cornerPosition, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); - } - - function generateLargeRectWithChipMissingChallenge() { - const width = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); - const height = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - const sideWithCutout = randomElement( [ 'left', 'right', 'top', 'bottom' ] ); - let cutoutWidth; - let cutoutHeight; - let cutoutOffset; - if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { - cutoutWidth = 1; - cutoutHeight = random.nextIntBetween( 1, 3 ); - cutoutOffset = random.nextIntBetween( 1, height - cutoutHeight - 1 ); - } - else { - cutoutWidth = random.nextIntBetween( 1, 3 ); - cutoutHeight = 1; - cutoutOffset = random.nextIntBetween( 1, width - cutoutWidth - 1 ); + // Test the challenge against the history of recently generated challenges to see if it is unique. + function isChallengeUnique( challenge ) { + let challengeIsUnique = true; + for ( let i = 0; i < challengeHistory.length; i++ ) { + if ( isChallengeSimilar( challenge, challengeHistory[ i ] ) ) { + challengeIsUnique = false; + break; } - const perimeterShape = createUShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - sideWithCutout, cutoutWidth * UNIT_SQUARE_LENGTH, cutoutHeight * UNIT_SQUARE_LENGTH, - cutoutOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); } + return challengeIsUnique; + } - function generateLargeRectWithSmallHoleMissingChallenge() { - const width = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); - const height = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - let holeWidth; - let holeHeight; - if ( random.nextDouble() < 0.5 ) { - holeWidth = random.nextIntBetween( 1, 3 ); - holeHeight = 1; - } - else { - holeHeight = random.nextIntBetween( 1, 3 ); - holeWidth = 1; - } - const holeXOffset = random.nextIntBetween( 1, width - holeWidth - 1 ); - const holeYOffset = random.nextIntBetween( 1, height - holeHeight - 1 ); - const perimeterShape = createPerimeterShapeWithHole( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, - holeWidth * UNIT_SQUARE_LENGTH, holeHeight * UNIT_SQUARE_LENGTH, holeXOffset * UNIT_SQUARE_LENGTH, - holeYOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + function generateBuildAreaChallenge() { + + // Create a unique challenge + const width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); + let height = 0; + while ( width * height < 8 || width * height > 36 ) { + height = random.nextIntBetween( 0, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + } + const color = BUILD_IT_COLOR_CHOOSER.nextColor(); + const exampleSolution = createMonochromeRectangularSolutionSpec( + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), + width, + height, + color + ); + const challenge = AreaBuilderGameChallenge.createBuildAreaChallenge( width * height, createBasicRectanglesShapeKit( color ), exampleSolution ); + return challenge; + } - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); - } + /** + * Generate a 'build it' area+perimeter challenge that consists of two connected rectangles. See the design spec + * for details. + */ + function generateTwoRectangleBuildAreaAndPerimeterChallenge() { + + // Create first rectangle dimensions + const width1 = random.nextIntBetween( 2, 6 ); + let height1; + do { + height1 = random.nextIntBetween( 1, 4 ); + } while ( width1 % 2 === height1 % 2 ); + + // Create second rectangle dimensions + let width2 = 0; + do { + width2 = random.nextIntBetween( 1, 6 ); + } while ( width1 + width2 > AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); + let height2; + do { + height2 = random.nextIntBetween( 1, 6 ); + } while ( width2 % 2 === height2 % 2 || height1 + height2 > AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + + // Choose the amount of overlap + const overlap = random.nextIntBetween( 1, Math.min( width1, width2 ) - 1 ); + + const left = Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - ( width1 + width2 - overlap ) ) / 2 ); + const top = Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - ( height1 + height2 ) ) / 2 ); + + // Create a solution spec by merging specs for each of the rectangles together. + const color = BUILD_IT_COLOR_CHOOSER.nextColor(); + const solutionSpec = createMonochromeRectangularSolutionSpec( left, top, width1, height1, color ).concat( + createMonochromeRectangularSolutionSpec( left + width1 - overlap, top + height1, width2, height2, color ) ); + + return ( AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge( width1 * height1 + width2 * height2, + 2 * width1 + 2 * height1 + 2 * width2 + 2 * height2 - 2 * overlap, createBasicRectanglesShapeKit( color ), solutionSpec ) ); + } - function generateLargeRectWithPieceMissingChallenge() { - return random.nextDouble() < 0.7 ? generateLargeRectWithChipMissingChallenge() : generateLargeRectWithSmallHoleMissingChallenge(); - } + function generateBuildAreaAndPerimeterChallenge() { - function generateShapeWithDiagonalFindAreaChallenge() { - const width = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); - const height = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); - const diagonalPosition = randomElement( [ 'leftTop', 'rightTop', 'leftBottom', 'rightBottom' ] ); - let diagonalSquareLength = 2; - if ( height > 4 && width > 4 && random.nextDouble() > 0.5 ) { - diagonalSquareLength = 4; - } - const cutWidth = random.nextIntBetween( 1, width - diagonalSquareLength ); - const cutHeight = random.nextIntBetween( 1, height - diagonalSquareLength ); + let width; + let height; - const perimeterShape = createShapeWithDiagonalAndMissingCorner( 0, 0, width * UNIT_SQUARE_LENGTH, - height * UNIT_SQUARE_LENGTH, diagonalPosition, diagonalSquareLength * UNIT_SQUARE_LENGTH, - cutWidth * UNIT_SQUARE_LENGTH, cutHeight * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + // Width can be any value from 3 to 8 excluding 7, see design doc. + do { + width = random.nextIntBetween( 3, 8 ); + } while ( width === 0 || width === 7 ); - return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); - } + // Choose the height based on the total area. + do { + height = random.nextIntBetween( 3, 8 ); + } while ( width * height < 12 || width * height > 36 || height === 7 || height > AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); - function generateEasyProportionalBuildAreaChallenge() { - return generateProportionalBuildAreaChallenge( 'easy', false ); - } + const color = BUILD_IT_COLOR_CHOOSER.nextColor(); - function generateHarderProportionalBuildAreaChallenge() { - return generateProportionalBuildAreaChallenge( 'harder', false ); + const exampleSolution = createMonochromeRectangularSolutionSpec( + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), + width, + height, + color + ); + return AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge( width * height, + 2 * width + 2 * height, createBasicRectanglesShapeKit( color ), exampleSolution ); + } + + function generateRectangularFindAreaChallenge() { + let width; + let height; + do { + width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); + height = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); + } while ( width * height < 16 || width * height > 36 ); + const perimeterShape = createRectangularPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateLShapedFindAreaChallenge() { + let width; + let height; + do { + width = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); + height = random.nextIntBetween( 2, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); + } while ( width * height < 16 || width * height > 36 ); + const missingWidth = random.nextIntBetween( 1, width - 1 ); + const missingHeight = random.nextIntBetween( 1, height - 1 ); + const missingCorner = randomElement( [ 'leftTop', 'rightTop', 'leftBottom', 'rightBottom' ] ); + const perimeterShape = createLShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + missingCorner, missingWidth * UNIT_SQUARE_LENGTH, missingHeight * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateUShapedFindAreaChallenge() { + let width; + let height; + do { + width = random.nextIntBetween( 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); + height = random.nextIntBetween( 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + } while ( width * height < 16 || width * height > 36 ); + const sideWithCutout = randomElement( [ 'left', 'right', 'top', 'bottom' ] ); + let cutoutWidth; + let cutoutHeight; + let cutoutOffset; + if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { + cutoutWidth = random.nextIntBetween( 2, width - 1 ); + cutoutHeight = random.nextIntBetween( 1, height - 2 ); + cutoutOffset = random.nextIntBetween( 1, height - cutoutHeight - 1 ); + } + else { + cutoutWidth = random.nextIntBetween( 1, width - 2 ); + cutoutHeight = random.nextIntBetween( 2, height - 1 ); + cutoutOffset = random.nextIntBetween( 1, width - cutoutWidth - 1 ); + } + const perimeterShape = createUShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + sideWithCutout, cutoutWidth * UNIT_SQUARE_LENGTH, cutoutHeight * UNIT_SQUARE_LENGTH, + cutoutOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateOShapedFindAreaChallenge() { + let width; + let height; + do { + width = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); + height = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + } while ( width * height < 16 || width * height > 36 ); + const holeWidth = random.nextIntBetween( 1, width - 2 ); + const holeHeight = random.nextIntBetween( 1, height - 2 ); + const holeXOffset = random.nextIntBetween( 1, width - holeWidth - 1 ); + const holeYOffset = random.nextIntBetween( 1, height - holeHeight - 1 ); + const perimeterShape = createPerimeterShapeWithHole( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + holeWidth * UNIT_SQUARE_LENGTH, holeHeight * UNIT_SQUARE_LENGTH, holeXOffset * UNIT_SQUARE_LENGTH, + holeYOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateIsoscelesRightTriangleSlantedHypotenuseFindAreaChallenge() { + const cornerPosition = randomElement( [ 'leftTop', 'rightTop', 'rightBottom', 'leftBottom' ] ); + let edgeLength = 0; + do { + edgeLength = random.nextIntBetween( 4, Math.min( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2, + AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ) ); + } while ( edgeLength % 2 !== 0 ); + const perimeterShape = createPerimeterShapeSlantedHypotenuseRightIsoscelesTriangle( 0, 0, + edgeLength * UNIT_SQUARE_LENGTH, cornerPosition, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); + } + + function generateIsoscelesRightTriangleLevelHypotenuseFindAreaChallenge() { + const cornerPosition = randomElement( [ 'centerTop', 'rightCenter', 'centerBottom', 'leftCenter' ] ); + let hypotenuseLength = 0; + let maxHypotenuse; + if ( cornerPosition === 'centerTop' || cornerPosition === 'centerBottom' ) { + maxHypotenuse = AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4; + } + else { + maxHypotenuse = AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2; + } + do { + hypotenuseLength = random.nextIntBetween( 2, maxHypotenuse ); + } while ( hypotenuseLength % 2 !== 0 ); + const perimeterShape = createPerimeterShapeLevelHypotenuseRightIsoscelesTriangle( 0, 0, + hypotenuseLength * UNIT_SQUARE_LENGTH, cornerPosition, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); + } + + function generateLargeRectWithChipMissingChallenge() { + const width = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); + const height = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + const sideWithCutout = randomElement( [ 'left', 'right', 'top', 'bottom' ] ); + let cutoutWidth; + let cutoutHeight; + let cutoutOffset; + if ( sideWithCutout === 'left' || sideWithCutout === 'right' ) { + cutoutWidth = 1; + cutoutHeight = random.nextIntBetween( 1, 3 ); + cutoutOffset = random.nextIntBetween( 1, height - cutoutHeight - 1 ); + } + else { + cutoutWidth = random.nextIntBetween( 1, 3 ); + cutoutHeight = 1; + cutoutOffset = random.nextIntBetween( 1, width - cutoutWidth - 1 ); + } + const perimeterShape = createUShapedPerimeterShape( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + sideWithCutout, cutoutWidth * UNIT_SQUARE_LENGTH, cutoutHeight * UNIT_SQUARE_LENGTH, + cutoutOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateLargeRectWithSmallHoleMissingChallenge() { + const width = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 2 ); + const height = random.nextIntBetween( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 2 ); + let holeWidth; + let holeHeight; + if ( random.nextDouble() < 0.5 ) { + holeWidth = random.nextIntBetween( 1, 3 ); + holeHeight = 1; + } + else { + holeHeight = random.nextIntBetween( 1, 3 ); + holeWidth = 1; + } + const holeXOffset = random.nextIntBetween( 1, width - holeWidth - 1 ); + const holeYOffset = random.nextIntBetween( 1, height - holeHeight - 1 ); + const perimeterShape = createPerimeterShapeWithHole( 0, 0, width * UNIT_SQUARE_LENGTH, height * UNIT_SQUARE_LENGTH, + holeWidth * UNIT_SQUARE_LENGTH, holeHeight * UNIT_SQUARE_LENGTH, holeXOffset * UNIT_SQUARE_LENGTH, + holeYOffset * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); + + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, BASIC_RECTANGLES_SHAPE_KIT ); + } + + function generateLargeRectWithPieceMissingChallenge() { + return random.nextDouble() < 0.7 ? generateLargeRectWithChipMissingChallenge() : generateLargeRectWithSmallHoleMissingChallenge(); + } + + function generateShapeWithDiagonalFindAreaChallenge() { + const width = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - 4 ); + const height = random.nextIntBetween( 3, AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - 4 ); + const diagonalPosition = randomElement( [ 'leftTop', 'rightTop', 'leftBottom', 'rightBottom' ] ); + let diagonalSquareLength = 2; + if ( height > 4 && width > 4 && random.nextDouble() > 0.5 ) { + diagonalSquareLength = 4; } + const cutWidth = random.nextIntBetween( 1, width - diagonalSquareLength ); + const cutHeight = random.nextIntBetween( 1, height - diagonalSquareLength ); - function generateProportionalBuildAreaChallenge( difficulty, includePerimeter ) { - assert && assert( difficulty === 'easy' || difficulty === 'harder' ); - let width; - let height; + const perimeterShape = createShapeWithDiagonalAndMissingCorner( 0, 0, width * UNIT_SQUARE_LENGTH, + height * UNIT_SQUARE_LENGTH, diagonalPosition, diagonalSquareLength * UNIT_SQUARE_LENGTH, + cutWidth * UNIT_SQUARE_LENGTH, cutHeight * UNIT_SQUARE_LENGTH, FIND_THE_AREA_COLOR_CHOOSER.nextColor() ); - // Randomly generate width, height, and the possible factors from which a proportional challenge can be created. - const factors = []; - do { - height = random.nextIntBetween( 3, 6 ); - if ( height === 3 ) { - width = random.nextIntBetween( 4, 8 ); - } - else { - width = random.nextIntBetween( 2, 10 ); - } + return AreaBuilderGameChallenge.createFindAreaChallenge( perimeterShape, RECTANGLES_AND_TRIANGLES_SHAPE_KIT ); + } - const minFactor = difficulty === 'easy' ? 2 : 5; - const maxFactor = difficulty === 'easy' ? 4 : 9; + function generateEasyProportionalBuildAreaChallenge() { + return generateProportionalBuildAreaChallenge( 'easy', false ); + } - const area = width * height; - for ( let i = minFactor; i <= maxFactor; i++ ) { - if ( area % i === 0 ) { - // This is a factor of the area. - factors.push( i ); - } - } - } while ( factors.length === 0 ); - - // Choose the fractional proportion. - const fractionDenominator = randomElement( factors ); - let color1FractionNumerator; - do { - color1FractionNumerator = random.nextIntBetween( 1, fractionDenominator - 1 ); - } while ( Utils.gcd( color1FractionNumerator, fractionDenominator ) > 1 ); - const color1Fraction = new Fraction( color1FractionNumerator, fractionDenominator ); - - // Choose the colors for this challenge - const colorPair = COLOR_PAIR_CHOOSER.nextColorPair(); - - // Create the example solution - const exampleSolution = createTwoColorRectangularSolutionSpec( - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), - Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), - width, - height, - colorPair.color1, - colorPair.color2, - color1Fraction.getValue() - ); - - const userShapes = createTwoToneRectangleBuildKit( colorPair.color1, colorPair.color2 ); - - // Build the challenge from all the pieces. - if ( includePerimeter ) { - return AreaBuilderGameChallenge.createTwoToneBuildAreaAndPerimeterChallenge( width * height, - ( 2 * width + 2 * height ), colorPair.color1, colorPair.color2, color1Fraction, userShapes, exampleSolution ); + function generateHarderProportionalBuildAreaChallenge() { + return generateProportionalBuildAreaChallenge( 'harder', false ); + } + + function generateProportionalBuildAreaChallenge( difficulty, includePerimeter ) { + assert && assert( difficulty === 'easy' || difficulty === 'harder' ); + let width; + let height; + + // Randomly generate width, height, and the possible factors from which a proportional challenge can be created. + const factors = []; + do { + height = random.nextIntBetween( 3, 6 ); + if ( height === 3 ) { + width = random.nextIntBetween( 4, 8 ); } else { - return AreaBuilderGameChallenge.createTwoToneBuildAreaChallenge( width * height, colorPair.color1, - colorPair.color2, color1Fraction, userShapes, exampleSolution ); + width = random.nextIntBetween( 2, 10 ); } - } - function generateEasyProportionalBuildAreaAndPerimeterChallenge() { - return generateProportionalBuildAreaChallenge( 'easy', true ); - } + const minFactor = difficulty === 'easy' ? 2 : 5; + const maxFactor = difficulty === 'easy' ? 4 : 9; - function generateHarderProportionalBuildAreaAndPerimeterChallenge() { - return generateProportionalBuildAreaChallenge( 'harder', true ); + const area = width * height; + for ( let i = minFactor; i <= maxFactor; i++ ) { + if ( area % i === 0 ) { + // This is a factor of the area. + factors.push( i ); + } + } + } while ( factors.length === 0 ); + + // Choose the fractional proportion. + const fractionDenominator = randomElement( factors ); + let color1FractionNumerator; + do { + color1FractionNumerator = random.nextIntBetween( 1, fractionDenominator - 1 ); + } while ( Utils.gcd( color1FractionNumerator, fractionDenominator ) > 1 ); + const color1Fraction = new Fraction( color1FractionNumerator, fractionDenominator ); + + // Choose the colors for this challenge + const colorPair = COLOR_PAIR_CHOOSER.nextColorPair(); + + // Create the example solution + const exampleSolution = createTwoColorRectangularSolutionSpec( + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_WIDTH - width ) / 2 ), + Math.floor( ( AreaBuilderGameModel.SHAPE_BOARD_UNIT_HEIGHT - height ) / 2 ), + width, + height, + colorPair.color1, + colorPair.color2, + color1Fraction.getValue() + ); + + const userShapes = createTwoToneRectangleBuildKit( colorPair.color1, colorPair.color2 ); + + // Build the challenge from all the pieces. + if ( includePerimeter ) { + return AreaBuilderGameChallenge.createTwoToneBuildAreaAndPerimeterChallenge( width * height, + ( 2 * width + 2 * height ), colorPair.color1, colorPair.color2, color1Fraction, userShapes, exampleSolution ); + } + else { + return AreaBuilderGameChallenge.createTwoToneBuildAreaChallenge( width * height, colorPair.color1, + colorPair.color2, color1Fraction, userShapes, exampleSolution ); } + } - // Challenge history, used to make sure unique challenges are generated. - var challengeHistory = []; + function generateEasyProportionalBuildAreaAndPerimeterChallenge() { + return generateProportionalBuildAreaChallenge( 'easy', true ); + } - // Use the provided generation function to create challenges until a unique one has been created. - function generateUniqueChallenge( generationFunction ) { - let challenge; - let uniqueChallengeGenerated = false; - let attempts = 0; - while ( !uniqueChallengeGenerated ) { - challenge = generationFunction(); - attempts++; - uniqueChallengeGenerated = isChallengeUnique( challenge ); - if ( attempts > 12 && !uniqueChallengeGenerated ) { - // Remove the oldest half of challenges. - challengeHistory = challengeHistory.slice( 0, challengeHistory.length / 2 ); - attempts = 0; - } - } + function generateHarderProportionalBuildAreaAndPerimeterChallenge() { + return generateProportionalBuildAreaChallenge( 'harder', true ); + } - challengeHistory.push( challenge ); - return challenge; - } + // Challenge history, used to make sure unique challenges are generated. + var challengeHistory = []; - // Level 4 is required to limit the number of shapes available, to only allow unit squares, and to have not grid - // control. This function modifies the challenges to conform to this. - function makeLevel4SpecificModifications( challenge ) { - challenge.toolSpec.gridControl = false; - challenge.userShapes = [ - { - shape: UNIT_SQUARE_SHAPE, - color: AreaBuilderSharedConstants.GREENISH_COLOR - } - ]; - - // Limit the number of shapes to the length of the larger side. This encourages certain strategies. - assert && assert( challenge.backgroundShape.exteriorPerimeters.length === 1, 'Unexpected configuration for background shape.' ); - const perimeterShape = new PerimeterShape( challenge.backgroundShape.exteriorPerimeters, [], UNIT_SQUARE_LENGTH ); - challenge.userShapes[ 0 ].creationLimit = Math.max( perimeterShape.getWidth() / UNIT_SQUARE_LENGTH, - perimeterShape.getHeight() / UNIT_SQUARE_LENGTH ); - return challenge; - } - - /** - * Generate a set of challenges for the given game level. - * - * @public - * @param level - * @param numChallenges - * @returns {Array} - */ - this.generateChallengeSet = function( level, numChallenges ) { - let challengeSet = []; - let tempChallenge; - switch( level ) { - case 0: - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateBuildAreaChallenge ) ); } ); - _.times( 2, function() { challengeSet.push( generateUniqueChallenge( generateRectangularFindAreaChallenge ) ); } ); - challengeSet.push( generateUniqueChallenge( generateLShapedFindAreaChallenge ) ); - break; - - case 1: - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateBuildAreaAndPerimeterChallenge ) ); } ); - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateTwoRectangleBuildAreaAndPerimeterChallenge ) ); } ); - break; - - case 2: - challengeSet.push( generateUniqueChallenge( generateUShapedFindAreaChallenge ) ); - challengeSet.push( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ); - challengeSet.push( generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ) ); - challengeSet = random.shuffle( challengeSet ); - var triangleChallenges = random.shuffle( [ - generateUniqueChallenge( generateIsoscelesRightTriangleLevelHypotenuseFindAreaChallenge ), - generateUniqueChallenge( generateIsoscelesRightTriangleSlantedHypotenuseFindAreaChallenge ) - ] ); - triangleChallenges.forEach( function( challenge ) { challengeSet.push( challenge ); } ); - challengeSet.push( generateUniqueChallenge( generateLargeRectWithPieceMissingChallenge ) ); - break; - - case 3: - // For this level, the grid is disabled for all challenges and some different build kits are used. - challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateUShapedFindAreaChallenge ) ) ); - challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ) ); - challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ) ); - challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ) ) ); - challengeSet = random.shuffle( challengeSet ); - // For the next challenge, choose randomly from the shapes that don't have diagonals. - tempChallenge = generateUniqueChallenge( randomElement( [ generateLShapedFindAreaChallenge, generateUShapedFindAreaChallenge ] ) ); - tempChallenge.toolSpec.gridControl = false; - tempChallenge.userShapes = null; - challengeSet.push( tempChallenge ); - tempChallenge = generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ); - tempChallenge.toolSpec.gridControl = false; - tempChallenge.userShapes = null; - challengeSet.push( tempChallenge ); - break; - - case 4: - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateEasyProportionalBuildAreaChallenge ) ); } ); - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateHarderProportionalBuildAreaChallenge ) ); } ); - break; - - case 5: - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateEasyProportionalBuildAreaAndPerimeterChallenge ) ); } ); - _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateHarderProportionalBuildAreaAndPerimeterChallenge ) ); } ); - break; - - default: - throw new Error( 'Unsupported game level: ' + level ); + // Use the provided generation function to create challenges until a unique one has been created. + function generateUniqueChallenge( generationFunction ) { + let challenge; + let uniqueChallengeGenerated = false; + let attempts = 0; + while ( !uniqueChallengeGenerated ) { + challenge = generationFunction(); + attempts++; + uniqueChallengeGenerated = isChallengeUnique( challenge ); + if ( attempts > 12 && !uniqueChallengeGenerated ) { + // Remove the oldest half of challenges. + challengeHistory = challengeHistory.slice( 0, challengeHistory.length / 2 ); + attempts = 0; } - assert && assert( challengeSet.length === numChallenges, 'Error: Didn\'t generate correct number of challenges.' ); - return challengeSet; - }; + } + + challengeHistory.push( challenge ); + return challenge; } - areaBuilder.register( 'AreaBuilderChallengeFactory', AreaBuilderChallengeFactory ); + // Level 4 is required to limit the number of shapes available, to only allow unit squares, and to have not grid + // control. This function modifies the challenges to conform to this. + function makeLevel4SpecificModifications( challenge ) { + challenge.toolSpec.gridControl = false; + challenge.userShapes = [ + { + shape: UNIT_SQUARE_SHAPE, + color: AreaBuilderSharedConstants.GREENISH_COLOR + } + ]; + + // Limit the number of shapes to the length of the larger side. This encourages certain strategies. + assert && assert( challenge.backgroundShape.exteriorPerimeters.length === 1, 'Unexpected configuration for background shape.' ); + const perimeterShape = new PerimeterShape( challenge.backgroundShape.exteriorPerimeters, [], UNIT_SQUARE_LENGTH ); + challenge.userShapes[ 0 ].creationLimit = Math.max( perimeterShape.getWidth() / UNIT_SQUARE_LENGTH, + perimeterShape.getHeight() / UNIT_SQUARE_LENGTH ); + return challenge; + } - return inherit( Object, AreaBuilderChallengeFactory, {}, {} ); -} ); \ No newline at end of file + /** + * Generate a set of challenges for the given game level. + * + * @public + * @param level + * @param numChallenges + * @returns {Array} + */ + this.generateChallengeSet = function( level, numChallenges ) { + let challengeSet = []; + let tempChallenge; + switch( level ) { + case 0: + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateBuildAreaChallenge ) ); } ); + _.times( 2, function() { challengeSet.push( generateUniqueChallenge( generateRectangularFindAreaChallenge ) ); } ); + challengeSet.push( generateUniqueChallenge( generateLShapedFindAreaChallenge ) ); + break; + + case 1: + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateBuildAreaAndPerimeterChallenge ) ); } ); + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateTwoRectangleBuildAreaAndPerimeterChallenge ) ); } ); + break; + + case 2: + challengeSet.push( generateUniqueChallenge( generateUShapedFindAreaChallenge ) ); + challengeSet.push( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ); + challengeSet.push( generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ) ); + challengeSet = random.shuffle( challengeSet ); + var triangleChallenges = random.shuffle( [ + generateUniqueChallenge( generateIsoscelesRightTriangleLevelHypotenuseFindAreaChallenge ), + generateUniqueChallenge( generateIsoscelesRightTriangleSlantedHypotenuseFindAreaChallenge ) + ] ); + triangleChallenges.forEach( function( challenge ) { challengeSet.push( challenge ); } ); + challengeSet.push( generateUniqueChallenge( generateLargeRectWithPieceMissingChallenge ) ); + break; + + case 3: + // For this level, the grid is disabled for all challenges and some different build kits are used. + challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateUShapedFindAreaChallenge ) ) ); + challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ) ); + challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateOShapedFindAreaChallenge ) ) ); + challengeSet.push( makeLevel4SpecificModifications( generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ) ) ); + challengeSet = random.shuffle( challengeSet ); + // For the next challenge, choose randomly from the shapes that don't have diagonals. + tempChallenge = generateUniqueChallenge( randomElement( [ generateLShapedFindAreaChallenge, generateUShapedFindAreaChallenge ] ) ); + tempChallenge.toolSpec.gridControl = false; + tempChallenge.userShapes = null; + challengeSet.push( tempChallenge ); + tempChallenge = generateUniqueChallenge( generateShapeWithDiagonalFindAreaChallenge ); + tempChallenge.toolSpec.gridControl = false; + tempChallenge.userShapes = null; + challengeSet.push( tempChallenge ); + break; + + case 4: + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateEasyProportionalBuildAreaChallenge ) ); } ); + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateHarderProportionalBuildAreaChallenge ) ); } ); + break; + + case 5: + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateEasyProportionalBuildAreaAndPerimeterChallenge ) ); } ); + _.times( 3, function() { challengeSet.push( generateUniqueChallenge( generateHarderProportionalBuildAreaAndPerimeterChallenge ) ); } ); + break; + + default: + throw new Error( 'Unsupported game level: ' + level ); + } + assert && assert( challengeSet.length === numChallenges, 'Error: Didn\'t generate correct number of challenges.' ); + return challengeSet; + }; +} + +areaBuilder.register( 'AreaBuilderChallengeFactory', AreaBuilderChallengeFactory ); + +export default inherit( Object, AreaBuilderChallengeFactory, {}, {} ); \ No newline at end of file diff --git a/js/game/model/AreaBuilderGameChallenge.js b/js/game/model/AreaBuilderGameChallenge.js index b3de5be..5432da4 100644 --- a/js/game/model/AreaBuilderGameChallenge.js +++ b/js/game/model/AreaBuilderGameChallenge.js @@ -6,178 +6,175 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const BuildSpec = require( 'AREA_BUILDER/game/model/BuildSpec' ); - const inherit = require( 'PHET_CORE/inherit' ); - const PerimeterShape = require( 'AREA_BUILDER/common/model/PerimeterShape' ); - - /** - * @param {Object} toolSpec An object that specifies which tools are available to the user. It should have three - * boolean properties - 'gridControl', 'dimensionsControl', and 'decompositionToolControl' - that indicate whether - * the user is allowed to control these things for this challenge. - * @param {Array} userShapes An array of shape specification that describe the shapes that can be created and - * manipulated by the user for this challenge. Each shape specification is an object with a 'shape' field and a - * 'color' field. This value can be null to signify no user shapes are present for the challenge. - * @param {BuildSpec} buildSpec Object that specifies what the user should build, see BuildSpec.js file for details. - * @param {PerimeterShape} backgroundShape Shape that should appear on the board, null for challenges that don't - * require such a shape. - * @param {string} checkSpec Specifies what should be checked when the user pressed the 'Check' button. Valid values - * are 'areaEntered', 'areaConstructed', 'areaAndPerimeterConstructed', 'areaAndProportionConstructed', - * 'areaPerimeterAndProportionConstructed'. - * @param {Array} exampleBuildItSolution An example solution for a build problem. It consists of a list of - * cell positions for unit squares and a color, e.g. { cellColumn: x, cellRow: y, color: 'blue' }. This should be - * null for challenges where no example solution needs to be shown. - * @constructor - */ - function AreaBuilderGameChallenge( toolSpec, userShapes, buildSpec, backgroundShape, checkSpec, exampleBuildItSolution ) { - // Verification - assert && assert( buildSpec instanceof BuildSpec || buildSpec === null ); - assert && assert( backgroundShape instanceof PerimeterShape || backgroundShape === null ); - - // Fields, all public. - this.toolSpec = toolSpec; - this.userShapes = userShapes; - this.buildSpec = buildSpec; - this.backgroundShape = backgroundShape; - this.checkSpec = checkSpec; - this.exampleBuildItSolution = exampleBuildItSolution; - } - - AreaBuilderGameChallenge.createBuildAreaChallenge = function( areaToBuild, userShapes, exampleSolution ) { - return new AreaBuilderGameChallenge( - // toolSpec - { - gridControl: true, - dimensionsControl: true, - decompositionToolControl: true - }, - - // userShapes - userShapes, - - // buildSpec - BuildSpec.areaOnly( areaToBuild ), - - // backgroundShape - null, - - // checkSpec - 'areaConstructed', - - // exampleBuildItSolution - exampleSolution - ); - }; - - AreaBuilderGameChallenge.createTwoToneBuildAreaChallenge = function( areaToBuild, color1, color2, color1Fraction, userShapes, exampleSolution ) { - return new AreaBuilderGameChallenge( - // toolSpec - { - gridControl: true, - dimensionsControl: true, - decompositionToolControl: true - }, - - // userShapes - userShapes, - - // buildSpec - BuildSpec.areaAndProportions( areaToBuild, color1, color2, color1Fraction ), - - // backgroundShape - null, - - // checkSpec - 'areaAndProportionConstructed', - - // exampleBuildItSolution - exampleSolution - ); - }; - - AreaBuilderGameChallenge.createTwoToneBuildAreaAndPerimeterChallenge = function( areaToBuild, perimeterToBuild, color1, color2, color1Fraction, userShapes, exampleSolution ) { - return new AreaBuilderGameChallenge( - // toolSpec - { - gridControl: true, - dimensionsControl: true, - decompositionToolControl: true - }, - - // userShapes - userShapes, - - // buildSpec - BuildSpec.areaPerimeterAndProportions( areaToBuild, perimeterToBuild, color1, color2, color1Fraction ), - - // backgroundShape - null, - - // checkSpec - 'areaPerimeterAndProportionConstructed', - - // exampleBuildItSolution - exampleSolution - ); - }; - - AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge = function( areaToBuild, perimeterToBuild, userShapes, exampleSolution ) { - return new AreaBuilderGameChallenge( - // toolSpec - { - gridControl: true, - dimensionsControl: true, - decompositionToolControl: true - }, - - // userShapes - userShapes, - - // buildSpec - BuildSpec.areaAndPerimeter( areaToBuild, perimeterToBuild ), - - // backgroundShape - null, - - // checkSpec - 'areaAndPerimeterConstructed', - - // exampleBuildItSolution - exampleSolution - ); - }; - - AreaBuilderGameChallenge.createFindAreaChallenge = function( areaShape, userShapes ) { - return new AreaBuilderGameChallenge( - // toolSpec - { - gridControl: true, - dimensionsControl: true, - decompositionToolControl: true - }, - - // userShapes - userShapes, - - // buildSpec - null, - - // backgroundShape - areaShape, - - // checkSpec - 'areaEntered', - - // exampleBuildItSolution - null - ); - }; - - areaBuilder.register( 'AreaBuilderGameChallenge', AreaBuilderGameChallenge ); - return inherit( Object, AreaBuilderGameChallenge ); -} ); \ No newline at end of file +import inherit from '../../../../phet-core/js/inherit.js'; +import areaBuilder from '../../areaBuilder.js'; +import PerimeterShape from '../../common/model/PerimeterShape.js'; +import BuildSpec from './BuildSpec.js'; + +/** + * @param {Object} toolSpec An object that specifies which tools are available to the user. It should have three + * boolean properties - 'gridControl', 'dimensionsControl', and 'decompositionToolControl' - that indicate whether + * the user is allowed to control these things for this challenge. + * @param {Array} userShapes An array of shape specification that describe the shapes that can be created and + * manipulated by the user for this challenge. Each shape specification is an object with a 'shape' field and a + * 'color' field. This value can be null to signify no user shapes are present for the challenge. + * @param {BuildSpec} buildSpec Object that specifies what the user should build, see BuildSpec.js file for details. + * @param {PerimeterShape} backgroundShape Shape that should appear on the board, null for challenges that don't + * require such a shape. + * @param {string} checkSpec Specifies what should be checked when the user pressed the 'Check' button. Valid values + * are 'areaEntered', 'areaConstructed', 'areaAndPerimeterConstructed', 'areaAndProportionConstructed', + * 'areaPerimeterAndProportionConstructed'. + * @param {Array} exampleBuildItSolution An example solution for a build problem. It consists of a list of + * cell positions for unit squares and a color, e.g. { cellColumn: x, cellRow: y, color: 'blue' }. This should be + * null for challenges where no example solution needs to be shown. + * @constructor + */ +function AreaBuilderGameChallenge( toolSpec, userShapes, buildSpec, backgroundShape, checkSpec, exampleBuildItSolution ) { + // Verification + assert && assert( buildSpec instanceof BuildSpec || buildSpec === null ); + assert && assert( backgroundShape instanceof PerimeterShape || backgroundShape === null ); + + // Fields, all public. + this.toolSpec = toolSpec; + this.userShapes = userShapes; + this.buildSpec = buildSpec; + this.backgroundShape = backgroundShape; + this.checkSpec = checkSpec; + this.exampleBuildItSolution = exampleBuildItSolution; +} + +AreaBuilderGameChallenge.createBuildAreaChallenge = function( areaToBuild, userShapes, exampleSolution ) { + return new AreaBuilderGameChallenge( + // toolSpec + { + gridControl: true, + dimensionsControl: true, + decompositionToolControl: true + }, + + // userShapes + userShapes, + + // buildSpec + BuildSpec.areaOnly( areaToBuild ), + + // backgroundShape + null, + + // checkSpec + 'areaConstructed', + + // exampleBuildItSolution + exampleSolution + ); +}; + +AreaBuilderGameChallenge.createTwoToneBuildAreaChallenge = function( areaToBuild, color1, color2, color1Fraction, userShapes, exampleSolution ) { + return new AreaBuilderGameChallenge( + // toolSpec + { + gridControl: true, + dimensionsControl: true, + decompositionToolControl: true + }, + + // userShapes + userShapes, + + // buildSpec + BuildSpec.areaAndProportions( areaToBuild, color1, color2, color1Fraction ), + + // backgroundShape + null, + + // checkSpec + 'areaAndProportionConstructed', + + // exampleBuildItSolution + exampleSolution + ); +}; + +AreaBuilderGameChallenge.createTwoToneBuildAreaAndPerimeterChallenge = function( areaToBuild, perimeterToBuild, color1, color2, color1Fraction, userShapes, exampleSolution ) { + return new AreaBuilderGameChallenge( + // toolSpec + { + gridControl: true, + dimensionsControl: true, + decompositionToolControl: true + }, + + // userShapes + userShapes, + + // buildSpec + BuildSpec.areaPerimeterAndProportions( areaToBuild, perimeterToBuild, color1, color2, color1Fraction ), + + // backgroundShape + null, + + // checkSpec + 'areaPerimeterAndProportionConstructed', + + // exampleBuildItSolution + exampleSolution + ); +}; + +AreaBuilderGameChallenge.createBuildAreaAndPerimeterChallenge = function( areaToBuild, perimeterToBuild, userShapes, exampleSolution ) { + return new AreaBuilderGameChallenge( + // toolSpec + { + gridControl: true, + dimensionsControl: true, + decompositionToolControl: true + }, + + // userShapes + userShapes, + + // buildSpec + BuildSpec.areaAndPerimeter( areaToBuild, perimeterToBuild ), + + // backgroundShape + null, + + // checkSpec + 'areaAndPerimeterConstructed', + + // exampleBuildItSolution + exampleSolution + ); +}; + +AreaBuilderGameChallenge.createFindAreaChallenge = function( areaShape, userShapes ) { + return new AreaBuilderGameChallenge( + // toolSpec + { + gridControl: true, + dimensionsControl: true, + decompositionToolControl: true + }, + + // userShapes + userShapes, + + // buildSpec + null, + + // backgroundShape + areaShape, + + // checkSpec + 'areaEntered', + + // exampleBuildItSolution + null + ); +}; + +areaBuilder.register( 'AreaBuilderGameChallenge', AreaBuilderGameChallenge ); + +inherit( Object, AreaBuilderGameChallenge ); +export default AreaBuilderGameChallenge; \ No newline at end of file diff --git a/js/game/model/AreaBuilderGameModel.js b/js/game/model/AreaBuilderGameModel.js index 6da939d..fca4380 100644 --- a/js/game/model/AreaBuilderGameModel.js +++ b/js/game/model/AreaBuilderGameModel.js @@ -9,327 +9,323 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const BuildSpec = require( 'AREA_BUILDER/game/model/BuildSpec' ); - const Color = require( 'SCENERY/util/Color' ); - const Dimension2 = require( 'DOT/Dimension2' ); - const inherit = require( 'PHET_CORE/inherit' ); - const MovableShape = require( 'AREA_BUILDER/common/model/MovableShape' ); - const ObservableArray = require( 'AXON/ObservableArray' ); - const Property = require( 'AXON/Property' ); - const Shape = require( 'KITE/Shape' ); - const ShapePlacementBoard = require( 'AREA_BUILDER/common/model/ShapePlacementBoard' ); - const Vector2 = require( 'DOT/Vector2' ); - - // constants - const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; // In screen coords, which are roughly pixels - const BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 12, UNIT_SQUARE_LENGTH * 8 ); - const UNIT_SQUARE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); - - /** - * @constructor - */ - function AreaBuilderGameModel() { - - this.showGridOnBoardProperty = new Property( false ); - this.showDimensionsProperty = new Property( false ); - - // @public Value where the user's submission of area is stored. - this.areaGuess = 0; - - // @public The shape board where the user will build and/or evaluate shapes. - this.shapePlacementBoard = new ShapePlacementBoard( - BOARD_SIZE, - UNIT_SQUARE_LENGTH, - new Vector2( ( AreaBuilderSharedConstants.LAYOUT_BOUNDS.width - BOARD_SIZE.width ) * 0.55, 85 ), // Position empirically determined - '*', // Allow any color shape to be placed on the board - this.showGridOnBoardProperty, - this.showDimensionsProperty - ); - - // @public Array where shapes that are added by the user are tracked. - this.movableShapes = new ObservableArray(); - - // @private The position from which squares that animate onto the board to show a solution should emerge. The - // offset is empirically determined to be somewhere in the carousel. - this.solutionShapeOrigin = new Vector2( this.shapePlacementBoard.bounds.left + 30, this.shapePlacementBoard.bounds.maxY + 30 ); - } - areaBuilder.register( 'AreaBuilderGameModel', AreaBuilderGameModel ); - - return inherit( Object, AreaBuilderGameModel, { - - // @private - replace a composite shape with unit squares - replaceShapeWithUnitSquares: function( movableShape ) { - const self = this; - assert && assert( - movableShape.shape.bounds.width > UNIT_SQUARE_LENGTH || movableShape.shape.bounds.height > UNIT_SQUARE_LENGTH, - 'This method should not be called for non-composite shapes' - ); - - // break the shape into the constituent squares - const constituentShapes = movableShape.decomposeIntoSquares( UNIT_SQUARE_LENGTH ); - - // add the newly created squares to this model - constituentShapes.forEach( function( shape ) { self.addUserCreatedMovableShape( shape ); } ); - - // replace the shape on the shape placement board with unit squares - self.shapePlacementBoard.replaceShapeWithUnitSquares( movableShape, constituentShapes ); - - // remove the original composite shape from this model - self.movableShapes.remove( movableShape ); - }, - - /** - * Function for adding new movable shapes to this model when the user is creating them, generally by clicking on - * some sort of creator node. - * @public - * @param movableShape - */ - addUserCreatedMovableShape: function( movableShape ) { - const self = this; - this.movableShapes.push( movableShape ); - - movableShape.userControlledProperty.lazyLink( function( userControlled ) { - if ( !userControlled ) { - if ( self.shapePlacementBoard.placeShape( movableShape ) ) { - if ( movableShape.shape.bounds.width > UNIT_SQUARE_LENGTH || movableShape.shape.bounds.height > UNIT_SQUARE_LENGTH ) { - - // This is a composite shape, meaning that it is made up of more than one unit square. Rather than - // tracking these, the design team decided that they should decompose into individual unit squares once - // they have been placed. - if ( movableShape.animatingProperty.get() ) { - movableShape.animatingProperty.lazyLink( function decomposeCompositeShape( animating ) { - - if ( !animating ) { - - // unhook this function - movableShape.animatingProperty.unlink( decomposeCompositeShape ); - - // replace this composite shape with individual unit squares - if ( self.shapePlacementBoard.isResidentShape( movableShape ) ) { - self.replaceShapeWithUnitSquares( movableShape ); - } +import ObservableArray from '../../../../axon/js/ObservableArray.js'; +import Property from '../../../../axon/js/Property.js'; +import Dimension2 from '../../../../dot/js/Dimension2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import MovableShape from '../../common/model/MovableShape.js'; +import ShapePlacementBoard from '../../common/model/ShapePlacementBoard.js'; +import BuildSpec from './BuildSpec.js'; + +// constants +const UNIT_SQUARE_LENGTH = AreaBuilderSharedConstants.UNIT_SQUARE_LENGTH; // In screen coords, which are roughly pixels +const BOARD_SIZE = new Dimension2( UNIT_SQUARE_LENGTH * 12, UNIT_SQUARE_LENGTH * 8 ); +const UNIT_SQUARE_SHAPE = Shape.rect( 0, 0, UNIT_SQUARE_LENGTH, UNIT_SQUARE_LENGTH ); + +/** + * @constructor + */ +function AreaBuilderGameModel() { + + this.showGridOnBoardProperty = new Property( false ); + this.showDimensionsProperty = new Property( false ); + + // @public Value where the user's submission of area is stored. + this.areaGuess = 0; + + // @public The shape board where the user will build and/or evaluate shapes. + this.shapePlacementBoard = new ShapePlacementBoard( + BOARD_SIZE, + UNIT_SQUARE_LENGTH, + new Vector2( ( AreaBuilderSharedConstants.LAYOUT_BOUNDS.width - BOARD_SIZE.width ) * 0.55, 85 ), // Position empirically determined + '*', // Allow any color shape to be placed on the board + this.showGridOnBoardProperty, + this.showDimensionsProperty + ); + + // @public Array where shapes that are added by the user are tracked. + this.movableShapes = new ObservableArray(); + + // @private The position from which squares that animate onto the board to show a solution should emerge. The + // offset is empirically determined to be somewhere in the carousel. + this.solutionShapeOrigin = new Vector2( this.shapePlacementBoard.bounds.left + 30, this.shapePlacementBoard.bounds.maxY + 30 ); +} + +areaBuilder.register( 'AreaBuilderGameModel', AreaBuilderGameModel ); + +export default inherit( Object, AreaBuilderGameModel, { + + // @private - replace a composite shape with unit squares + replaceShapeWithUnitSquares: function( movableShape ) { + const self = this; + assert && assert( + movableShape.shape.bounds.width > UNIT_SQUARE_LENGTH || movableShape.shape.bounds.height > UNIT_SQUARE_LENGTH, + 'This method should not be called for non-composite shapes' + ); + + // break the shape into the constituent squares + const constituentShapes = movableShape.decomposeIntoSquares( UNIT_SQUARE_LENGTH ); + + // add the newly created squares to this model + constituentShapes.forEach( function( shape ) { self.addUserCreatedMovableShape( shape ); } ); + + // replace the shape on the shape placement board with unit squares + self.shapePlacementBoard.replaceShapeWithUnitSquares( movableShape, constituentShapes ); + + // remove the original composite shape from this model + self.movableShapes.remove( movableShape ); + }, + + /** + * Function for adding new movable shapes to this model when the user is creating them, generally by clicking on + * some sort of creator node. + * @public + * @param movableShape + */ + addUserCreatedMovableShape: function( movableShape ) { + const self = this; + this.movableShapes.push( movableShape ); + + movableShape.userControlledProperty.lazyLink( function( userControlled ) { + if ( !userControlled ) { + if ( self.shapePlacementBoard.placeShape( movableShape ) ) { + if ( movableShape.shape.bounds.width > UNIT_SQUARE_LENGTH || movableShape.shape.bounds.height > UNIT_SQUARE_LENGTH ) { + + // This is a composite shape, meaning that it is made up of more than one unit square. Rather than + // tracking these, the design team decided that they should decompose into individual unit squares once + // they have been placed. + if ( movableShape.animatingProperty.get() ) { + movableShape.animatingProperty.lazyLink( function decomposeCompositeShape( animating ) { + + if ( !animating ) { + + // unhook this function + movableShape.animatingProperty.unlink( decomposeCompositeShape ); + + // replace this composite shape with individual unit squares + if ( self.shapePlacementBoard.isResidentShape( movableShape ) ) { + self.replaceShapeWithUnitSquares( movableShape ); } - } ); - } - else { + } + } ); + } + else { - // decompose the shape now, since it is already on the board - self.replaceShapeWithUnitSquares( movableShape ); - } + // decompose the shape now, since it is already on the board + self.replaceShapeWithUnitSquares( movableShape ); } } - else { - // Shape did not go onto board, possibly because it's not over the board or the board is full. Send it - // home. - movableShape.returnToOrigin( true ); - } - } - } ); - - // Remove the shape if it returns to its origin, since at that point it has essentially been 'put away'. - movableShape.returnedToOriginEmitter.addListener( function() { - if ( !movableShape.userControlledProperty.get() ) { - self.movableShapes.remove( movableShape ); } - } ); - - // Another point at which the shape is removed is if it fades away. - movableShape.fadeProportionProperty.link( function fadeHandler( fadeProportion ) { - if ( fadeProportion === 1 ) { - self.movableShapes.remove( movableShape ); - movableShape.fadeProportionProperty.unlink( fadeHandler ); + else { + // Shape did not go onto board, possibly because it's not over the board or the board is full. Send it + // home. + movableShape.returnToOrigin( true ); } - } ); - }, - - /** - * Add a unit square directly to the shape placement board in the specified cell position (as opposed to model - * position). This was created to enable solutions to game challenges to be shown, but may have other uses. - * @param cellColumn - * @param cellRow - * @param color - * @private - */ - addUnitSquareDirectlyToBoard: function( cellColumn, cellRow, color ) { - const self = this; - const shape = new MovableShape( UNIT_SQUARE_SHAPE, color, this.solutionShapeOrigin ); - this.movableShapes.push( shape ); - - // Remove this shape when it gets returned to its original position. - shape.returnedToOriginEmitter.addListener( function() { - if ( !shape.userControlledProperty.get() ) { - self.movableShapes.remove( shape ); - } - } ); + } + } ); - this.shapePlacementBoard.addShapeDirectlyToCell( cellColumn, cellRow, shape ); - }, - - // @public, Clear the placement board of all shapes placed on it by the user - clearShapePlacementBoard: function() { - this.shapePlacementBoard.releaseAllShapes( 'jumpHome' ); - }, - - // @public? - startLevel: function() { - // Clear the 'show dimensions' and 'show grid' flag at the beginning of each new level. - this.shapePlacementBoard.showDimensionsProperty.value = false; - this.shapePlacementBoard.showGridProperty.value = false; - }, - - // @public - displayCorrectAnswer: function( challenge ) { - const self = this; - if ( challenge.buildSpec ) { - - // clear whatever the user had added - this.clearShapePlacementBoard(); - - // suspend updates of the shape placement board so that the answer can be added efficiently - this.shapePlacementBoard.suspendUpdatesForBlockAdd(); - - // Add the shapes that comprise the solution. - assert && assert( challenge.exampleBuildItSolution !== null, 'Error: Challenge does not contain an example solution.' ); - challenge.exampleBuildItSolution.forEach( function( shapePlacementSpec ) { - self.addUnitSquareDirectlyToBoard( shapePlacementSpec.cellColumn, shapePlacementSpec.cellRow, shapePlacementSpec.color ); - } ); + // Remove the shape if it returns to its origin, since at that point it has essentially been 'put away'. + movableShape.returnedToOriginEmitter.addListener( function() { + if ( !movableShape.userControlledProperty.get() ) { + self.movableShapes.remove( movableShape ); } - else if ( challenge.checkSpec === 'areaEntered' ) { - // For 'find the area' challenges, we turn on the grid for the background shape when displaying the answer. - this.shapePlacementBoard.showGridOnBackgroundShape = true; + } ); + + // Another point at which the shape is removed is if it fades away. + movableShape.fadeProportionProperty.link( function fadeHandler( fadeProportion ) { + if ( fadeProportion === 1 ) { + self.movableShapes.remove( movableShape ); + movableShape.fadeProportionProperty.unlink( fadeHandler ); } - }, - - // @public - checkAnswer: function( challenge ) { - - let answerIsCorrect = false; - let userBuiltSpec; - switch( challenge.checkSpec ) { - - case 'areaEntered': - answerIsCorrect = this.areaGuess === challenge.backgroundShape.unitArea; - break; - - case 'areaConstructed': - answerIsCorrect = challenge.buildSpec.area === this.shapePlacementBoard.areaAndPerimeterProperty.get().area; - break; - - case 'areaAndPerimeterConstructed': - answerIsCorrect = challenge.buildSpec.area === this.shapePlacementBoard.areaAndPerimeterProperty.get().area && - challenge.buildSpec.perimeter === this.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter; - break; - - case 'areaAndProportionConstructed': - userBuiltSpec = new BuildSpec( - this.shapePlacementBoard.areaAndPerimeterProperty.get().area, - null, - { - color1: challenge.buildSpec.proportions.color1, - color2: challenge.buildSpec.proportions.color2, - color1Proportion: this.getProportionOfColor( challenge.buildSpec.proportions.color1 ) - } - ); - answerIsCorrect = userBuiltSpec.equals( challenge.buildSpec ); - break; - - case 'areaPerimeterAndProportionConstructed': - userBuiltSpec = new BuildSpec( - this.shapePlacementBoard.areaAndPerimeterProperty.get().area, - this.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter, - { - color1: challenge.buildSpec.proportions.color1, - color2: challenge.buildSpec.proportions.color2, - color1Proportion: this.getProportionOfColor( challenge.buildSpec.proportions.color1 ) - } - ); - answerIsCorrect = userBuiltSpec.equals( challenge.buildSpec ); - break; - - default: - assert && assert( false, 'Unhandled check spec' ); - answerIsCorrect = false; - break; + } ); + }, + + /** + * Add a unit square directly to the shape placement board in the specified cell position (as opposed to model + * position). This was created to enable solutions to game challenges to be shown, but may have other uses. + * @param cellColumn + * @param cellRow + * @param color + * @private + */ + addUnitSquareDirectlyToBoard: function( cellColumn, cellRow, color ) { + const self = this; + const shape = new MovableShape( UNIT_SQUARE_SHAPE, color, this.solutionShapeOrigin ); + this.movableShapes.push( shape ); + + // Remove this shape when it gets returned to its original position. + shape.returnedToOriginEmitter.addListener( function() { + if ( !shape.userControlledProperty.get() ) { + self.movableShapes.remove( shape ); } + } ); + + this.shapePlacementBoard.addShapeDirectlyToCell( cellColumn, cellRow, shape ); + }, + + // @public, Clear the placement board of all shapes placed on it by the user + clearShapePlacementBoard: function() { + this.shapePlacementBoard.releaseAllShapes( 'jumpHome' ); + }, + + // @public? + startLevel: function() { + // Clear the 'show dimensions' and 'show grid' flag at the beginning of each new level. + this.shapePlacementBoard.showDimensionsProperty.value = false; + this.shapePlacementBoard.showGridProperty.value = false; + }, + + // @public + displayCorrectAnswer: function( challenge ) { + const self = this; + if ( challenge.buildSpec ) { - return answerIsCorrect; - }, - - // @public, Called from main model so that this model can do what it needs to in order to give the user another chance. - tryAgain: function() { - // Nothing needs to be reset in this model to allow the user to try again. - }, - - /** - * Returns the proportion of the shapes on the board that are the same color as the provided value. - * @param color - */ - getProportionOfColor: function( color ) { - // Pass through to the shape placement board. - return this.shapePlacementBoard.getProportionOfColor( color ); - }, - - /** - * Set up anything in the model that is needed for the specified challenge. - * - * @param challenge - * @public - */ - setChallenge: function( challenge ) { - if ( challenge ) { - assert && assert( typeof( challenge.backgroundShape !== 'undefined' ) ); - - // Set the background shape. - this.shapePlacementBoard.setBackgroundShape( challenge.backgroundShape, true ); - this.shapePlacementBoard.showGridOnBackgroundShape = false; // Initially off, may be turned on when showing solution. - - // Set the board to either form composite shapes or allow free placement. - this.shapePlacementBoard.formCompositeProperty.set( challenge.backgroundShape === null ); - - // Set the color scheme of the composite so that the placed squares can be seen if needed. - if ( challenge.buildSpec && this.shapePlacementBoard.formCompositeProperty.get() && challenge.userShapes ) { - - // Make the perimeter color be a darker version of the first user shape. - const perimeterColor = Color.toColor( challenge.userShapes[ 0 ].color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); - - let fillColor; - if ( challenge.buildSpec.proportions ) { - // The composite shape needs to be see through so that the original shapes can be seen. This allows - // multiple colors to be depicted, but generally doesn't look quite as good. - fillColor = null; + // clear whatever the user had added + this.clearShapePlacementBoard(); + + // suspend updates of the shape placement board so that the answer can be added efficiently + this.shapePlacementBoard.suspendUpdatesForBlockAdd(); + + // Add the shapes that comprise the solution. + assert && assert( challenge.exampleBuildItSolution !== null, 'Error: Challenge does not contain an example solution.' ); + challenge.exampleBuildItSolution.forEach( function( shapePlacementSpec ) { + self.addUnitSquareDirectlyToBoard( shapePlacementSpec.cellColumn, shapePlacementSpec.cellRow, shapePlacementSpec.color ); + } ); + } + else if ( challenge.checkSpec === 'areaEntered' ) { + // For 'find the area' challenges, we turn on the grid for the background shape when displaying the answer. + this.shapePlacementBoard.showGridOnBackgroundShape = true; + } + }, + + // @public + checkAnswer: function( challenge ) { + + let answerIsCorrect = false; + let userBuiltSpec; + switch( challenge.checkSpec ) { + + case 'areaEntered': + answerIsCorrect = this.areaGuess === challenge.backgroundShape.unitArea; + break; + + case 'areaConstructed': + answerIsCorrect = challenge.buildSpec.area === this.shapePlacementBoard.areaAndPerimeterProperty.get().area; + break; + + case 'areaAndPerimeterConstructed': + answerIsCorrect = challenge.buildSpec.area === this.shapePlacementBoard.areaAndPerimeterProperty.get().area && + challenge.buildSpec.perimeter === this.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter; + break; + + case 'areaAndProportionConstructed': + userBuiltSpec = new BuildSpec( + this.shapePlacementBoard.areaAndPerimeterProperty.get().area, + null, + { + color1: challenge.buildSpec.proportions.color1, + color2: challenge.buildSpec.proportions.color2, + color1Proportion: this.getProportionOfColor( challenge.buildSpec.proportions.color1 ) } - else { - // The fill color should be the same as the user shapes. Assume all user shapes are the same color. - fillColor = challenge.userShapes[ 0 ].color; + ); + answerIsCorrect = userBuiltSpec.equals( challenge.buildSpec ); + break; + + case 'areaPerimeterAndProportionConstructed': + userBuiltSpec = new BuildSpec( + this.shapePlacementBoard.areaAndPerimeterProperty.get().area, + this.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter, + { + color1: challenge.buildSpec.proportions.color1, + color2: challenge.buildSpec.proportions.color2, + color1Proportion: this.getProportionOfColor( challenge.buildSpec.proportions.color1 ) } + ); + answerIsCorrect = userBuiltSpec.equals( challenge.buildSpec ); + break; + + default: + assert && assert( false, 'Unhandled check spec' ); + answerIsCorrect = false; + break; + } - this.shapePlacementBoard.setCompositeShapeColorScheme( fillColor, perimeterColor ); - } - } - }, + return answerIsCorrect; + }, + + // @public, Called from main model so that this model can do what it needs to in order to give the user another chance. + tryAgain: function() { + // Nothing needs to be reset in this model to allow the user to try again. + }, - step: function( dt ) { - this.movableShapes.forEach( function( movableShape ) { movableShape.step( dt ); } ); - }, + /** + * Returns the proportion of the shapes on the board that are the same color as the provided value. + * @param color + */ + getProportionOfColor: function( color ) { + // Pass through to the shape placement board. + return this.shapePlacementBoard.getProportionOfColor( color ); + }, - // Resets all model elements - reset: function() { - this.shapePlacementBoard.releaseAllShapes( 'jumpHome' ); - this.movableShapes.clear(); + /** + * Set up anything in the model that is needed for the specified challenge. + * + * @param challenge + * @public + */ + setChallenge: function( challenge ) { + if ( challenge ) { + assert && assert( typeof ( challenge.backgroundShape !== 'undefined' ) ); + + // Set the background shape. + this.shapePlacementBoard.setBackgroundShape( challenge.backgroundShape, true ); + this.shapePlacementBoard.showGridOnBackgroundShape = false; // Initially off, may be turned on when showing solution. + + // Set the board to either form composite shapes or allow free placement. + this.shapePlacementBoard.formCompositeProperty.set( challenge.backgroundShape === null ); + + // Set the color scheme of the composite so that the placed squares can be seen if needed. + if ( challenge.buildSpec && this.shapePlacementBoard.formCompositeProperty.get() && challenge.userShapes ) { + + // Make the perimeter color be a darker version of the first user shape. + const perimeterColor = Color.toColor( challenge.userShapes[ 0 ].color ).colorUtilsDarker( AreaBuilderSharedConstants.PERIMETER_DARKEN_FACTOR ); + + let fillColor; + if ( challenge.buildSpec.proportions ) { + // The composite shape needs to be see through so that the original shapes can be seen. This allows + // multiple colors to be depicted, but generally doesn't look quite as good. + fillColor = null; + } + else { + // The fill color should be the same as the user shapes. Assume all user shapes are the same color. + fillColor = challenge.userShapes[ 0 ].color; + } + + this.shapePlacementBoard.setCompositeShapeColorScheme( fillColor, perimeterColor ); + } } }, - { - // Size of the shape board in terms of the unit length, needed by the challenge factory. - SHAPE_BOARD_UNIT_WIDTH: BOARD_SIZE.width / UNIT_SQUARE_LENGTH, - SHAPE_BOARD_UNIT_HEIGHT: BOARD_SIZE.height / UNIT_SQUARE_LENGTH, - UNIT_SQUARE_LENGTH: UNIT_SQUARE_LENGTH + + step: function( dt ) { + this.movableShapes.forEach( function( movableShape ) { movableShape.step( dt ); } ); + }, + + // Resets all model elements + reset: function() { + this.shapePlacementBoard.releaseAllShapes( 'jumpHome' ); + this.movableShapes.clear(); } - ); -} ); + }, + { + // Size of the shape board in terms of the unit length, needed by the challenge factory. + SHAPE_BOARD_UNIT_WIDTH: BOARD_SIZE.width / UNIT_SQUARE_LENGTH, + SHAPE_BOARD_UNIT_HEIGHT: BOARD_SIZE.height / UNIT_SQUARE_LENGTH, + UNIT_SQUARE_LENGTH: UNIT_SQUARE_LENGTH + } +); \ No newline at end of file diff --git a/js/game/model/BuildSpec.js b/js/game/model/BuildSpec.js index ddb7d45..979271a 100644 --- a/js/game/model/BuildSpec.js +++ b/js/game/model/BuildSpec.js @@ -6,102 +6,98 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const Color = require( 'SCENERY/util/Color' ); - const Fraction = require( 'PHETCOMMON/model/Fraction' ); - const inherit = require( 'PHET_CORE/inherit' ); +import inherit from '../../../../phet-core/js/inherit.js'; +import Fraction from '../../../../phetcommon/js/model/Fraction.js'; +import Color from '../../../../scenery/js/util/Color.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; - /** - * @param {number} area - Area of the shape that the user should construct from smaller shapes - * @param {number} [perimeter] - Perimeter of the shapes that the user should construct - * @param {Object} [colorProportionsSpec] - An object that specifies two colors and the proportion of the first color - * that should be present in the user's solution. - * @constructor - */ - function BuildSpec( area, perimeter, colorProportionsSpec ) { - assert && assert( typeof( area ) === 'number' || area === AreaBuilderSharedConstants.INVALID_VALUE ); - this.area = area; - if ( typeof( perimeter ) !== 'undefined' && perimeter !== null ) { - assert && assert( typeof( perimeter ) === 'number' || perimeter === AreaBuilderSharedConstants.INVALID_VALUE ); - this.perimeter = perimeter; - } - if ( colorProportionsSpec ) { - assert && assert( colorProportionsSpec.color1Proportion instanceof Fraction ); - this.proportions = { - color1: Color.toColor( colorProportionsSpec.color1 ), - color2: Color.toColor( colorProportionsSpec.color2 ), - color1Proportion: colorProportionsSpec.color1Proportion - }; - } +/** + * @param {number} area - Area of the shape that the user should construct from smaller shapes + * @param {number} [perimeter] - Perimeter of the shapes that the user should construct + * @param {Object} [colorProportionsSpec] - An object that specifies two colors and the proportion of the first color + * that should be present in the user's solution. + * @constructor + */ +function BuildSpec( area, perimeter, colorProportionsSpec ) { + assert && assert( typeof ( area ) === 'number' || area === AreaBuilderSharedConstants.INVALID_VALUE ); + this.area = area; + if ( typeof ( perimeter ) !== 'undefined' && perimeter !== null ) { + assert && assert( typeof ( perimeter ) === 'number' || perimeter === AreaBuilderSharedConstants.INVALID_VALUE ); + this.perimeter = perimeter; } + if ( colorProportionsSpec ) { + assert && assert( colorProportionsSpec.color1Proportion instanceof Fraction ); + this.proportions = { + color1: Color.toColor( colorProportionsSpec.color1 ), + color2: Color.toColor( colorProportionsSpec.color2 ), + color1Proportion: colorProportionsSpec.color1Proportion + }; + } +} - areaBuilder.register( 'BuildSpec', BuildSpec ); - - return inherit( Object, BuildSpec, { - equals: function( that ) { +areaBuilder.register( 'BuildSpec', BuildSpec ); - if ( !( that instanceof BuildSpec ) ) { return false; } +export default inherit( Object, BuildSpec, { + equals: function( that ) { - // Compare area, which should always be defined. - if ( this.area !== that.area ) { - return false; - } + if ( !( that instanceof BuildSpec ) ) { return false; } - // Compare perimeter - if ( this.perimeter && !that.perimeter || - !this.perimeter && that.perimeter || - this.perimeter !== that.perimeter ) { - return false; - } + // Compare area, which should always be defined. + if ( this.area !== that.area ) { + return false; + } - // Compare proportions - if ( !this.proportions && !that.proportions ) { - // Neither defines proportions, so we're good. - return true; - } + // Compare perimeter + if ( this.perimeter && !that.perimeter || + !this.perimeter && that.perimeter || + this.perimeter !== that.perimeter ) { + return false; + } - if ( this.proportions && !that.proportions || !this.proportions && that.proportions ) { - // One defines proportions and the other doesn't, so they don't match. - return false; - } + // Compare proportions + if ( !this.proportions && !that.proportions ) { + // Neither defines proportions, so we're good. + return true; + } - // From here, if the proportion spec matches, the build specs are equal. - return ( this.proportions.color1.equals( that.proportions.color1 ) && - this.proportions.color2.equals( that.proportions.color2 ) && - this.proportions.color1Proportion.equals( that.proportions.color1Proportion ) ); + if ( this.proportions && !that.proportions || !this.proportions && that.proportions ) { + // One defines proportions and the other doesn't, so they don't match. + return false; } - }, { - // Static creator functions - areaOnly: function( area ) { - return new BuildSpec( area ); - }, + // From here, if the proportion spec matches, the build specs are equal. + return ( this.proportions.color1.equals( that.proportions.color1 ) && + this.proportions.color2.equals( that.proportions.color2 ) && + this.proportions.color1Proportion.equals( that.proportions.color1Proportion ) ); + } +}, { - areaAndPerimeter: function( area, perimeter ) { - return new BuildSpec( area, perimeter ); - }, + // Static creator functions + areaOnly: function( area ) { + return new BuildSpec( area ); + }, - areaAndProportions: function( area, color1, color2, color1Proportion ) { - return new BuildSpec( area, null, { - color1: color1, - color2: color2, - color1Proportion: color1Proportion - } - ); - }, + areaAndPerimeter: function( area, perimeter ) { + return new BuildSpec( area, perimeter ); + }, - areaPerimeterAndProportions: function( area, perimeter, color1, color2, color1Proportion ) { - return new BuildSpec( area, perimeter, { - color1: color1, - color2: color2, - color1Proportion: color1Proportion - } - ); - } - } ); + areaAndProportions: function( area, color1, color2, color1Proportion ) { + return new BuildSpec( area, null, { + color1: color1, + color2: color2, + color1Proportion: color1Proportion + } + ); + }, + + areaPerimeterAndProportions: function( area, perimeter, color1, color2, color1Proportion ) { + return new BuildSpec( area, perimeter, { + color1: color1, + color2: color2, + color1Proportion: color1Proportion + } + ); + } } ); \ No newline at end of file diff --git a/js/game/model/GameState.js b/js/game/model/GameState.js index 426f42f..f6b2a54 100644 --- a/js/game/model/GameState.js +++ b/js/game/model/GameState.js @@ -5,26 +5,22 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); +import areaBuilder from '../../areaBuilder.js'; - const GameState = { - CHOOSING_LEVEL: 'choosingLevel', - PRESENTING_INTERACTIVE_CHALLENGE: 'presentingInteractiveChallenge', - SHOWING_CORRECT_ANSWER_FEEDBACK: 'showingCorrectAnswerFeedback', - SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN: 'showingIncorrectAnswerFeedbackTryAgain', - SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON: 'showingIncorrectAnswerFeedbackMoveOn', - DISPLAYING_CORRECT_ANSWER: 'displayingCorrectAnswer', - SHOWING_LEVEL_RESULTS: 'showingLevelResults' - }; +const GameState = { + CHOOSING_LEVEL: 'choosingLevel', + PRESENTING_INTERACTIVE_CHALLENGE: 'presentingInteractiveChallenge', + SHOWING_CORRECT_ANSWER_FEEDBACK: 'showingCorrectAnswerFeedback', + SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN: 'showingIncorrectAnswerFeedbackTryAgain', + SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON: 'showingIncorrectAnswerFeedbackMoveOn', + DISPLAYING_CORRECT_ANSWER: 'displayingCorrectAnswer', + SHOWING_LEVEL_RESULTS: 'showingLevelResults' +}; - // verify that enum is immutable, without the runtime penalty in production code - if ( assert ) { Object.freeze( GameState ); } +// verify that enum is immutable, without the runtime penalty in production code +if ( assert ) { Object.freeze( GameState ); } - areaBuilder.register( 'GameState', GameState ); +areaBuilder.register( 'GameState', GameState ); - return GameState; -} ); \ No newline at end of file +export default GameState; \ No newline at end of file diff --git a/js/game/model/QuizGameModel.js b/js/game/model/QuizGameModel.js index 406ad74..a1d0577 100644 --- a/js/game/model/QuizGameModel.js +++ b/js/game/model/QuizGameModel.js @@ -12,225 +12,221 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const GameState = require( 'AREA_BUILDER/game/model/GameState' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Property = require( 'AXON/Property' ); - const timer = require( 'AXON/timer' ); - - /** - * @param challengeFactory - Factory object that is used to create challenges, examine usage for details. - * @param simSpecificModel - Model containing the elements of the game that are unique to this sim, used to delegate - * delegate certain actions. Look through code for usage details. - * @param {Object} [options] - * @constructor - */ - function QuizGameModel( challengeFactory, simSpecificModel, options ) { - const self = this; - this.challengeFactory = challengeFactory; // @private - this.simSpecificModel = simSpecificModel; // @public - - options = merge( { - numberOfLevels: 6, - challengesPerSet: 6, - maxPointsPerChallenge: 2, - maxAttemptsPerChallenge: 2 - }, options ); - - // @public - model properties - this.timerEnabledProperty = new Property( false ); - this.levelProperty = new Property( 0 ); - this.challengeIndexProperty = new Property( 0 ); - this.currentChallengeProperty = new Property( null ); - this.scoreProperty = new Property( 0 ); - this.elapsedTimeProperty = new Property( 0 ); - this.gameStateProperty = new Property( GameState.CHOOSING_LEVEL ); // Current state of the game, see GameState for valid values. - - // other public vars - this.numberOfLevels = options.numberOfLevels; // @public - this.challengesPerSet = options.challengesPerSet; // @public - this.maxPointsPerChallenge = options.maxPointsPerChallenge; // @public - this.maxPossibleScore = options.challengesPerSet * options.maxPointsPerChallenge; // @public - this.maxAttemptsPerChallenge = options.maxAttemptsPerChallenge; // @private - - // @private Wall time at which current level was started. - self.gameStartTime = 0; - - // Best times and scores. - self.bestTimes = []; // @public - self.bestScoreProperties = []; // @public - _.times( options.numberOfLevels, function() { - self.bestTimes.push( null ); - self.bestScoreProperties.push( new Property( 0 ) ); - } ); - - // Counter used to track number of incorrect answers. - this.incorrectGuessesOnCurrentChallenge = 0; // @public - - // Current set of challenges, which collectively comprise a single level, on which the user is currently working. - self.challengeList = null; // @private - - // Let the sim-specific model know when the challenge changes. - self.currentChallengeProperty.lazyLink( function( challenge ) { simSpecificModel.setChallenge( challenge ); } ); - } - - areaBuilder.register( 'QuizGameModel', QuizGameModel ); - - return inherit( Object, QuizGameModel, - { - // @private - step: function( dt ) { - this.simSpecificModel.step( dt ); - }, - - // reset this model - reset: function() { - this.timerEnabledProperty.reset(); - this.levelProperty.reset(); - this.challengeIndexProperty.reset(); - this.currentChallengeProperty.reset(); - this.scoreProperty.reset(); - this.elapsedTimeProperty.reset(); - this.gameStateProperty.reset(); - this.bestScoreProperties.forEach( function( bestScoreProperty ) { bestScoreProperty.reset(); } ); - this.bestTimes = []; - const self = this; - _.times( this.numberOfLevels, function() { - self.bestTimes.push( null ); - } ); - }, - - // starts new level - startLevel: function( level ) { - this.levelProperty.set( level ); - this.scoreProperty.reset(); - this.challengeIndexProperty.set( 0 ); - this.incorrectGuessesOnCurrentChallenge = 0; - this.restartGameTimer(); - - // Create the list of challenges. - this.challengeList = this.challengeFactory.generateChallengeSet( level, this.challengesPerSet ); - - // Set up the model for the next challenge - this.currentChallengeProperty.set( this.challengeList[ this.challengeIndexProperty.get() ] ); - - // Let the sim-specific model know that a new level is being started in case it needs to do any initialization. - this.simSpecificModel.startLevel(); - // Change to new game state. - this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); +import Property from '../../../../axon/js/Property.js'; +import timer from '../../../../axon/js/timer.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import areaBuilder from '../../areaBuilder.js'; +import GameState from './GameState.js'; - // Flag set to indicate new best time, cleared each time a level is started. - this.newBestTime = false; - }, - - setChoosingLevelState: function() { - this.gameStateProperty.set( GameState.CHOOSING_LEVEL ); - }, - - getChallengeCurrentPointValue: function() { - return Math.max( this.maxPointsPerChallenge - this.incorrectGuessesOnCurrentChallenge, 0 ); - }, - - // Check the user's proposed answer. - checkAnswer: function( answer ) { - this.handleProposedAnswer( this.simSpecificModel.checkAnswer( this.currentChallengeProperty.get() ) ); - }, - - // @private - handleProposedAnswer: function( answerIsCorrect ) { - let pointsEarned = 0; - if ( answerIsCorrect ) { - // The user answered the challenge correctly. - this.gameStateProperty.set( GameState.SHOWING_CORRECT_ANSWER_FEEDBACK ); - if ( this.incorrectGuessesOnCurrentChallenge === 0 ) { - // User got it right the first time. - pointsEarned = this.maxPointsPerChallenge; - } - else { - // User got it wrong at first, but got it right now. - pointsEarned = Math.max( this.maxPointsPerChallenge - this.incorrectGuessesOnCurrentChallenge, 0 ); - } - this.scoreProperty.value += pointsEarned; +/** + * @param challengeFactory - Factory object that is used to create challenges, examine usage for details. + * @param simSpecificModel - Model containing the elements of the game that are unique to this sim, used to delegate + * delegate certain actions. Look through code for usage details. + * @param {Object} [options] + * @constructor + */ +function QuizGameModel( challengeFactory, simSpecificModel, options ) { + const self = this; + this.challengeFactory = challengeFactory; // @private + this.simSpecificModel = simSpecificModel; // @public + + options = merge( { + numberOfLevels: 6, + challengesPerSet: 6, + maxPointsPerChallenge: 2, + maxAttemptsPerChallenge: 2 + }, options ); + + // @public - model properties + this.timerEnabledProperty = new Property( false ); + this.levelProperty = new Property( 0 ); + this.challengeIndexProperty = new Property( 0 ); + this.currentChallengeProperty = new Property( null ); + this.scoreProperty = new Property( 0 ); + this.elapsedTimeProperty = new Property( 0 ); + this.gameStateProperty = new Property( GameState.CHOOSING_LEVEL ); // Current state of the game, see GameState for valid values. + + // other public vars + this.numberOfLevels = options.numberOfLevels; // @public + this.challengesPerSet = options.challengesPerSet; // @public + this.maxPointsPerChallenge = options.maxPointsPerChallenge; // @public + this.maxPossibleScore = options.challengesPerSet * options.maxPointsPerChallenge; // @public + this.maxAttemptsPerChallenge = options.maxAttemptsPerChallenge; // @private + + // @private Wall time at which current level was started. + self.gameStartTime = 0; + + // Best times and scores. + self.bestTimes = []; // @public + self.bestScoreProperties = []; // @public + _.times( options.numberOfLevels, function() { + self.bestTimes.push( null ); + self.bestScoreProperties.push( new Property( 0 ) ); + } ); + + // Counter used to track number of incorrect answers. + this.incorrectGuessesOnCurrentChallenge = 0; // @public + + // Current set of challenges, which collectively comprise a single level, on which the user is currently working. + self.challengeList = null; // @private + + // Let the sim-specific model know when the challenge changes. + self.currentChallengeProperty.lazyLink( function( challenge ) { simSpecificModel.setChallenge( challenge ); } ); +} + +areaBuilder.register( 'QuizGameModel', QuizGameModel ); + +export default inherit( Object, QuizGameModel, + { + // @private + step: function( dt ) { + this.simSpecificModel.step( dt ); + }, + + // reset this model + reset: function() { + this.timerEnabledProperty.reset(); + this.levelProperty.reset(); + this.challengeIndexProperty.reset(); + this.currentChallengeProperty.reset(); + this.scoreProperty.reset(); + this.elapsedTimeProperty.reset(); + this.gameStateProperty.reset(); + this.bestScoreProperties.forEach( function( bestScoreProperty ) { bestScoreProperty.reset(); } ); + this.bestTimes = []; + const self = this; + _.times( this.numberOfLevels, function() { + self.bestTimes.push( null ); + } ); + }, + + // starts new level + startLevel: function( level ) { + this.levelProperty.set( level ); + this.scoreProperty.reset(); + this.challengeIndexProperty.set( 0 ); + this.incorrectGuessesOnCurrentChallenge = 0; + this.restartGameTimer(); + + // Create the list of challenges. + this.challengeList = this.challengeFactory.generateChallengeSet( level, this.challengesPerSet ); + + // Set up the model for the next challenge + this.currentChallengeProperty.set( this.challengeList[ this.challengeIndexProperty.get() ] ); + + // Let the sim-specific model know that a new level is being started in case it needs to do any initialization. + this.simSpecificModel.startLevel(); + + // Change to new game state. + this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); + + // Flag set to indicate new best time, cleared each time a level is started. + this.newBestTime = false; + }, + + setChoosingLevelState: function() { + this.gameStateProperty.set( GameState.CHOOSING_LEVEL ); + }, + + getChallengeCurrentPointValue: function() { + return Math.max( this.maxPointsPerChallenge - this.incorrectGuessesOnCurrentChallenge, 0 ); + }, + + // Check the user's proposed answer. + checkAnswer: function( answer ) { + this.handleProposedAnswer( this.simSpecificModel.checkAnswer( this.currentChallengeProperty.get() ) ); + }, + + // @private + handleProposedAnswer: function( answerIsCorrect ) { + let pointsEarned = 0; + if ( answerIsCorrect ) { + // The user answered the challenge correctly. + this.gameStateProperty.set( GameState.SHOWING_CORRECT_ANSWER_FEEDBACK ); + if ( this.incorrectGuessesOnCurrentChallenge === 0 ) { + // User got it right the first time. + pointsEarned = this.maxPointsPerChallenge; } else { - // The user got it wrong. - this.incorrectGuessesOnCurrentChallenge++; - if ( this.incorrectGuessesOnCurrentChallenge < this.maxAttemptsPerChallenge ) { - this.gameStateProperty.set( GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ); - } - else { - this.gameStateProperty.set( GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON ); - } + // User got it wrong at first, but got it right now. + pointsEarned = Math.max( this.maxPointsPerChallenge - this.incorrectGuessesOnCurrentChallenge, 0 ); } - }, - - // @private - newGame: function() { - this.stopGameTimer(); - this.gameStateProperty.set( GameState.CHOOSING_LEVEL ); - this.incorrectGuessesOnCurrentChallenge = 0; - }, - - // Move to the next challenge in the current challenge set. - nextChallenge: function() { - const currentLevel = this.levelProperty.get(); - this.incorrectGuessesOnCurrentChallenge = 0; - if ( this.challengeIndexProperty.get() + 1 < this.challengeList.length ) { - // Move to the next challenge. - this.challengeIndexProperty.value++; - this.currentChallengeProperty.set( this.challengeList[ this.challengeIndexProperty.get() ] ); - this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); + this.scoreProperty.value += pointsEarned; + } + else { + // The user got it wrong. + this.incorrectGuessesOnCurrentChallenge++; + if ( this.incorrectGuessesOnCurrentChallenge < this.maxAttemptsPerChallenge ) { + this.gameStateProperty.set( GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ); } else { - // All challenges completed for this level. See if this is a new best time and, if so, record it. - if ( this.scoreProperty.get() === this.maxPossibleScore ) { - // Perfect game. See if new best time. - if ( this.bestTimes[ currentLevel ] === null || this.elapsedTimeProperty.get() < this.bestTimes[ currentLevel ] ) { - this.newBestTime = this.bestTimes[ currentLevel ] !== null; // Don't set this flag for the first 'best time', only when the time improves. - this.bestTimes[ currentLevel ] = this.elapsedTimeProperty.get(); - } + this.gameStateProperty.set( GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON ); + } + } + }, + + // @private + newGame: function() { + this.stopGameTimer(); + this.gameStateProperty.set( GameState.CHOOSING_LEVEL ); + this.incorrectGuessesOnCurrentChallenge = 0; + }, + + // Move to the next challenge in the current challenge set. + nextChallenge: function() { + const currentLevel = this.levelProperty.get(); + this.incorrectGuessesOnCurrentChallenge = 0; + if ( this.challengeIndexProperty.get() + 1 < this.challengeList.length ) { + // Move to the next challenge. + this.challengeIndexProperty.value++; + this.currentChallengeProperty.set( this.challengeList[ this.challengeIndexProperty.get() ] ); + this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); + } + else { + // All challenges completed for this level. See if this is a new best time and, if so, record it. + if ( this.scoreProperty.get() === this.maxPossibleScore ) { + // Perfect game. See if new best time. + if ( this.bestTimes[ currentLevel ] === null || this.elapsedTimeProperty.get() < this.bestTimes[ currentLevel ] ) { + this.newBestTime = this.bestTimes[ currentLevel ] !== null; // Don't set this flag for the first 'best time', only when the time improves. + this.bestTimes[ currentLevel ] = this.elapsedTimeProperty.get(); } - this.bestScoreProperties[ currentLevel ].value = this.scoreProperty.get(); - - // Done with this game, show the results. - this.gameStateProperty.set( GameState.SHOWING_LEVEL_RESULTS ); } - }, + this.bestScoreProperties[ currentLevel ].value = this.scoreProperty.get(); - tryAgain: function() { - this.simSpecificModel.tryAgain(); - this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); - }, + // Done with this game, show the results. + this.gameStateProperty.set( GameState.SHOWING_LEVEL_RESULTS ); + } + }, - displayCorrectAnswer: function() { + tryAgain: function() { + this.simSpecificModel.tryAgain(); + this.gameStateProperty.set( GameState.PRESENTING_INTERACTIVE_CHALLENGE ); + }, - // Set the challenge to display the correct answer. - this.simSpecificModel.displayCorrectAnswer( this.currentChallengeProperty.get() ); + displayCorrectAnswer: function() { - // Update the game state. - this.gameStateProperty.set( GameState.DISPLAYING_CORRECT_ANSWER ); - }, + // Set the challenge to display the correct answer. + this.simSpecificModel.displayCorrectAnswer( this.currentChallengeProperty.get() ); - // @private - restartGameTimer: function() { - if ( this.gameTimerId !== null ) { - window.clearInterval( this.gameTimerId ); - } - this.elapsedTimeProperty.set( 0 ); - const self = this; - this.gameTimerId = timer.setInterval( function() { self.elapsedTimeProperty.value += 1; }, 1000 ); - }, + // Update the game state. + this.gameStateProperty.set( GameState.DISPLAYING_CORRECT_ANSWER ); + }, - // @private - stopGameTimer: function() { + // @private + restartGameTimer: function() { + if ( this.gameTimerId !== null ) { window.clearInterval( this.gameTimerId ); - this.gameTimerId = null; } - } ); -} ); + this.elapsedTimeProperty.set( 0 ); + const self = this; + this.gameTimerId = timer.setInterval( function() { self.elapsedTimeProperty.value += 1; }, 1000 ); + }, + + // @private + stopGameTimer: function() { + window.clearInterval( this.gameTimerId ); + this.gameTimerId = null; + } + } ); \ No newline at end of file diff --git a/js/game/view/AreaBuilderGameView.js b/js/game/view/AreaBuilderGameView.js index f2ac921..49f006b 100644 --- a/js/game/view/AreaBuilderGameView.js +++ b/js/game/view/AreaBuilderGameView.js @@ -5,943 +5,940 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const Animation = require( 'TWIXT/Animation' ); - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderControlPanel = require( 'AREA_BUILDER/common/view/AreaBuilderControlPanel' ); - const AreaBuilderGameModel = require( 'AREA_BUILDER/game/model/AreaBuilderGameModel' ); - const AreaBuilderScoreboard = require( 'AREA_BUILDER/game/view/AreaBuilderScoreboard' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const BuildSpec = require( 'AREA_BUILDER/game/model/BuildSpec' ); - const Carousel = require( 'SUN/Carousel' ); - const ColorProportionsPrompt = require( 'AREA_BUILDER/game/view/ColorProportionsPrompt' ); - const Easing = require( 'TWIXT/Easing' ); - const EraserButton = require( 'SCENERY_PHET/buttons/EraserButton' ); - const FaceWithPointsNode = require( 'SCENERY_PHET/FaceWithPointsNode' ); - const GameAudioPlayer = require( 'VEGAS/GameAudioPlayer' ); - const GameIconFactory = require( 'AREA_BUILDER/game/view/GameIconFactory' ); - const GameInfoBanner = require( 'AREA_BUILDER/game/view/GameInfoBanner' ); - const GameState = require( 'AREA_BUILDER/game/model/GameState' ); - const HBox = require( 'SCENERY/nodes/HBox' ); - const inherit = require( 'PHET_CORE/inherit' ); - const LevelCompletedNode = require( 'VEGAS/LevelCompletedNode' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const NumberEntryControl = require( 'SCENERY_PHET/NumberEntryControl' ); - const Panel = require( 'SUN/Panel' ); - const PhetColorScheme = require( 'SCENERY_PHET/PhetColorScheme' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const RectangularPushButton = require( 'SUN/buttons/RectangularPushButton' ); - const ScreenView = require( 'JOIST/ScreenView' ); - const ShapeCreatorNode = require( 'AREA_BUILDER/common/view/ShapeCreatorNode' ); - const ShapeNode = require( 'AREA_BUILDER/common/view/ShapeNode' ); - const ShapePlacementBoardNode = require( 'AREA_BUILDER/common/view/ShapePlacementBoardNode' ); - const StartGameLevelNode = require( 'AREA_BUILDER/game/view/StartGameLevelNode' ); - const StringUtils = require( 'PHETCOMMON/util/StringUtils' ); - const Text = require( 'SCENERY/nodes/Text' ); - const TextPushButton = require( 'SUN/buttons/TextPushButton' ); - const VBox = require( 'SCENERY/nodes/VBox' ); - const YouBuiltWindow = require( 'AREA_BUILDER/game/view/YouBuiltWindow' ); - const YouEnteredWindow = require( 'AREA_BUILDER/game/view/YouEnteredWindow' ); - - // strings - const areaEqualsString = require( 'string!AREA_BUILDER/areaEquals' ); - const areaQuestionString = require( 'string!AREA_BUILDER/areaQuestion' ); - const aSolutionColonString = require( 'string!AREA_BUILDER/aSolutionColon' ); - const aSolutionString = require( 'string!AREA_BUILDER/aSolution' ); - const buildItString = require( 'string!AREA_BUILDER/buildIt' ); - const checkString = require( 'string!VEGAS/check' ); - const findTheAreaString = require( 'string!AREA_BUILDER/findTheArea' ); - const nextString = require( 'string!VEGAS/next' ); - const perimeterEqualsString = require( 'string!AREA_BUILDER/perimeterEquals' ); - const solutionColonString = require( 'string!AREA_BUILDER/solutionColon' ); - const solutionString = require( 'string!AREA_BUILDER/solution' ); - const startOverString = require( 'string!AREA_BUILDER/startOver' ); - const tryAgainString = require( 'string!VEGAS/tryAgain' ); - const yourGoalString = require( 'string!AREA_BUILDER/yourGoal' ); - - // constants - const BUTTON_FONT = new PhetFont( 18 ); - const BUTTON_FILL = PhetColorScheme.BUTTON_YELLOW; - const INFO_BANNER_HEIGHT = 60; // Height of the prompt and solution banners, empirically determined. - const GOAL_PROMPT_FONT = new PhetFont( { size: 20, weight: 'bold' } ); - const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; - const ITEMS_PER_CAROUSEL_PAGE = 4; - const BUTTON_TOUCH_AREA_DILATION = 7; - - /** - * @param {AreaBuilderGameModel} gameModel - * @constructor - */ - function AreaBuilderGameView( gameModel ) { - ScreenView.call( this, { layoutBounds: AreaBuilderSharedConstants.LAYOUT_BOUNDS } ); - const self = this; - self.model = gameModel; - // Create the game audio player. - this.gameAudioPlayer = new GameAudioPlayer(); +import ScreenView from '../../../../joist/js/ScreenView.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; +import EraserButton from '../../../../scenery-phet/js/buttons/EraserButton.js'; +import FaceWithPointsNode from '../../../../scenery-phet/js/FaceWithPointsNode.js'; +import NumberEntryControl from '../../../../scenery-phet/js/NumberEntryControl.js'; +import PhetColorScheme from '../../../../scenery-phet/js/PhetColorScheme.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import HBox from '../../../../scenery/js/nodes/HBox.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import VBox from '../../../../scenery/js/nodes/VBox.js'; +import RectangularPushButton from '../../../../sun/js/buttons/RectangularPushButton.js'; +import TextPushButton from '../../../../sun/js/buttons/TextPushButton.js'; +import Carousel from '../../../../sun/js/Carousel.js'; +import Panel from '../../../../sun/js/Panel.js'; +import Animation from '../../../../twixt/js/Animation.js'; +import Easing from '../../../../twixt/js/Easing.js'; +import GameAudioPlayer from '../../../../vegas/js/GameAudioPlayer.js'; +import LevelCompletedNode from '../../../../vegas/js/LevelCompletedNode.js'; +import vegasStrings from '../../../../vegas/js/vegas-strings.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import AreaBuilderControlPanel from '../../common/view/AreaBuilderControlPanel.js'; +import ShapeCreatorNode from '../../common/view/ShapeCreatorNode.js'; +import ShapeNode from '../../common/view/ShapeNode.js'; +import ShapePlacementBoardNode from '../../common/view/ShapePlacementBoardNode.js'; +import AreaBuilderGameModel from '../model/AreaBuilderGameModel.js'; +import BuildSpec from '../model/BuildSpec.js'; +import GameState from '../model/GameState.js'; +import AreaBuilderScoreboard from './AreaBuilderScoreboard.js'; +import ColorProportionsPrompt from './ColorProportionsPrompt.js'; +import GameIconFactory from './GameIconFactory.js'; +import GameInfoBanner from './GameInfoBanner.js'; +import StartGameLevelNode from './StartGameLevelNode.js'; +import YouBuiltWindow from './YouBuiltWindow.js'; +import YouEnteredWindow from './YouEnteredWindow.js'; + +const areaEqualsString = areaBuilderStrings.areaEquals; +const areaQuestionString = areaBuilderStrings.areaQuestion; +const aSolutionColonString = areaBuilderStrings.aSolutionColon; +const aSolutionString = areaBuilderStrings.aSolution; +const buildItString = areaBuilderStrings.buildIt; +const checkString = vegasStrings.check; +const findTheAreaString = areaBuilderStrings.findTheArea; +const nextString = vegasStrings.next; +const perimeterEqualsString = areaBuilderStrings.perimeterEquals; +const solutionColonString = areaBuilderStrings.solutionColon; +const solutionString = areaBuilderStrings.solution; +const startOverString = areaBuilderStrings.startOver; +const tryAgainString = vegasStrings.tryAgain; +const yourGoalString = areaBuilderStrings.yourGoal; + +// constants +const BUTTON_FONT = new PhetFont( 18 ); +const BUTTON_FILL = PhetColorScheme.BUTTON_YELLOW; +const INFO_BANNER_HEIGHT = 60; // Height of the prompt and solution banners, empirically determined. +const GOAL_PROMPT_FONT = new PhetFont( { size: 20, weight: 'bold' } ); +const SPACE_AROUND_SHAPE_PLACEMENT_BOARD = AreaBuilderSharedConstants.CONTROLS_INSET; +const ITEMS_PER_CAROUSEL_PAGE = 4; +const BUTTON_TOUCH_AREA_DILATION = 7; - // Create a root node and send to back so that the layout bounds box can be made visible if needed. - this.rootNode = new Node(); - this.addChild( self.rootNode ); - this.rootNode.moveToBack(); +/** + * @param {AreaBuilderGameModel} gameModel + * @constructor + */ +function AreaBuilderGameView( gameModel ) { + ScreenView.call( this, { layoutBounds: AreaBuilderSharedConstants.LAYOUT_BOUNDS } ); + const self = this; + self.model = gameModel; + + // Create the game audio player. + this.gameAudioPlayer = new GameAudioPlayer(); + + // Create a root node and send to back so that the layout bounds box can be made visible if needed. + this.rootNode = new Node(); + this.addChild( self.rootNode ); + this.rootNode.moveToBack(); + + // Add layers used to control game appearance. + this.controlLayer = new Node(); + this.rootNode.addChild( this.controlLayer ); + this.challengeLayer = new Node(); + this.rootNode.addChild( this.challengeLayer ); + + // Add the node that allows the user to choose a game level to play. + this.startGameLevelNode = new StartGameLevelNode( + function( level ) { + self.numberEntryControl.clear(); + gameModel.startLevel( level ); + }, + function() { gameModel.reset(); }, + gameModel.timerEnabledProperty, + [ + GameIconFactory.createIcon( 1 ), + GameIconFactory.createIcon( 2 ), + GameIconFactory.createIcon( 3 ), + GameIconFactory.createIcon( 4 ), + GameIconFactory.createIcon( 5 ), + GameIconFactory.createIcon( 6 ) + ], + gameModel.bestScoreProperties, + { + numStarsOnButtons: gameModel.challengesPerSet, + perfectScore: gameModel.maxPossibleScore, + numLevels: gameModel.numberOfLevels, + numButtonRows: 2, + controlsInset: AreaBuilderSharedConstants.CONTROLS_INSET + } + ); + this.rootNode.addChild( this.startGameLevelNode ); + + // Set up the constant portions of the challenge view. + this.shapeBoard = new ShapePlacementBoardNode( gameModel.simSpecificModel.shapePlacementBoard ); + this.shapeBoardOriginalBounds = this.shapeBoard.bounds.copy(); // Necessary because the shape board's bounds can vary when shapes are placed. + this.maxShapeBoardTextWidth = this.shapeBoardOriginalBounds.width * 0.9; + this.yourGoalTitle = new Text( yourGoalString, { + font: new PhetFont( { size: 24, weight: 'bold' } ), + maxWidth: this.maxShapeBoardTextWidth + } ); + this.challengeLayer.addChild( this.shapeBoard ); + this.eraserButton = new EraserButton( { + right: this.shapeBoard.left, + top: this.shapeBoard.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, + touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, + touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, + listener: function() { - // Add layers used to control game appearance. - this.controlLayer = new Node(); - this.rootNode.addChild( this.controlLayer ); - this.challengeLayer = new Node(); - this.rootNode.addChild( this.challengeLayer ); + const challenge = gameModel.currentChallengeProperty.get(); + let shapeReleaseMode = 'fade'; - // Add the node that allows the user to choose a game level to play. - this.startGameLevelNode = new StartGameLevelNode( - function( level ) { - self.numberEntryControl.clear(); - gameModel.startLevel( level ); - }, - function() { gameModel.reset(); }, - gameModel.timerEnabledProperty, - [ - GameIconFactory.createIcon( 1 ), - GameIconFactory.createIcon( 2 ), - GameIconFactory.createIcon( 3 ), - GameIconFactory.createIcon( 4 ), - GameIconFactory.createIcon( 5 ), - GameIconFactory.createIcon( 6 ) - ], - gameModel.bestScoreProperties, - { - numStarsOnButtons: gameModel.challengesPerSet, - perfectScore: gameModel.maxPossibleScore, - numLevels: gameModel.numberOfLevels, - numButtonRows: 2, - controlsInset: AreaBuilderSharedConstants.CONTROLS_INSET - } - ); - this.rootNode.addChild( this.startGameLevelNode ); - - // Set up the constant portions of the challenge view. - this.shapeBoard = new ShapePlacementBoardNode( gameModel.simSpecificModel.shapePlacementBoard ); - this.shapeBoardOriginalBounds = this.shapeBoard.bounds.copy(); // Necessary because the shape board's bounds can vary when shapes are placed. - this.maxShapeBoardTextWidth = this.shapeBoardOriginalBounds.width * 0.9; - this.yourGoalTitle = new Text( yourGoalString, { - font: new PhetFont( { size: 24, weight: 'bold' } ), - maxWidth: this.maxShapeBoardTextWidth - } ); - this.challengeLayer.addChild( this.shapeBoard ); - this.eraserButton = new EraserButton( { - right: this.shapeBoard.left, - top: this.shapeBoard.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, - touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, - touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, - listener: function() { - - const challenge = gameModel.currentChallengeProperty.get(); - let shapeReleaseMode = 'fade'; - - if ( challenge.checkSpec === 'areaEntered' && challenge.userShapes && challenge.userShapes[ 0 ].creationLimit ) { - - // In the case where there is a limited number of shapes, have them animate back to the carousel instead of - // fading away so that the user understands that the stash is being replenished. - shapeReleaseMode = 'animateHome'; - } - gameModel.simSpecificModel.shapePlacementBoard.releaseAllShapes( shapeReleaseMode ); + if ( challenge.checkSpec === 'areaEntered' && challenge.userShapes && challenge.userShapes[ 0 ].creationLimit ) { - // If the game was showing the user incorrect feedback when they pressed this button, auto-advance to the - // next state. - if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { - self.numberEntryControl.clear(); - gameModel.tryAgain(); - } + // In the case where there is a limited number of shapes, have them animate back to the carousel instead of + // fading away so that the user understands that the stash is being replenished. + shapeReleaseMode = 'animateHome'; } - } ); - this.challengeLayer.addChild( this.eraserButton ); - this.youBuiltWindow = new YouBuiltWindow( this.layoutBounds.width - this.shapeBoard.right - 14 ); - this.challengeLayer.addChild( this.youBuiltWindow ); - this.youEnteredWindow = new YouEnteredWindow( this.layoutBounds.width - this.shapeBoard.right - 14 ); - this.challengeLayer.addChild( this.youEnteredWindow ); - this.challengePromptBanner = new GameInfoBanner( this.shapeBoard.width, INFO_BANNER_HEIGHT, '#1b1464', { - left: this.shapeBoard.left, - bottom: this.shapeBoard.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD - } ); - this.challengeLayer.addChild( this.challengePromptBanner ); - this.solutionBanner = new GameInfoBanner( this.shapeBoard.width, INFO_BANNER_HEIGHT, '#fbb03b', { - left: this.shapeBoard.left, - bottom: this.shapeBoard.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD - } ); - this.challengeLayer.addChild( this.solutionBanner ); + gameModel.simSpecificModel.shapePlacementBoard.releaseAllShapes( shapeReleaseMode ); - // Add the control panel - this.controlPanel = new AreaBuilderControlPanel( - gameModel.simSpecificModel.showGridOnBoardProperty, - gameModel.simSpecificModel.showDimensionsProperty, - { centerX: ( this.layoutBounds.x + this.shapeBoard.left ) / 2, bottom: this.shapeBoard.bottom } - ); - this.controlLayer.addChild( this.controlPanel ); - - // Add the scoreboard. - this.scoreboard = new AreaBuilderScoreboard( - gameModel.levelProperty, - gameModel.challengeIndexProperty, - gameModel.challengesPerSet, - gameModel.scoreProperty, - gameModel.elapsedTimeProperty, - { - centerX: ( this.layoutBounds.x + this.shapeBoard.left ) / 2, - top: this.shapeBoard.top, - maxWidth: this.controlPanel.width + // If the game was showing the user incorrect feedback when they pressed this button, auto-advance to the + // next state. + if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { + self.numberEntryControl.clear(); + gameModel.tryAgain(); } - ); - this.controlLayer.addChild( this.scoreboard ); + } + } ); + this.challengeLayer.addChild( this.eraserButton ); + this.youBuiltWindow = new YouBuiltWindow( this.layoutBounds.width - this.shapeBoard.right - 14 ); + this.challengeLayer.addChild( this.youBuiltWindow ); + this.youEnteredWindow = new YouEnteredWindow( this.layoutBounds.width - this.shapeBoard.right - 14 ); + this.challengeLayer.addChild( this.youEnteredWindow ); + this.challengePromptBanner = new GameInfoBanner( this.shapeBoard.width, INFO_BANNER_HEIGHT, '#1b1464', { + left: this.shapeBoard.left, + bottom: this.shapeBoard.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD + } ); + this.challengeLayer.addChild( this.challengePromptBanner ); + this.solutionBanner = new GameInfoBanner( this.shapeBoard.width, INFO_BANNER_HEIGHT, '#fbb03b', { + left: this.shapeBoard.left, + bottom: this.shapeBoard.top - SPACE_AROUND_SHAPE_PLACEMENT_BOARD + } ); + this.challengeLayer.addChild( this.solutionBanner ); + + // Add the control panel + this.controlPanel = new AreaBuilderControlPanel( + gameModel.simSpecificModel.showGridOnBoardProperty, + gameModel.simSpecificModel.showDimensionsProperty, + { centerX: ( this.layoutBounds.x + this.shapeBoard.left ) / 2, bottom: this.shapeBoard.bottom } + ); + this.controlLayer.addChild( this.controlPanel ); + + // Add the scoreboard. + this.scoreboard = new AreaBuilderScoreboard( + gameModel.levelProperty, + gameModel.challengeIndexProperty, + gameModel.challengesPerSet, + gameModel.scoreProperty, + gameModel.elapsedTimeProperty, + { + centerX: ( this.layoutBounds.x + this.shapeBoard.left ) / 2, + top: this.shapeBoard.top, + maxWidth: this.controlPanel.width + } + ); + this.controlLayer.addChild( this.scoreboard ); - // Control visibility of elapsed time indicator in the scoreboard. - this.model.timerEnabledProperty.link( function( timerEnabled ) { - self.scoreboard.timeVisibleProperty.set( timerEnabled ); - } ); + // Control visibility of elapsed time indicator in the scoreboard. + this.model.timerEnabledProperty.link( function( timerEnabled ) { + self.scoreboard.timeVisibleProperty.set( timerEnabled ); + } ); - // Add the button for returning to the level selection screen. - this.controlLayer.addChild( new RectangularPushButton( { - content: new Text( startOverString, { font: BUTTON_FONT, maxWidth: this.controlPanel.width } ), - touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, - touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, - listener: function() { - gameModel.simSpecificModel.reset(); - gameModel.setChoosingLevelState(); - }, - baseColor: BUTTON_FILL, - centerX: this.scoreboard.centerX, - centerY: this.solutionBanner.centerY - } ) ); - - // Add the 'Build Prompt' node that is shown temporarily over the board to instruct the user about what to build. - this.buildPromptVBox = new VBox( { - children: [ - this.yourGoalTitle - ], - spacing: 20 - } ); - this.buildPromptPanel = new Panel( this.buildPromptVBox, { - stroke: null, - xMargin: 10, - yMargin: 10 - } ); - this.challengeLayer.addChild( this.buildPromptPanel ); - - // Define some variables for taking a snapshot of the user's solution. - this.areaOfUserCreatedShape = 0; - this.perimeterOfUserCreatedShape = 0; - this.color1Proportion = null; - - // Add and lay out the game control buttons. - this.gameControlButtons = []; - const buttonOptions = { - font: BUTTON_FONT, - baseColor: BUTTON_FILL, - cornerRadius: 4, - touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, - touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, - maxWidth: ( this.layoutBounds.maxX - this.shapeBoardOriginalBounds.maxX ) * 0.9 - }; - this.checkAnswerButton = new TextPushButton( checkString, merge( { - listener: function() { - self.updateUserAnswer(); - gameModel.checkAnswer(); - } - }, buttonOptions ) ); - this.gameControlButtons.push( this.checkAnswerButton ); + // Add the button for returning to the level selection screen. + this.controlLayer.addChild( new RectangularPushButton( { + content: new Text( startOverString, { font: BUTTON_FONT, maxWidth: this.controlPanel.width } ), + touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, + touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, + listener: function() { + gameModel.simSpecificModel.reset(); + gameModel.setChoosingLevelState(); + }, + baseColor: BUTTON_FILL, + centerX: this.scoreboard.centerX, + centerY: this.solutionBanner.centerY + } ) ); + + // Add the 'Build Prompt' node that is shown temporarily over the board to instruct the user about what to build. + this.buildPromptVBox = new VBox( { + children: [ + this.yourGoalTitle + ], + spacing: 20 + } ); + this.buildPromptPanel = new Panel( this.buildPromptVBox, { + stroke: null, + xMargin: 10, + yMargin: 10 + } ); + this.challengeLayer.addChild( this.buildPromptPanel ); + + // Define some variables for taking a snapshot of the user's solution. + this.areaOfUserCreatedShape = 0; + this.perimeterOfUserCreatedShape = 0; + this.color1Proportion = null; + + // Add and lay out the game control buttons. + this.gameControlButtons = []; + const buttonOptions = { + font: BUTTON_FONT, + baseColor: BUTTON_FILL, + cornerRadius: 4, + touchAreaXDilation: BUTTON_TOUCH_AREA_DILATION, + touchAreaYDilation: BUTTON_TOUCH_AREA_DILATION, + maxWidth: ( this.layoutBounds.maxX - this.shapeBoardOriginalBounds.maxX ) * 0.9 + }; + this.checkAnswerButton = new TextPushButton( checkString, merge( { + listener: function() { + self.updateUserAnswer(); + gameModel.checkAnswer(); + } + }, buttonOptions ) ); + this.gameControlButtons.push( this.checkAnswerButton ); - this.nextButton = new TextPushButton( nextString, merge( { - listener: function() { - self.numberEntryControl.clear(); - gameModel.nextChallenge(); - } - }, buttonOptions ) ); - this.gameControlButtons.push( this.nextButton ); + this.nextButton = new TextPushButton( nextString, merge( { + listener: function() { + self.numberEntryControl.clear(); + gameModel.nextChallenge(); + } + }, buttonOptions ) ); + this.gameControlButtons.push( this.nextButton ); - this.tryAgainButton = new TextPushButton( tryAgainString, merge( { - listener: function() { - self.numberEntryControl.clear(); - gameModel.tryAgain(); - } - }, buttonOptions ) ); - this.gameControlButtons.push( this.tryAgainButton ); + this.tryAgainButton = new TextPushButton( tryAgainString, merge( { + listener: function() { + self.numberEntryControl.clear(); + gameModel.tryAgain(); + } + }, buttonOptions ) ); + this.gameControlButtons.push( this.tryAgainButton ); - // Solution button for 'find the area' style of challenge, which has one specific answer. - this.solutionButton = new TextPushButton( solutionString, merge( { - listener: function() { - gameModel.displayCorrectAnswer(); - } - }, buttonOptions ) ); - this.gameControlButtons.push( this.solutionButton ); - - // Solution button for 'build it' style of challenge, which has many potential answers. - this.showASolutionButton = new TextPushButton( aSolutionString, merge( { - listener: function() { - self.okayToUpdateYouBuiltWindow = false; - gameModel.displayCorrectAnswer(); - } - }, buttonOptions ) ); - this.gameControlButtons.push( this.showASolutionButton ); - - const buttonCenterX = ( this.layoutBounds.width + this.shapeBoard.right ) / 2; - const buttonBottom = this.shapeBoard.bottom; - this.gameControlButtons.forEach( function( button ) { - button.centerX = buttonCenterX; - button.bottom = buttonBottom; - self.controlLayer.addChild( button ); - } ); + // Solution button for 'find the area' style of challenge, which has one specific answer. + this.solutionButton = new TextPushButton( solutionString, merge( { + listener: function() { + gameModel.displayCorrectAnswer(); + } + }, buttonOptions ) ); + this.gameControlButtons.push( this.solutionButton ); + + // Solution button for 'build it' style of challenge, which has many potential answers. + this.showASolutionButton = new TextPushButton( aSolutionString, merge( { + listener: function() { + self.okayToUpdateYouBuiltWindow = false; + gameModel.displayCorrectAnswer(); + } + }, buttonOptions ) ); + this.gameControlButtons.push( this.showASolutionButton ); + + const buttonCenterX = ( this.layoutBounds.width + this.shapeBoard.right ) / 2; + const buttonBottom = this.shapeBoard.bottom; + this.gameControlButtons.forEach( function( button ) { + button.centerX = buttonCenterX; + button.bottom = buttonBottom; + self.controlLayer.addChild( button ); + } ); - // Add the number entry control, which is only visible on certain challenge types. - this.numberEntryControl = new NumberEntryControl( { - centerX: buttonCenterX, - bottom: this.checkAnswerButton.top - 10 - } ); - this.challengeLayer.addChild( this.numberEntryControl ); - this.areaQuestionPrompt = new Text( areaQuestionString, { // This prompt goes with the number entry control. - font: new PhetFont( 20 ), - centerX: this.numberEntryControl.centerX, - bottom: this.numberEntryControl.top - 10, - maxWidth: this.numberEntryControl.width - } ); - this.challengeLayer.addChild( this.areaQuestionPrompt ); + // Add the number entry control, which is only visible on certain challenge types. + this.numberEntryControl = new NumberEntryControl( { + centerX: buttonCenterX, + bottom: this.checkAnswerButton.top - 10 + } ); + this.challengeLayer.addChild( this.numberEntryControl ); + this.areaQuestionPrompt = new Text( areaQuestionString, { // This prompt goes with the number entry control. + font: new PhetFont( 20 ), + centerX: this.numberEntryControl.centerX, + bottom: this.numberEntryControl.top - 10, + maxWidth: this.numberEntryControl.width + } ); + this.challengeLayer.addChild( this.areaQuestionPrompt ); - this.numberEntryControl.keypad.valueStringProperty.link( function( valueString ) { + this.numberEntryControl.keypad.valueStringProperty.link( function( valueString ) { - // Handle the case where the user just starts entering digits instead of pressing the "Try Again" button. In - // this case, we go ahead and make the state transition to the next state. - if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { - gameModel.tryAgain(); - } + // Handle the case where the user just starts entering digits instead of pressing the "Try Again" button. In + // this case, we go ahead and make the state transition to the next state. + if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { + gameModel.tryAgain(); + } - // Update the state of the 'Check' button when the user enters new digits. - self.updatedCheckButtonEnabledState(); - } ); + // Update the state of the 'Check' button when the user enters new digits. + self.updatedCheckButtonEnabledState(); + } ); - // Add the 'feedback node' that is used to visually indicate correct and incorrect answers. - this.faceWithPointsNode = new FaceWithPointsNode( { - faceDiameter: 85, - pointsAlignment: 'rightBottom', - centerX: buttonCenterX, - top: buttonBottom + 20, - pointsFont: new PhetFont( { size: 20, weight: 'bold' } ) - } ); - this.addChild( this.faceWithPointsNode ); + // Add the 'feedback node' that is used to visually indicate correct and incorrect answers. + this.faceWithPointsNode = new FaceWithPointsNode( { + faceDiameter: 85, + pointsAlignment: 'rightBottom', + centerX: buttonCenterX, + top: buttonBottom + 20, + pointsFont: new PhetFont( { size: 20, weight: 'bold' } ) + } ); + this.addChild( this.faceWithPointsNode ); - // Handle comings and goings of model shapes. - gameModel.simSpecificModel.movableShapes.addItemAddedListener( function( addedShape ) { + // Handle comings and goings of model shapes. + gameModel.simSpecificModel.movableShapes.addItemAddedListener( function( addedShape ) { - // Create and add the view representation for this shape. - const shapeNode = new ShapeNode( addedShape, self.layoutBounds ); - self.challengeLayer.addChild( shapeNode ); + // Create and add the view representation for this shape. + const shapeNode = new ShapeNode( addedShape, self.layoutBounds ); + self.challengeLayer.addChild( shapeNode ); - // Add a listener that handles changes to the userControlled state. - const userControlledListener = function( userControlled ) { - if ( userControlled ) { - shapeNode.moveToFront(); + // Add a listener that handles changes to the userControlled state. + const userControlledListener = function( userControlled ) { + if ( userControlled ) { + shapeNode.moveToFront(); - // If the game was in the state where it was prompting the user to try again, and the user started - // interacting with shapes without pressing the 'Try Again' button, go ahead and make the state change - // automatically. - if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { - gameModel.tryAgain(); - } - } - }; - addedShape.userControlledProperty.link( userControlledListener ); - - // Add the removal listener for if and when this shape is removed from the model. - gameModel.simSpecificModel.movableShapes.addItemRemovedListener( function removalListener( removedShape ) { - if ( removedShape === addedShape ) { - self.challengeLayer.removeChild( shapeNode ); - addedShape.userControlledProperty.unlink( userControlledListener ); - gameModel.simSpecificModel.movableShapes.removeItemRemovedListener( removalListener ); + // If the game was in the state where it was prompting the user to try again, and the user started + // interacting with shapes without pressing the 'Try Again' button, go ahead and make the state change + // automatically. + if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN ) { + gameModel.tryAgain(); } - } ); - - // If the initial build prompt is visible, hide it. - if ( self.buildPromptPanel.opacity === 1 ) { - // using a function instead, see Seasons sim, PanelNode.js for an example. - new Animation( { - from: self.buildPromptPanel.opacity, - to: 0, - setValue: opacity => { self.buildPromptPanel.opacity = opacity; }, - duration: 0.5, - easing: Easing.CUBIC_IN_OUT - } ).start(); - } - - // If this is a 'built it' style challenge, and this is the first element being added to the board, add the - // build spec to the banner so that the user can reference it as they add more shapes to the board. - if ( gameModel.currentChallengeProperty.get().buildSpec && self.challengePromptBanner.buildSpecProperty.value === null ) { - self.challengePromptBanner.buildSpecProperty.value = gameModel.currentChallengeProperty.get().buildSpec; } - } ); + }; + addedShape.userControlledProperty.link( userControlledListener ); - gameModel.simSpecificModel.movableShapes.addItemRemovedListener( function() { - // If the challenge is a 'build it' style challenge, and the game is in the state where the user is being given - // the opportunity to view a solution, and the user just removed a piece, check if they now have the correct - // answer. - if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON && !self.isAnyShapeMoving() ) { - self.model.checkAnswer(); + // Add the removal listener for if and when this shape is removed from the model. + gameModel.simSpecificModel.movableShapes.addItemRemovedListener( function removalListener( removedShape ) { + if ( removedShape === addedShape ) { + self.challengeLayer.removeChild( shapeNode ); + addedShape.userControlledProperty.unlink( userControlledListener ); + gameModel.simSpecificModel.movableShapes.removeItemRemovedListener( removalListener ); } } ); - gameModel.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.link( function( areaAndPerimeter ) { - - self.updatedCheckButtonEnabledState(); - - // If the challenge is a 'build it' style challenge, and the game is in the state where the user is being - // given the opportunity to view a solution, and they just changed what they had built, update the 'you built' - // window. - if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON && - self.model.currentChallengeProperty.get().buildSpec && - self.okayToUpdateYouBuiltWindow ) { - self.updateUserAnswer(); - self.updateYouBuiltWindow( self.model.currentChallengeProperty.get() ); + // If the initial build prompt is visible, hide it. + if ( self.buildPromptPanel.opacity === 1 ) { + // using a function instead, see Seasons sim, PanelNode.js for an example. + new Animation( { + from: self.buildPromptPanel.opacity, + to: 0, + setValue: opacity => { self.buildPromptPanel.opacity = opacity; }, + duration: 0.5, + easing: Easing.CUBIC_IN_OUT + } ).start(); + } - // If the user has put all shapes away, check to see if they now have the correct answer. - if ( !self.isAnyShapeMoving() ) { - self.model.checkAnswer(); - } - } - } ); + // If this is a 'built it' style challenge, and this is the first element being added to the board, add the + // build spec to the banner so that the user can reference it as they add more shapes to the board. + if ( gameModel.currentChallengeProperty.get().buildSpec && self.challengePromptBanner.buildSpecProperty.value === null ) { + self.challengePromptBanner.buildSpecProperty.value = gameModel.currentChallengeProperty.get().buildSpec; + } + } ); - // Various other initialization - this.levelCompletedNode = null; // @private - this.shapeCarouselLayer = new Node(); // @private - this.challengeLayer.addChild( this.shapeCarouselLayer ); - this.clearDimensionsControlOnNextChallenge = false; // @private + gameModel.simSpecificModel.movableShapes.addItemRemovedListener( function() { + // If the challenge is a 'build it' style challenge, and the game is in the state where the user is being given + // the opportunity to view a solution, and the user just removed a piece, check if they now have the correct + // answer. + if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON && !self.isAnyShapeMoving() ) { + self.model.checkAnswer(); + } + } ); - // Hook up the update function for handling changes to game state. - gameModel.gameStateProperty.link( self.handleGameStateChange.bind( self ) ); + gameModel.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.link( function( areaAndPerimeter ) { - // Set up a flag to block updates of the 'You Built' window when showing the solution. This is necessary because - // adding the shapes to the board in order to show the solution triggers updates of this window. - this.okayToUpdateYouBuiltWindow = true; // @private - } + self.updatedCheckButtonEnabledState(); - areaBuilder.register( 'AreaBuilderGameView', AreaBuilderGameView ); + // If the challenge is a 'build it' style challenge, and the game is in the state where the user is being + // given the opportunity to view a solution, and they just changed what they had built, update the 'you built' + // window. + if ( gameModel.gameStateProperty.value === GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON && + self.model.currentChallengeProperty.get().buildSpec && + self.okayToUpdateYouBuiltWindow ) { + self.updateUserAnswer(); + self.updateYouBuiltWindow( self.model.currentChallengeProperty.get() ); - return inherit( ScreenView, AreaBuilderGameView, { + // If the user has put all shapes away, check to see if they now have the correct answer. + if ( !self.isAnyShapeMoving() ) { + self.model.checkAnswer(); + } + } + } ); - // @private, When the game state changes, update the view with the appropriate buttons and readouts. - handleGameStateChange: function( gameState ) { + // Various other initialization + this.levelCompletedNode = null; // @private + this.shapeCarouselLayer = new Node(); // @private + this.challengeLayer.addChild( this.shapeCarouselLayer ); + this.clearDimensionsControlOnNextChallenge = false; // @private - // Hide all nodes - the appropriate ones will be shown later based on the current state. - this.hideAllGameNodes(); + // Hook up the update function for handling changes to game state. + gameModel.gameStateProperty.link( self.handleGameStateChange.bind( self ) ); - const challenge = this.model.currentChallengeProperty.get(); // convenience var + // Set up a flag to block updates of the 'You Built' window when showing the solution. This is necessary because + // adding the shapes to the board in order to show the solution triggers updates of this window. + this.okayToUpdateYouBuiltWindow = true; // @private +} - // Show the nodes appropriate to the state - switch( gameState ) { +areaBuilder.register( 'AreaBuilderGameView', AreaBuilderGameView ); - case GameState.CHOOSING_LEVEL: - this.handleChoosingLevelState(); - break; +export default inherit( ScreenView, AreaBuilderGameView, { - case GameState.PRESENTING_INTERACTIVE_CHALLENGE: - this.handlePresentingInteractiveChallengeState( challenge ); - break; + // @private, When the game state changes, update the view with the appropriate buttons and readouts. + handleGameStateChange: function( gameState ) { - case GameState.SHOWING_CORRECT_ANSWER_FEEDBACK: - this.handleShowingCorrectAnswerFeedbackState( challenge ); - break; + // Hide all nodes - the appropriate ones will be shown later based on the current state. + this.hideAllGameNodes(); - case GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN: - this.handleShowingIncorrectAnswerFeedbackTryAgainState( challenge ); - break; + const challenge = this.model.currentChallengeProperty.get(); // convenience var - case GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON: - this.handleShowingIncorrectAnswerFeedbackMoveOnState( challenge ); - break; + // Show the nodes appropriate to the state + switch( gameState ) { - case GameState.DISPLAYING_CORRECT_ANSWER: - this.handleDisplayingCorrectAnswerState( challenge ); - break; + case GameState.CHOOSING_LEVEL: + this.handleChoosingLevelState(); + break; - case GameState.SHOWING_LEVEL_RESULTS: - this.handleShowingLevelResultsState(); - break; + case GameState.PRESENTING_INTERACTIVE_CHALLENGE: + this.handlePresentingInteractiveChallengeState( challenge ); + break; - default: - throw new Error( 'Unhandled game state: ' + gameState ); - } - }, + case GameState.SHOWING_CORRECT_ANSWER_FEEDBACK: + this.handleShowingCorrectAnswerFeedbackState( challenge ); + break; - // @private - handleChoosingLevelState: function() { - this.show( [ this.startGameLevelNode ] ); - this.hideChallenge(); - }, + case GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_TRY_AGAIN: + this.handleShowingIncorrectAnswerFeedbackTryAgainState( challenge ); + break; - // @private - handlePresentingInteractiveChallengeState: function( challenge ) { - this.challengeLayer.pickable = null; // Pass through, prunes subtree, see Scenery documentation for details. - this.presentChallenge(); - - // Make a list of the nodes to be shown in this state. - const nodesToShow = [ - this.scoreboard, - this.controlPanel, - this.checkAnswerButton, - this.challengePromptBanner - ]; - - // Add the nodes that are only shown for certain challenge types or under certain conditions. - if ( challenge.checkSpec === 'areaEntered' ) { - nodesToShow.push( this.numberEntryControl ); - nodesToShow.push( this.areaQuestionPrompt ); - } - if ( challenge.userShapes ) { - nodesToShow.push( this.shapeCarouselLayer ); - nodesToShow.push( this.eraserButton ); - } + case GameState.SHOWING_INCORRECT_ANSWER_FEEDBACK_MOVE_ON: + this.handleShowingIncorrectAnswerFeedbackMoveOnState( challenge ); + break; - this.show( nodesToShow ); - this.showChallengeGraphics(); - this.updatedCheckButtonEnabledState(); - this.okayToUpdateYouBuiltWindow = true; + case GameState.DISPLAYING_CORRECT_ANSWER: + this.handleDisplayingCorrectAnswerState( challenge ); + break; - if ( this.clearDimensionsControlOnNextChallenge ) { - this.model.simSpecificModel.showDimensionsProperty.set( false ); - this.clearDimensionsControlOnNextChallenge = false; - } - }, + case GameState.SHOWING_LEVEL_RESULTS: + this.handleShowingLevelResultsState(); + break; - // @private - handleShowingCorrectAnswerFeedbackState: function( challenge ) { + default: + throw new Error( 'Unhandled game state: ' + gameState ); + } + }, + + // @private + handleChoosingLevelState: function() { + this.show( [ this.startGameLevelNode ] ); + this.hideChallenge(); + }, + + // @private + handlePresentingInteractiveChallengeState: function( challenge ) { + this.challengeLayer.pickable = null; // Pass through, prunes subtree, see Scenery documentation for details. + this.presentChallenge(); + + // Make a list of the nodes to be shown in this state. + const nodesToShow = [ + this.scoreboard, + this.controlPanel, + this.checkAnswerButton, + this.challengePromptBanner + ]; + + // Add the nodes that are only shown for certain challenge types or under certain conditions. + if ( challenge.checkSpec === 'areaEntered' ) { + nodesToShow.push( this.numberEntryControl ); + nodesToShow.push( this.areaQuestionPrompt ); + } + if ( challenge.userShapes ) { + nodesToShow.push( this.shapeCarouselLayer ); + nodesToShow.push( this.eraserButton ); + } - // Make a list of the nodes to be shown in this state. - const nodesToShow = [ - this.scoreboard, - this.controlPanel, - this.nextButton, - this.challengePromptBanner, - this.faceWithPointsNode - ]; + this.show( nodesToShow ); + this.showChallengeGraphics(); + this.updatedCheckButtonEnabledState(); + this.okayToUpdateYouBuiltWindow = true; - // Update and show the nodes that vary based on the challenge configurations. - if ( challenge.buildSpec ) { - this.updateYouBuiltWindow( challenge ); - nodesToShow.push( this.youBuiltWindow ); - } - else { - this.updateYouEnteredWindow( challenge ); - nodesToShow.push( this.youEnteredWindow ); - } + if ( this.clearDimensionsControlOnNextChallenge ) { + this.model.simSpecificModel.showDimensionsProperty.set( false ); + this.clearDimensionsControlOnNextChallenge = false; + } + }, + + // @private + handleShowingCorrectAnswerFeedbackState: function( challenge ) { + + // Make a list of the nodes to be shown in this state. + const nodesToShow = [ + this.scoreboard, + this.controlPanel, + this.nextButton, + this.challengePromptBanner, + this.faceWithPointsNode + ]; + + // Update and show the nodes that vary based on the challenge configurations. + if ( challenge.buildSpec ) { + this.updateYouBuiltWindow( challenge ); + nodesToShow.push( this.youBuiltWindow ); + } + else { + this.updateYouEnteredWindow( challenge ); + nodesToShow.push( this.youEnteredWindow ); + } - // Give the user the appropriate audio and visual feedback - this.gameAudioPlayer.correctAnswer(); - this.faceWithPointsNode.smile(); - this.faceWithPointsNode.setPoints( this.model.getChallengeCurrentPointValue() ); + // Give the user the appropriate audio and visual feedback + this.gameAudioPlayer.correctAnswer(); + this.faceWithPointsNode.smile(); + this.faceWithPointsNode.setPoints( this.model.getChallengeCurrentPointValue() ); + + // Disable interaction with the challenge elements. + this.challengeLayer.pickable = false; + + // Make the nodes visible + this.show( nodesToShow ); + }, + + // @private + handleShowingIncorrectAnswerFeedbackTryAgainState: function( challenge ) { + + // Make a list of the nodes to be shown in this state. + const nodesToShow = [ + this.scoreboard, + this.controlPanel, + this.tryAgainButton, + this.challengePromptBanner, + this.faceWithPointsNode + ]; + + // Add the nodes whose visibility varies based on the challenge configuration. + if ( challenge.checkSpec === 'areaEntered' ) { + nodesToShow.push( this.numberEntryControl ); + nodesToShow.push( this.areaQuestionPrompt ); + } + if ( challenge.userShapes ) { + nodesToShow.push( this.shapeCarouselLayer ); + nodesToShow.push( this.eraserButton ); + } - // Disable interaction with the challenge elements. - this.challengeLayer.pickable = false; + // Give the user the appropriate feedback. + this.gameAudioPlayer.wrongAnswer(); + this.faceWithPointsNode.frown(); + this.faceWithPointsNode.setPoints( this.model.scoreProperty.get() ); - // Make the nodes visible - this.show( nodesToShow ); - }, + if ( challenge.checkSpec === 'areaEntered' ) { + // Set the keypad to allow the user to start entering a new value. + this.numberEntryControl.setClearOnNextKeyPress( true ); + } - // @private - handleShowingIncorrectAnswerFeedbackTryAgainState: function( challenge ) { - - // Make a list of the nodes to be shown in this state. - const nodesToShow = [ - this.scoreboard, - this.controlPanel, - this.tryAgainButton, - this.challengePromptBanner, - this.faceWithPointsNode - ]; - - // Add the nodes whose visibility varies based on the challenge configuration. - if ( challenge.checkSpec === 'areaEntered' ) { - nodesToShow.push( this.numberEntryControl ); - nodesToShow.push( this.areaQuestionPrompt ); - } + // Show the nodes + this.show( nodesToShow ); + }, + + // @private + handleShowingIncorrectAnswerFeedbackMoveOnState: function( challenge ) { + + // Make a list of the nodes to be shown in this state. + const nodesToShow = [ + this.scoreboard, + this.controlPanel, + this.challengePromptBanner, + this.faceWithPointsNode + ]; + + // Add the nodes whose visibility varies based on the challenge configuration. + if ( challenge.buildSpec ) { + nodesToShow.push( this.showASolutionButton ); + this.updateYouBuiltWindow( challenge ); + nodesToShow.push( this.youBuiltWindow ); if ( challenge.userShapes ) { nodesToShow.push( this.shapeCarouselLayer ); nodesToShow.push( this.eraserButton ); } + } + else { + nodesToShow.push( this.solutionButton ); + this.updateYouEnteredWindow( challenge ); + nodesToShow.push( this.youEnteredWindow ); + } - // Give the user the appropriate feedback. - this.gameAudioPlayer.wrongAnswer(); - this.faceWithPointsNode.frown(); - this.faceWithPointsNode.setPoints( this.model.scoreProperty.get() ); - - if ( challenge.checkSpec === 'areaEntered' ) { - // Set the keypad to allow the user to start entering a new value. - this.numberEntryControl.setClearOnNextKeyPress( true ); - } - - // Show the nodes - this.show( nodesToShow ); - }, - - // @private - handleShowingIncorrectAnswerFeedbackMoveOnState: function( challenge ) { - - // Make a list of the nodes to be shown in this state. - const nodesToShow = [ - this.scoreboard, - this.controlPanel, - this.challengePromptBanner, - this.faceWithPointsNode - ]; - - // Add the nodes whose visibility varies based on the challenge configuration. - if ( challenge.buildSpec ) { - nodesToShow.push( this.showASolutionButton ); - this.updateYouBuiltWindow( challenge ); - nodesToShow.push( this.youBuiltWindow ); - if ( challenge.userShapes ) { - nodesToShow.push( this.shapeCarouselLayer ); - nodesToShow.push( this.eraserButton ); - } - } - else { - nodesToShow.push( this.solutionButton ); - this.updateYouEnteredWindow( challenge ); - nodesToShow.push( this.youEnteredWindow ); - } - - this.show( nodesToShow ); - - // Give the user the appropriate feedback - this.gameAudioPlayer.wrongAnswer(); - this.faceWithPointsNode.frown(); - this.faceWithPointsNode.setPoints( this.model.scoreProperty.get() ); + this.show( nodesToShow ); - // For 'built it' style challenges, the user can still interact while in this state in case they want to try - // to get it right. In 'find the area' challenges, further interaction is disallowed. - if ( challenge.checkSpec === 'areaEntered' ) { - this.challengeLayer.pickable = false; - } + // Give the user the appropriate feedback + this.gameAudioPlayer.wrongAnswer(); + this.faceWithPointsNode.frown(); + this.faceWithPointsNode.setPoints( this.model.scoreProperty.get() ); - // Show the nodes. - this.show( nodesToShow ); - }, - - // @private - handleDisplayingCorrectAnswerState: function( challenge ) { - // Make a list of the nodes to be shown in this state. - const nodesToShow = [ - this.scoreboard, - this.controlPanel, - this.nextButton, - this.solutionBanner - ]; - - // Keep the appropriate feedback window visible. - if ( challenge.buildSpec ) { - nodesToShow.push( this.youBuiltWindow ); - } - else { - nodesToShow.push( this.youEnteredWindow ); - } - - // Update the solution banner. - this.solutionBanner.reset(); - if ( challenge.buildSpec ) { - this.solutionBanner.titleTextProperty.value = aSolutionColonString; - this.solutionBanner.buildSpecProperty.value = challenge.buildSpec; - } - else { - this.solutionBanner.titleTextProperty.value = solutionColonString; - this.solutionBanner.areaToFindProperty.value = challenge.backgroundShape.unitArea; - } - this.showChallengeGraphics(); - - // Disable interaction with the challenge elements. + // For 'built it' style challenges, the user can still interact while in this state in case they want to try + // to get it right. In 'find the area' challenges, further interaction is disallowed. + if ( challenge.checkSpec === 'areaEntered' ) { this.challengeLayer.pickable = false; + } - // Turn on the dimensions indicator, since it may make the answer more clear for the user. - this.clearDimensionsControlOnNextChallenge = !this.model.simSpecificModel.showDimensionsProperty.get(); - this.model.simSpecificModel.showDimensionsProperty.set( true ); + // Show the nodes. + this.show( nodesToShow ); + }, + + // @private + handleDisplayingCorrectAnswerState: function( challenge ) { + // Make a list of the nodes to be shown in this state. + const nodesToShow = [ + this.scoreboard, + this.controlPanel, + this.nextButton, + this.solutionBanner + ]; + + // Keep the appropriate feedback window visible. + if ( challenge.buildSpec ) { + nodesToShow.push( this.youBuiltWindow ); + } + else { + nodesToShow.push( this.youEnteredWindow ); + } - // Show the nodes. - this.show( nodesToShow ); - }, + // Update the solution banner. + this.solutionBanner.reset(); + if ( challenge.buildSpec ) { + this.solutionBanner.titleTextProperty.value = aSolutionColonString; + this.solutionBanner.buildSpecProperty.value = challenge.buildSpec; + } + else { + this.solutionBanner.titleTextProperty.value = solutionColonString; + this.solutionBanner.areaToFindProperty.value = challenge.backgroundShape.unitArea; + } + this.showChallengeGraphics(); - // @private - handleShowingLevelResultsState: function() { - if ( this.model.scoreProperty.get() === this.model.maxPossibleScore ) { - this.gameAudioPlayer.gameOverPerfectScore(); - } - else if ( this.model.scoreProperty.get() === 0 ) { - this.gameAudioPlayer.gameOverZeroScore(); - } - else { - this.gameAudioPlayer.gameOverImperfectScore(); - } + // Disable interaction with the challenge elements. + this.challengeLayer.pickable = false; - this.showLevelResultsNode(); - this.hideChallenge(); - }, + // Turn on the dimensions indicator, since it may make the answer more clear for the user. + this.clearDimensionsControlOnNextChallenge = !this.model.simSpecificModel.showDimensionsProperty.get(); + this.model.simSpecificModel.showDimensionsProperty.set( true ); - // @private Update the window that depicts what the user has built. - updateYouBuiltWindow: function( challenge ) { - assert && assert( challenge.buildSpec, 'This method should only be called for challenges that include a build spec.' ); - const userBuiltSpec = new BuildSpec( - this.areaOfUserCreatedShape, - challenge.buildSpec.perimeter ? this.perimeterOfUserCreatedShape : null, - challenge.buildSpec.proportions ? { - color1: challenge.buildSpec.proportions.color1, - color2: challenge.buildSpec.proportions.color2, - color1Proportion: this.color1Proportion - } : null - ); - this.youBuiltWindow.setBuildSpec( userBuiltSpec ); - this.youBuiltWindow.setColorBasedOnAnswerCorrectness( userBuiltSpec.equals( challenge.buildSpec ) ); - this.youBuiltWindow.centerY = this.shapeBoardOriginalBounds.centerY; - this.youBuiltWindow.centerX = ( this.layoutBounds.maxX + this.shapeBoardOriginalBounds.maxX ) / 2; - }, + // Show the nodes. + this.show( nodesToShow ); + }, - // @private Update the window that depicts what the user has entered using the keypad. - updateYouEnteredWindow: function( challenge ) { - assert && assert( challenge.checkSpec === 'areaEntered', 'This method should only be called for find-the-area style challenges.' ); - this.youEnteredWindow.setValueEntered( this.model.simSpecificModel.areaGuess ); - this.youEnteredWindow.setColorBasedOnAnswerCorrectness( challenge.backgroundShape.unitArea === this.model.simSpecificModel.areaGuess ); - this.youEnteredWindow.centerY = this.shapeBoardOriginalBounds.centerY; - this.youEnteredWindow.centerX = ( this.layoutBounds.maxX + this.shapeBoardOriginalBounds.maxX ) / 2; - }, + // @private + handleShowingLevelResultsState: function() { + if ( this.model.scoreProperty.get() === this.model.maxPossibleScore ) { + this.gameAudioPlayer.gameOverPerfectScore(); + } + else if ( this.model.scoreProperty.get() === 0 ) { + this.gameAudioPlayer.gameOverZeroScore(); + } + else { + this.gameAudioPlayer.gameOverImperfectScore(); + } - // @private Grab a snapshot of whatever the user has built or entered - updateUserAnswer: function() { - // Save the parameters of what the user has built, if they've built anything. - this.areaOfUserCreatedShape = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().area; - this.perimeterOfUserCreatedShape = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter; - const challenge = this.model.currentChallengeProperty.get(); // convenience var - if ( challenge.buildSpec && challenge.buildSpec.proportions ) { - this.color1Proportion = this.model.simSpecificModel.getProportionOfColor( challenge.buildSpec.proportions.color1 ); - } - else { - this.color1Proportion = null; - } + this.showLevelResultsNode(); + this.hideChallenge(); + }, + + // @private Update the window that depicts what the user has built. + updateYouBuiltWindow: function( challenge ) { + assert && assert( challenge.buildSpec, 'This method should only be called for challenges that include a build spec.' ); + const userBuiltSpec = new BuildSpec( + this.areaOfUserCreatedShape, + challenge.buildSpec.perimeter ? this.perimeterOfUserCreatedShape : null, + challenge.buildSpec.proportions ? { + color1: challenge.buildSpec.proportions.color1, + color2: challenge.buildSpec.proportions.color2, + color1Proportion: this.color1Proportion + } : null + ); + this.youBuiltWindow.setBuildSpec( userBuiltSpec ); + this.youBuiltWindow.setColorBasedOnAnswerCorrectness( userBuiltSpec.equals( challenge.buildSpec ) ); + this.youBuiltWindow.centerY = this.shapeBoardOriginalBounds.centerY; + this.youBuiltWindow.centerX = ( this.layoutBounds.maxX + this.shapeBoardOriginalBounds.maxX ) / 2; + }, + + // @private Update the window that depicts what the user has entered using the keypad. + updateYouEnteredWindow: function( challenge ) { + assert && assert( challenge.checkSpec === 'areaEntered', 'This method should only be called for find-the-area style challenges.' ); + this.youEnteredWindow.setValueEntered( this.model.simSpecificModel.areaGuess ); + this.youEnteredWindow.setColorBasedOnAnswerCorrectness( challenge.backgroundShape.unitArea === this.model.simSpecificModel.areaGuess ); + this.youEnteredWindow.centerY = this.shapeBoardOriginalBounds.centerY; + this.youEnteredWindow.centerX = ( this.layoutBounds.maxX + this.shapeBoardOriginalBounds.maxX ) / 2; + }, + + // @private Grab a snapshot of whatever the user has built or entered + updateUserAnswer: function() { + // Save the parameters of what the user has built, if they've built anything. + this.areaOfUserCreatedShape = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().area; + this.perimeterOfUserCreatedShape = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().perimeter; + const challenge = this.model.currentChallengeProperty.get(); // convenience var + if ( challenge.buildSpec && challenge.buildSpec.proportions ) { + this.color1Proportion = this.model.simSpecificModel.getProportionOfColor( challenge.buildSpec.proportions.color1 ); + } + else { + this.color1Proportion = null; + } - // Submit the user's area guess, if there is one. - this.model.simSpecificModel.areaGuess = this.numberEntryControl.value; - }, + // Submit the user's area guess, if there is one. + this.model.simSpecificModel.areaGuess = this.numberEntryControl.value; + }, - // @private Returns true if any shape is animating or user controlled, false if not. - isAnyShapeMoving: function() { - for ( let i = 0; i < this.model.simSpecificModel.movableShapes.length; i++ ) { - if ( this.model.simSpecificModel.movableShapes.get( i ).animatingProperty.get() || - this.model.simSpecificModel.movableShapes.get( i ).userControlledProperty.get() ) { - return true; - } + // @private Returns true if any shape is animating or user controlled, false if not. + isAnyShapeMoving: function() { + for ( let i = 0; i < this.model.simSpecificModel.movableShapes.length; i++ ) { + if ( this.model.simSpecificModel.movableShapes.get( i ).animatingProperty.get() || + this.model.simSpecificModel.movableShapes.get( i ).userControlledProperty.get() ) { + return true; } - return false; - }, + } + return false; + }, - // @private, Present the challenge to the user and set things up so that they can submit their answer. - presentChallenge: function() { + // @private, Present the challenge to the user and set things up so that they can submit their answer. + presentChallenge: function() { - const self = this; + const self = this; - if ( this.model.incorrectGuessesOnCurrentChallenge === 0 ) { + if ( this.model.incorrectGuessesOnCurrentChallenge === 0 ) { - // Clean up previous challenge. - this.model.simSpecificModel.clearShapePlacementBoard(); - this.challengePromptBanner.reset(); - this.shapeCarouselLayer.removeAllChildren(); + // Clean up previous challenge. + this.model.simSpecificModel.clearShapePlacementBoard(); + this.challengePromptBanner.reset(); + this.shapeCarouselLayer.removeAllChildren(); - const challenge = this.model.currentChallengeProperty.get(); // Convenience var + const challenge = this.model.currentChallengeProperty.get(); // Convenience var - // Set up the challenge prompt banner, which appears above the shape placement board. - this.challengePromptBanner.titleTextProperty.value = challenge.buildSpec ? buildItString : findTheAreaString; + // Set up the challenge prompt banner, which appears above the shape placement board. + this.challengePromptBanner.titleTextProperty.value = challenge.buildSpec ? buildItString : findTheAreaString; - // If needed, set up the goal prompt that will initially appear over the shape placement board (in the z-order). - if ( challenge.buildSpec ) { + // If needed, set up the goal prompt that will initially appear over the shape placement board (in the z-order). + if ( challenge.buildSpec ) { - this.buildPromptVBox.removeAllChildren(); - this.buildPromptVBox.addChild( this.yourGoalTitle ); - const areaGoalNode = new Text( StringUtils.format( areaEqualsString, challenge.buildSpec.area ), { - font: GOAL_PROMPT_FONT, - maxWidth: this.shapeBoardOriginalBounds.width * 0.9 - } ); - if ( challenge.buildSpec.proportions ) { - const areaPrompt = new Node(); - areaPrompt.addChild( areaGoalNode ); - areaGoalNode.text = areaGoalNode.text + ','; - const colorProportionsPrompt = new ColorProportionsPrompt( challenge.buildSpec.proportions.color1, - challenge.buildSpec.proportions.color2, challenge.buildSpec.proportions.color1Proportion, { - font: new PhetFont( { size: 16, weight: 'bold' } ), - left: areaGoalNode.width + 10, - centerY: areaGoalNode.centerY, - maxWidth: this.shapeBoardOriginalBounds.width * 0.9 - } - ); - areaPrompt.addChild( colorProportionsPrompt ); - - // make sure the prompt will fit on the board - important for translatability - if ( areaPrompt.width > this.shapeBoardOriginalBounds.width * 0.9 ) { - areaPrompt.scale( ( this.shapeBoardOriginalBounds.width * 0.9 ) / areaPrompt.width ); + this.buildPromptVBox.removeAllChildren(); + this.buildPromptVBox.addChild( this.yourGoalTitle ); + const areaGoalNode = new Text( StringUtils.format( areaEqualsString, challenge.buildSpec.area ), { + font: GOAL_PROMPT_FONT, + maxWidth: this.shapeBoardOriginalBounds.width * 0.9 + } ); + if ( challenge.buildSpec.proportions ) { + const areaPrompt = new Node(); + areaPrompt.addChild( areaGoalNode ); + areaGoalNode.text = areaGoalNode.text + ','; + const colorProportionsPrompt = new ColorProportionsPrompt( challenge.buildSpec.proportions.color1, + challenge.buildSpec.proportions.color2, challenge.buildSpec.proportions.color1Proportion, { + font: new PhetFont( { size: 16, weight: 'bold' } ), + left: areaGoalNode.width + 10, + centerY: areaGoalNode.centerY, + maxWidth: this.shapeBoardOriginalBounds.width * 0.9 } + ); + areaPrompt.addChild( colorProportionsPrompt ); - this.buildPromptVBox.addChild( areaPrompt ); - } - else { - this.buildPromptVBox.addChild( areaGoalNode ); + // make sure the prompt will fit on the board - important for translatability + if ( areaPrompt.width > this.shapeBoardOriginalBounds.width * 0.9 ) { + areaPrompt.scale( ( this.shapeBoardOriginalBounds.width * 0.9 ) / areaPrompt.width ); } - if ( challenge.buildSpec.perimeter ) { - this.buildPromptVBox.addChild( new Text( StringUtils.format( perimeterEqualsString, challenge.buildSpec.perimeter ), { - font: GOAL_PROMPT_FONT, - maxWidth: this.maxShapeBoardTextWidth - } ) ); - } - - // Center the panel over the shape board and make it visible. - this.buildPromptPanel.centerX = this.shapeBoardOriginalBounds.centerX; - this.buildPromptPanel.centerY = this.shapeBoardOriginalBounds.centerY; - this.buildPromptPanel.visible = true; - this.buildPromptPanel.opacity = 1; // Necessary because the board is set to fade out elsewhere. + this.buildPromptVBox.addChild( areaPrompt ); } else { - this.buildPromptPanel.visible = false; + this.buildPromptVBox.addChild( areaGoalNode ); } - // Set the state of the control panel. - this.controlPanel.dimensionsIcon.setGridVisible( challenge.backgroundShape ? false : true ); - this.controlPanel.gridControlVisibleProperty.set( challenge.toolSpec.gridControl ); - this.controlPanel.dimensionsControlVisibleProperty.set( challenge.toolSpec.dimensionsControl ); - if ( challenge.backgroundShape ) { - this.controlPanel.dimensionsIcon.setColor( challenge.backgroundShape.fillColor ); - } - else if ( challenge.userShapes ) { - this.controlPanel.dimensionsIcon.setColor( challenge.userShapes[ 0 ].color ); - } - else { - this.controlPanel.dimensionsIcon.setColor( AreaBuilderSharedConstants.GREENISH_COLOR ); + if ( challenge.buildSpec.perimeter ) { + this.buildPromptVBox.addChild( new Text( StringUtils.format( perimeterEqualsString, challenge.buildSpec.perimeter ), { + font: GOAL_PROMPT_FONT, + maxWidth: this.maxShapeBoardTextWidth + } ) ); } - // Create the carousel if included as part of this challenge - if ( challenge.userShapes !== null ) { - const creatorNodes = []; - challenge.userShapes.forEach( function( userShapeSpec ) { - const creatorNodeOptions = { - gridSpacing: AreaBuilderGameModel.UNIT_SQUARE_LENGTH, - shapeDragBounds: self.layoutBounds, - nonMovingAncestor: self.shapeCarouselLayer - }; - if ( userShapeSpec.creationLimit ) { - creatorNodeOptions.creationLimit = userShapeSpec.creationLimit; - } - creatorNodes.push( new ShapeCreatorNode( - userShapeSpec.shape, - userShapeSpec.color, - self.model.simSpecificModel.addUserCreatedMovableShape.bind( self.model.simSpecificModel ), - creatorNodeOptions - ) ); - } ); - if ( creatorNodes.length > ITEMS_PER_CAROUSEL_PAGE ) { - // Add a scrolling carousel. - this.shapeCarouselLayer.addChild( new Carousel( creatorNodes, { - orientation: 'horizontal', - itemsPerPage: ITEMS_PER_CAROUSEL_PAGE, - centerX: this.shapeBoardOriginalBounds.centerX, - top: this.shapeBoardOriginalBounds.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, - fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, - hideDisabledButtons: true - } ) ); - } - else { - // Add a non-scrolling panel - const creatorNodeHBox = new HBox( { children: creatorNodes, spacing: 20 } ); - this.shapeCarouselLayer.addChild( new Panel( creatorNodeHBox, { - centerX: this.shapeBoardOriginalBounds.centerX, - top: this.shapeBoardOriginalBounds.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, - xMargin: 50, - yMargin: 15, - fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR - } ) ); - } - } + // Center the panel over the shape board and make it visible. + this.buildPromptPanel.centerX = this.shapeBoardOriginalBounds.centerX; + this.buildPromptPanel.centerY = this.shapeBoardOriginalBounds.centerY; + this.buildPromptPanel.visible = true; + this.buildPromptPanel.opacity = 1; // Necessary because the board is set to fade out elsewhere. + } + else { + this.buildPromptPanel.visible = false; } - }, - - // @private, Utility method for hiding all of the game nodes whose visibility changes during the course of a challenge. - hideAllGameNodes: function() { - this.gameControlButtons.forEach( function( button ) { button.visible = false; } ); - this.setNodeVisibility( false, [ - this.startGameLevelNode, - this.faceWithPointsNode, - this.scoreboard, - this.controlPanel, - this.challengePromptBanner, - this.solutionBanner, - this.numberEntryControl, - this.areaQuestionPrompt, - this.youBuiltWindow, - this.youEnteredWindow, - this.shapeCarouselLayer, - this.eraserButton - ] ); - }, - - // @private - show: function( nodesToShow ) { - nodesToShow.forEach( function( nodeToShow ) { nodeToShow.visible = true; } ); - }, - - // @private - setNodeVisibility: function( isVisible, nodes ) { - nodes.forEach( function( node ) { node.visible = isVisible; } ); - }, - - // @private - hideChallenge: function() { - this.challengeLayer.visible = false; - this.controlLayer.visible = false; - }, - - // Show the graphic model elements for this challenge. - showChallengeGraphics: function() { - this.challengeLayer.visible = true; - this.controlLayer.visible = true; - }, - // @private - updatedCheckButtonEnabledState: function() { - if ( this.model.currentChallengeProperty.get() ) { - if ( this.model.currentChallengeProperty.get().checkSpec === 'areaEntered' ) { - this.checkAnswerButton.enabled = this.numberEntryControl.keypad.valueStringProperty.value.length > 0; + // Set the state of the control panel. + this.controlPanel.dimensionsIcon.setGridVisible( challenge.backgroundShape ? false : true ); + this.controlPanel.gridControlVisibleProperty.set( challenge.toolSpec.gridControl ); + this.controlPanel.dimensionsControlVisibleProperty.set( challenge.toolSpec.dimensionsControl ); + if ( challenge.backgroundShape ) { + this.controlPanel.dimensionsIcon.setColor( challenge.backgroundShape.fillColor ); + } + else if ( challenge.userShapes ) { + this.controlPanel.dimensionsIcon.setColor( challenge.userShapes[ 0 ].color ); + } + else { + this.controlPanel.dimensionsIcon.setColor( AreaBuilderSharedConstants.GREENISH_COLOR ); + } + + // Create the carousel if included as part of this challenge + if ( challenge.userShapes !== null ) { + const creatorNodes = []; + challenge.userShapes.forEach( function( userShapeSpec ) { + const creatorNodeOptions = { + gridSpacing: AreaBuilderGameModel.UNIT_SQUARE_LENGTH, + shapeDragBounds: self.layoutBounds, + nonMovingAncestor: self.shapeCarouselLayer + }; + if ( userShapeSpec.creationLimit ) { + creatorNodeOptions.creationLimit = userShapeSpec.creationLimit; + } + creatorNodes.push( new ShapeCreatorNode( + userShapeSpec.shape, + userShapeSpec.color, + self.model.simSpecificModel.addUserCreatedMovableShape.bind( self.model.simSpecificModel ), + creatorNodeOptions + ) ); + } ); + if ( creatorNodes.length > ITEMS_PER_CAROUSEL_PAGE ) { + // Add a scrolling carousel. + this.shapeCarouselLayer.addChild( new Carousel( creatorNodes, { + orientation: 'horizontal', + itemsPerPage: ITEMS_PER_CAROUSEL_PAGE, + centerX: this.shapeBoardOriginalBounds.centerX, + top: this.shapeBoardOriginalBounds.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, + fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR, + hideDisabledButtons: true + } ) ); } else { - this.checkAnswerButton.enabled = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().area > 0; + // Add a non-scrolling panel + const creatorNodeHBox = new HBox( { children: creatorNodes, spacing: 20 } ); + this.shapeCarouselLayer.addChild( new Panel( creatorNodeHBox, { + centerX: this.shapeBoardOriginalBounds.centerX, + top: this.shapeBoardOriginalBounds.bottom + SPACE_AROUND_SHAPE_PLACEMENT_BOARD, + xMargin: 50, + yMargin: 15, + fill: AreaBuilderSharedConstants.CONTROL_PANEL_BACKGROUND_COLOR + } ) ); } } - }, + } + }, + + // @private, Utility method for hiding all of the game nodes whose visibility changes during the course of a challenge. + hideAllGameNodes: function() { + this.gameControlButtons.forEach( function( button ) { button.visible = false; } ); + this.setNodeVisibility( false, [ + this.startGameLevelNode, + this.faceWithPointsNode, + this.scoreboard, + this.controlPanel, + this.challengePromptBanner, + this.solutionBanner, + this.numberEntryControl, + this.areaQuestionPrompt, + this.youBuiltWindow, + this.youEnteredWindow, + this.shapeCarouselLayer, + this.eraserButton + ] ); + }, + + // @private + show: function( nodesToShow ) { + nodesToShow.forEach( function( nodeToShow ) { nodeToShow.visible = true; } ); + }, + + // @private + setNodeVisibility: function( isVisible, nodes ) { + nodes.forEach( function( node ) { node.visible = isVisible; } ); + }, + + // @private + hideChallenge: function() { + this.challengeLayer.visible = false; + this.controlLayer.visible = false; + }, + + // Show the graphic model elements for this challenge. + showChallengeGraphics: function() { + this.challengeLayer.visible = true; + this.controlLayer.visible = true; + }, + + // @private + updatedCheckButtonEnabledState: function() { + if ( this.model.currentChallengeProperty.get() ) { + if ( this.model.currentChallengeProperty.get().checkSpec === 'areaEntered' ) { + this.checkAnswerButton.enabled = this.numberEntryControl.keypad.valueStringProperty.value.length > 0; + } + else { + this.checkAnswerButton.enabled = this.model.simSpecificModel.shapePlacementBoard.areaAndPerimeterProperty.get().area > 0; + } + } + }, - // @private - showLevelResultsNode: function() { - const self = this; - - // Set a new "level completed" node based on the results. - var levelCompletedNode = new LevelCompletedNode( - this.model.levelProperty.get() + 1, - this.model.scoreProperty.get(), - this.model.maxPossibleScore, - this.model.challengesPerSet, - this.model.timerEnabledProperty.get(), - this.model.elapsedTimeProperty.get(), - this.model.bestTimes[ this.model.levelProperty.get() ], - self.model.newBestTime, - function() { - self.model.gameStateProperty.set( GameState.CHOOSING_LEVEL ); - self.rootNode.removeChild( levelCompletedNode ); - levelCompletedNode = null; - }, - { - center: self.layoutBounds.center - } - ); + // @private + showLevelResultsNode: function() { + const self = this; - // Add the node. - this.rootNode.addChild( levelCompletedNode ); - } - } ); -} ); + // Set a new "level completed" node based on the results. + var levelCompletedNode = new LevelCompletedNode( + this.model.levelProperty.get() + 1, + this.model.scoreProperty.get(), + this.model.maxPossibleScore, + this.model.challengesPerSet, + this.model.timerEnabledProperty.get(), + this.model.elapsedTimeProperty.get(), + this.model.bestTimes[ this.model.levelProperty.get() ], + self.model.newBestTime, + function() { + self.model.gameStateProperty.set( GameState.CHOOSING_LEVEL ); + self.rootNode.removeChild( levelCompletedNode ); + levelCompletedNode = null; + }, + { + center: self.layoutBounds.center + } + ); + + // Add the node. + this.rootNode.addChild( levelCompletedNode ); + } +} ); \ No newline at end of file diff --git a/js/game/view/AreaBuilderScoreboard.js b/js/game/view/AreaBuilderScoreboard.js index 4653228..9d78bf2 100644 --- a/js/game/view/AreaBuilderScoreboard.js +++ b/js/game/view/AreaBuilderScoreboard.js @@ -5,93 +5,91 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const GameTimer = require( 'VEGAS/GameTimer' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Property = require( 'AXON/Property' ); - const StringUtils = require( 'PHETCOMMON/util/StringUtils' ); - const Text = require( 'SCENERY/nodes/Text' ); - const VBox = require( 'SCENERY/nodes/VBox' ); +import Property from '../../../../axon/js/Property.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import VBox from '../../../../scenery/js/nodes/VBox.js'; +import GameTimer from '../../../../vegas/js/GameTimer.js'; +import vegasStrings from '../../../../vegas/js/vegas-strings.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; - // strings - const labelScorePatternString = require( 'string!VEGAS/label.scorePattern' ); - const labelTimeString = require( 'string!VEGAS/label.time' ); - const levelString = require( 'string!AREA_BUILDER/level' ); - const pattern0Challenge1MaxString = require( 'string!AREA_BUILDER/pattern.0challenge.1max' ); +const labelScorePatternString = vegasStrings.label.scorePattern; +const labelTimeString = vegasStrings.label.time; +const levelString = areaBuilderStrings.level; +const pattern0Challenge1MaxString = areaBuilderStrings.pattern[ '0challenge' ][ '1max' ]; - /** - * @param levelProperty - * @param problemNumberProperty - * @param problemsPerLevel - * @param scoreProperty - * @param elapsedTimeProperty - * @param {Object} [options] - * @constructor - */ - function AreaBuilderScoreboard( levelProperty, problemNumberProperty, problemsPerLevel, scoreProperty, - elapsedTimeProperty, options ) { - Node.call( this ); +/** + * @param levelProperty + * @param problemNumberProperty + * @param problemsPerLevel + * @param scoreProperty + * @param elapsedTimeProperty + * @param {Object} [options] + * @constructor + */ +function AreaBuilderScoreboard( levelProperty, problemNumberProperty, problemsPerLevel, scoreProperty, + elapsedTimeProperty, options ) { + Node.call( this ); - options = merge( { maxWidth: Number.POSITIVE_INFINITY }, options ); + options = merge( { maxWidth: Number.POSITIVE_INFINITY }, options ); - // Properties that control which elements are visible and which are hidden. This constitutes the primary API. - this.timeVisibleProperty = new Property( true ); + // Properties that control which elements are visible and which are hidden. This constitutes the primary API. + this.timeVisibleProperty = new Property( true ); - // Create the labels - const levelIndicator = new Text( '', { - font: new PhetFont( { size: 20, weight: 'bold' } ), - maxWidth: options.maxWidth - } ); - levelProperty.link( function( level ) { - levelIndicator.text = StringUtils.format( levelString, level + 1 ); - } ); - const currentChallengeIndicator = new Text( '', { font: new PhetFont( { size: 16 } ), maxWidth: options.maxWidth } ); - problemNumberProperty.link( function( currentChallenge ) { - currentChallengeIndicator.text = StringUtils.format( pattern0Challenge1MaxString, currentChallenge + 1, problemsPerLevel ); - } ); - const scoreIndicator = new Text( '', { font: new PhetFont( 20 ), maxWidth: options.maxWidth } ); - scoreProperty.link( function( score ) { - scoreIndicator.text = StringUtils.format( labelScorePatternString, score ); - } ); - const elapsedTimeIndicator = new Text( '', { font: new PhetFont( 20 ), maxWidth: options.maxWidth } ); - elapsedTimeProperty.link( function( elapsedTime ) { - elapsedTimeIndicator.text = StringUtils.format( labelTimeString, GameTimer.formatTime( elapsedTime ) ); - } ); + // Create the labels + const levelIndicator = new Text( '', { + font: new PhetFont( { size: 20, weight: 'bold' } ), + maxWidth: options.maxWidth + } ); + levelProperty.link( function( level ) { + levelIndicator.text = StringUtils.format( levelString, level + 1 ); + } ); + const currentChallengeIndicator = new Text( '', { font: new PhetFont( { size: 16 } ), maxWidth: options.maxWidth } ); + problemNumberProperty.link( function( currentChallenge ) { + currentChallengeIndicator.text = StringUtils.format( pattern0Challenge1MaxString, currentChallenge + 1, problemsPerLevel ); + } ); + const scoreIndicator = new Text( '', { font: new PhetFont( 20 ), maxWidth: options.maxWidth } ); + scoreProperty.link( function( score ) { + scoreIndicator.text = StringUtils.format( labelScorePatternString, score ); + } ); + const elapsedTimeIndicator = new Text( '', { font: new PhetFont( 20 ), maxWidth: options.maxWidth } ); + elapsedTimeProperty.link( function( elapsedTime ) { + elapsedTimeIndicator.text = StringUtils.format( labelTimeString, GameTimer.formatTime( elapsedTime ) ); + } ); - // Create the panel. - const vBox = new VBox( { - children: [ - levelIndicator, - currentChallengeIndicator, - scoreIndicator, - elapsedTimeIndicator - ], - spacing: 12 - } ); - this.addChild( vBox ); + // Create the panel. + const vBox = new VBox( { + children: [ + levelIndicator, + currentChallengeIndicator, + scoreIndicator, + elapsedTimeIndicator + ], + spacing: 12 + } ); + this.addChild( vBox ); - // Add/remove the time indicator. - this.timeVisibleProperty.link( function( timeVisible ) { - if ( timeVisible && !vBox.hasChild( elapsedTimeIndicator ) ) { - // Insert just after the score indicator. - vBox.insertChild( vBox.indexOfChild( scoreIndicator ) + 1, elapsedTimeIndicator ); - } - else if ( !timeVisible && vBox.hasChild( elapsedTimeIndicator ) ) { - vBox.removeChild( elapsedTimeIndicator ); - } - } ); + // Add/remove the time indicator. + this.timeVisibleProperty.link( function( timeVisible ) { + if ( timeVisible && !vBox.hasChild( elapsedTimeIndicator ) ) { + // Insert just after the score indicator. + vBox.insertChild( vBox.indexOfChild( scoreIndicator ) + 1, elapsedTimeIndicator ); + } + else if ( !timeVisible && vBox.hasChild( elapsedTimeIndicator ) ) { + vBox.removeChild( elapsedTimeIndicator ); + } + } ); - this.mutate( options ); - } + this.mutate( options ); +} - areaBuilder.register( 'AreaBuilderScoreboard', AreaBuilderScoreboard ); +areaBuilder.register( 'AreaBuilderScoreboard', AreaBuilderScoreboard ); - return inherit( Node, AreaBuilderScoreboard ); -} ); \ No newline at end of file +inherit( Node, AreaBuilderScoreboard ); +export default AreaBuilderScoreboard; \ No newline at end of file diff --git a/js/game/view/ColorProportionsPrompt.js b/js/game/view/ColorProportionsPrompt.js index aa75548..7899263 100644 --- a/js/game/view/ColorProportionsPrompt.js +++ b/js/game/view/ColorProportionsPrompt.js @@ -6,109 +6,105 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const Fraction = require( 'PHETCOMMON/model/Fraction' ); - const FractionNode = require( 'AREA_BUILDER/game/view/FractionNode' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Path = require( 'SCENERY/nodes/Path' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Shape = require( 'KITE/Shape' ); - - // constants - const MULTI_LINE_SPACING = 5; // Empirically determined to look good - const SINGLE_LINE_SPACING = 12; // Empirically determined to look good - const PROMPT_TO_COLOR_SPACING = 4; // Empirically determined to look good - - /** - * @param {string || Color} color1 - Color value for the 1st color patch - * @param {string || Color} color2 - Color value for the 2nd color patch - * @param {Fraction} color1Proportion - Fraction of the whole that is comprised of color1, must be between 0 and 1, - * inclusive. The proportion for color2 is deduced from this value, with the two proportions summing to 1. - * @param {Object} [options] - * @constructor - */ - function ColorProportionsPrompt( color1, color2, color1Proportion, options ) { - Node.call( this ); - - options = merge( { - font: new PhetFont( { size: 18 } ), - textFill: 'black', - multiLine: false - }, options ); - - this.color1FractionNode = new FractionNode( color1Proportion, { - font: options.font, - color: options.textFill - } ); - this.addChild( this.color1FractionNode ); - - const color2Proportion = new Fraction( color1Proportion.denominator - color1Proportion.numerator, color1Proportion.denominator ); - this.color2FractionNode = new FractionNode( color2Proportion, { - font: options.font, - color: options.textFill - } ); - this.addChild( this.color2FractionNode ); - - const colorPatchShape = Shape.ellipse( 0, 0, this.color1FractionNode.bounds.height * 0.5, this.color1FractionNode.bounds.height * 0.35 ); - this.color1Patch = new Path( colorPatchShape, { - fill: color1, - left: this.color1FractionNode.right + PROMPT_TO_COLOR_SPACING, - centerY: this.color1FractionNode.centerY - } ); - this.addChild( this.color1Patch ); - - // Position the 2nd prompt based on whether or not the options specify multi-line. - if ( options.multiLine ) { - this.color2FractionNode.top = this.color1FractionNode.bottom + MULTI_LINE_SPACING; - } - else { - this.color2FractionNode.left = this.color1Patch.right + SINGLE_LINE_SPACING; - } - - this.color2Patch = new Path( colorPatchShape, { - fill: color2, - left: this.color2FractionNode.right + PROMPT_TO_COLOR_SPACING, - centerY: this.color2FractionNode.centerY - } ); - this.addChild( this.color2Patch ); - - this.mutate( options ); + +import Shape from '../../../../kite/js/Shape.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import Fraction from '../../../../phetcommon/js/model/Fraction.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Path from '../../../../scenery/js/nodes/Path.js'; +import areaBuilder from '../../areaBuilder.js'; +import FractionNode from './FractionNode.js'; + +// constants +const MULTI_LINE_SPACING = 5; // Empirically determined to look good +const SINGLE_LINE_SPACING = 12; // Empirically determined to look good +const PROMPT_TO_COLOR_SPACING = 4; // Empirically determined to look good + +/** + * @param {string || Color} color1 - Color value for the 1st color patch + * @param {string || Color} color2 - Color value for the 2nd color patch + * @param {Fraction} color1Proportion - Fraction of the whole that is comprised of color1, must be between 0 and 1, + * inclusive. The proportion for color2 is deduced from this value, with the two proportions summing to 1. + * @param {Object} [options] + * @constructor + */ +function ColorProportionsPrompt( color1, color2, color1Proportion, options ) { + Node.call( this ); + + options = merge( { + font: new PhetFont( { size: 18 } ), + textFill: 'black', + multiLine: false + }, options ); + + this.color1FractionNode = new FractionNode( color1Proportion, { + font: options.font, + color: options.textFill + } ); + this.addChild( this.color1FractionNode ); + + const color2Proportion = new Fraction( color1Proportion.denominator - color1Proportion.numerator, color1Proportion.denominator ); + this.color2FractionNode = new FractionNode( color2Proportion, { + font: options.font, + color: options.textFill + } ); + this.addChild( this.color2FractionNode ); + + const colorPatchShape = Shape.ellipse( 0, 0, this.color1FractionNode.bounds.height * 0.5, this.color1FractionNode.bounds.height * 0.35 ); + this.color1Patch = new Path( colorPatchShape, { + fill: color1, + left: this.color1FractionNode.right + PROMPT_TO_COLOR_SPACING, + centerY: this.color1FractionNode.centerY + } ); + this.addChild( this.color1Patch ); + + // Position the 2nd prompt based on whether or not the options specify multi-line. + if ( options.multiLine ) { + this.color2FractionNode.top = this.color1FractionNode.bottom + MULTI_LINE_SPACING; } + else { + this.color2FractionNode.left = this.color1Patch.right + SINGLE_LINE_SPACING; + } + + this.color2Patch = new Path( colorPatchShape, { + fill: color2, + left: this.color2FractionNode.right + PROMPT_TO_COLOR_SPACING, + centerY: this.color2FractionNode.centerY + } ); + this.addChild( this.color2Patch ); - areaBuilder.register( 'ColorProportionsPrompt', ColorProportionsPrompt ); + this.mutate( options ); +} - return inherit( Node, ColorProportionsPrompt, { +areaBuilder.register( 'ColorProportionsPrompt', ColorProportionsPrompt ); - set color1( color ) { - this.color1Patch.fill = color; - }, +export default inherit( Node, ColorProportionsPrompt, { - get color1() { - return this.color1Patch.fill; - }, + set color1( color ) { + this.color1Patch.fill = color; + }, - set color2( color ) { - this.color2Patch.fill = color; - }, + get color1() { + return this.color1Patch.fill; + }, - get color2() { - return this.color2Patch.fill; - }, + set color2( color ) { + this.color2Patch.fill = color; + }, - set color1Proportion( color1Proportion ) { - this.color1FractionNode.fraction = color1Proportion; - this.color2FractionNode.fraction = new Fraction( color1Proportion.denominator - color1Proportion.numerator, color1Proportion.denominator ); - }, + get color2() { + return this.color2Patch.fill; + }, - get color1Proportion() { - return this.color1FractionNode.fraction; - } + set color1Proportion( color1Proportion ) { + this.color1FractionNode.fraction = color1Proportion; + this.color2FractionNode.fraction = new Fraction( color1Proportion.denominator - color1Proportion.numerator, color1Proportion.denominator ); + }, + + get color1Proportion() { + return this.color1FractionNode.fraction; + } - } ); } ); \ No newline at end of file diff --git a/js/game/view/FeedbackWindow.js b/js/game/view/FeedbackWindow.js index 7c00659..586b5cd 100644 --- a/js/game/view/FeedbackWindow.js +++ b/js/game/view/FeedbackWindow.js @@ -6,74 +6,70 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const inherit = require( 'PHET_CORE/inherit' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const Panel = require( 'SUN/Panel' ); - const PhetColorScheme = require( 'SCENERY_PHET/PhetColorScheme' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Text = require( 'SCENERY/nodes/Text' ); +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import PhetColorScheme from '../../../../scenery-phet/js/PhetColorScheme.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import Panel from '../../../../sun/js/Panel.js'; +import areaBuilder from '../../areaBuilder.js'; - // constants - const X_MARGIN = 8; - const TITLE_FONT = new PhetFont( { size: 20, weight: 'bold' } ); - const NORMAL_TEXT_FONT = new PhetFont( { size: 18 } ); - const CORRECT_ANSWER_BACKGROUND_COLOR = 'white'; - const INCORRECT_ANSWER_BACKGROUND_COLOR = PhetColorScheme.PHET_LOGO_YELLOW; +// constants +const X_MARGIN = 8; +const TITLE_FONT = new PhetFont( { size: 20, weight: 'bold' } ); +const NORMAL_TEXT_FONT = new PhetFont( { size: 18 } ); +const CORRECT_ANSWER_BACKGROUND_COLOR = 'white'; +const INCORRECT_ANSWER_BACKGROUND_COLOR = PhetColorScheme.PHET_LOGO_YELLOW; - /** - * Constructor for the window that shows the user what they built. It is constructed with no contents, and the - * contents are added later when the build spec is set. - * - * @param {string} title - * @param {number} maxWidth - * @param {Object} [options] - * @constructor - */ - function FeedbackWindow( title, maxWidth, options ) { +/** + * Constructor for the window that shows the user what they built. It is constructed with no contents, and the + * contents are added later when the build spec is set. + * + * @param {string} title + * @param {number} maxWidth + * @param {Object} [options] + * @constructor + */ +function FeedbackWindow( title, maxWidth, options ) { - options = merge( { - fill: INCORRECT_ANSWER_BACKGROUND_COLOR, - stroke: 'black', - xMargin: X_MARGIN - }, options ); + options = merge( { + fill: INCORRECT_ANSWER_BACKGROUND_COLOR, + stroke: 'black', + xMargin: X_MARGIN + }, options ); - this.contentNode = new Node(); // @private + this.contentNode = new Node(); // @private - // title - this.titleNode = new Text( title, { font: TITLE_FONT } ); // @private - this.titleNode.scale( Math.min( ( maxWidth - 2 * X_MARGIN ) / this.titleNode.width, 1 ) ); - this.titleNode.top = 5; - this.contentNode.addChild( this.titleNode ); + // title + this.titleNode = new Text( title, { font: TITLE_FONT } ); // @private + this.titleNode.scale( Math.min( ( maxWidth - 2 * X_MARGIN ) / this.titleNode.width, 1 ) ); + this.titleNode.top = 5; + this.contentNode.addChild( this.titleNode ); - // Invoke super constructor - called here because content with no bounds doesn't work. This does not pass through - // position options - that needs to be handled in descendant classes. - Panel.call( this, this.contentNode, { fill: options.fill, stroke: options.stroke, xMargin: options.xMargin } ); - } + // Invoke super constructor - called here because content with no bounds doesn't work. This does not pass through + // position options - that needs to be handled in descendant classes. + Panel.call( this, this.contentNode, { fill: options.fill, stroke: options.stroke, xMargin: options.xMargin } ); +} - areaBuilder.register( 'FeedbackWindow', FeedbackWindow ); +areaBuilder.register( 'FeedbackWindow', FeedbackWindow ); - return inherit( Panel, FeedbackWindow, { +export default inherit( Panel, FeedbackWindow, { - /** - * Set the background color of this window based on whether or not the information being displayed is the correct - * answer. - * - * @param userAnswerIsCorrect - */ - setColorBasedOnAnswerCorrectness: function( userAnswerIsCorrect ) { - this.background.fill = userAnswerIsCorrect ? CORRECT_ANSWER_BACKGROUND_COLOR : INCORRECT_ANSWER_BACKGROUND_COLOR; - } - }, - { - // Statics - X_MARGIN: X_MARGIN, // Must be visible to subtypes so that max width can be calculated and, if necessary, scaled. - NORMAL_TEXT_FONT: NORMAL_TEXT_FONT // Font used in this window for text that is not the title. + /** + * Set the background color of this window based on whether or not the information being displayed is the correct + * answer. + * + * @param userAnswerIsCorrect + */ + setColorBasedOnAnswerCorrectness: function( userAnswerIsCorrect ) { + this.background.fill = userAnswerIsCorrect ? CORRECT_ANSWER_BACKGROUND_COLOR : INCORRECT_ANSWER_BACKGROUND_COLOR; } - ); -} ); \ No newline at end of file + }, + { + // Statics + X_MARGIN: X_MARGIN, // Must be visible to subtypes so that max width can be calculated and, if necessary, scaled. + NORMAL_TEXT_FONT: NORMAL_TEXT_FONT // Font used in this window for text that is not the title. + } +); \ No newline at end of file diff --git a/js/game/view/FractionNode.js b/js/game/view/FractionNode.js index c6c8f2b..a371a38 100644 --- a/js/game/view/FractionNode.js +++ b/js/game/view/FractionNode.js @@ -5,82 +5,78 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Line = require( 'SCENERY/nodes/Line' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Text = require( 'SCENERY/nodes/Text' ); +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Line from '../../../../scenery/js/nodes/Line.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import areaBuilder from '../../areaBuilder.js'; - /** - * @param {Fraction} fraction - * @param {Object} [options] - * @constructor - */ - function FractionNode( fraction, options ) { - Node.call( this ); - options = merge( { - // default options - font: new PhetFont( { size: 18 } ), - color: 'black', - fractionBarLineWidth: 1, +/** + * @param {Fraction} fraction + * @param {Object} [options] + * @constructor + */ +function FractionNode( fraction, options ) { + Node.call( this ); + options = merge( { + // default options + font: new PhetFont( { size: 18 } ), + color: 'black', + fractionBarLineWidth: 1, - // this option controls the width of the fraction bar as a function of the widest of the numerator and denominator. - fractionBarWidthProportion: 1.1 - }, options ); + // this option controls the width of the fraction bar as a function of the widest of the numerator and denominator. + fractionBarWidthProportion: 1.1 + }, options ); - assert && assert( options.fractionBarWidthProportion >= 1, 'The fraction bar must be at least the width of the larger fraction component.' ); + assert && assert( options.fractionBarWidthProportion >= 1, 'The fraction bar must be at least the width of the larger fraction component.' ); - // Create and add the pieces - this.numeratorNode = new Text( '0', { font: options.font, fill: options.color } ); - this.addChild( this.numeratorNode ); - this.denominatorNode = new Text( '0', { font: options.font, fill: options.color } ); - this.addChild( this.denominatorNode ); - const fractionBarWidth = options.fractionBarWidthProportion * Math.max( this.numeratorNode.width, this.denominatorNode.width ); - this.fractionBarNode = new Line( 0, 0, fractionBarWidth, 0, { - stroke: options.color, - lineWidth: options.fractionBarLineWidth - } ); - this.addChild( this.fractionBarNode ); + // Create and add the pieces + this.numeratorNode = new Text( '0', { font: options.font, fill: options.color } ); + this.addChild( this.numeratorNode ); + this.denominatorNode = new Text( '0', { font: options.font, fill: options.color } ); + this.addChild( this.denominatorNode ); + const fractionBarWidth = options.fractionBarWidthProportion * Math.max( this.numeratorNode.width, this.denominatorNode.width ); + this.fractionBarNode = new Line( 0, 0, fractionBarWidth, 0, { + stroke: options.color, + lineWidth: options.fractionBarLineWidth + } ); + this.addChild( this.fractionBarNode ); - this._fraction = fraction; - this.update(); - } + this._fraction = fraction; + this.update(); +} - areaBuilder.register( 'FractionNode', FractionNode ); +areaBuilder.register( 'FractionNode', FractionNode ); - return inherit( Node, FractionNode, { +export default inherit( Node, FractionNode, { - // @private - update: function() { - this.numeratorNode.text = this._fraction.numerator.toString(); - this.denominatorNode.text = this._fraction.denominator.toString(); + // @private + update: function() { + this.numeratorNode.text = this._fraction.numerator.toString(); + this.denominatorNode.text = this._fraction.denominator.toString(); - // Note: The fraction bar width is not updated here because the Line type didn't support changes when this code - // was developed and the code that used this node didn't really need it. If this code is being used in a more - // general way, where the elements of the fraction could reach multiple digits, adjustments to the size of the - // fraction bar will need to be added here. + // Note: The fraction bar width is not updated here because the Line type didn't support changes when this code + // was developed and the code that used this node didn't really need it. If this code is being used in a more + // general way, where the elements of the fraction could reach multiple digits, adjustments to the size of the + // fraction bar will need to be added here. - // layout - this.numeratorNode.centerX = this.fractionBarNode.centerX; - this.denominatorNode.centerX = this.fractionBarNode.centerX; - this.fractionBarNode.centerY = this.numeratorNode.bottom; - this.denominatorNode.top = this.fractionBarNode.bottom; - }, + // layout + this.numeratorNode.centerX = this.fractionBarNode.centerX; + this.denominatorNode.centerX = this.fractionBarNode.centerX; + this.fractionBarNode.centerY = this.numeratorNode.bottom; + this.denominatorNode.top = this.fractionBarNode.bottom; + }, - set fraction( fraction ) { - this._fraction = fraction; - this.update(); - }, + set fraction( fraction ) { + this._fraction = fraction; + this.update(); + }, - get fraction() { - return this._fraction; - } + get fraction() { + return this._fraction; + } - } ); } ); \ No newline at end of file diff --git a/js/game/view/GameIconFactory.js b/js/game/view/GameIconFactory.js index bc17061..c8d386a 100644 --- a/js/game/view/GameIconFactory.js +++ b/js/game/view/GameIconFactory.js @@ -5,167 +5,163 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const GridIcon = require( 'AREA_BUILDER/common/view/GridIcon' ); - const Vector2 = require( 'DOT/Vector2' ); +import Vector2 from '../../../../dot/js/Vector2.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; +import GridIcon from '../../common/view/GridIcon.js'; - // constants - const NUM_COLUMNS = 8; - const NUM_ROWS = 9; - const CELL_LENGTH = 3; - const GRID_ICON_OPTIONS = { - gridStroke: '#dddddd', - gridLineWidth: 0.25, - shapeLineWidth: 0.25 - }; +// constants +const NUM_COLUMNS = 8; +const NUM_ROWS = 9; +const CELL_LENGTH = 3; +const GRID_ICON_OPTIONS = { + gridStroke: '#dddddd', + gridLineWidth: 0.25, + shapeLineWidth: 0.25 +}; - /** - * Static object, not meant to be instantiated. - */ - const GameIconFactory = { - createIcon: function( level ) { - let color; - let occupiedCells; - switch( level ) { - case 1: - color = AreaBuilderSharedConstants.ORANGISH_COLOR; - occupiedCells = [ - new Vector2( 4, 1 ), - new Vector2( 3, 2 ), - new Vector2( 4, 2 ), - new Vector2( 4, 3 ), - new Vector2( 4, 4 ), - new Vector2( 4, 5 ), - new Vector2( 4, 6 ), - new Vector2( 3, 7 ), - new Vector2( 4, 7 ), - new Vector2( 5, 7 ) - ]; - break; +/** + * Static object, not meant to be instantiated. + */ +const GameIconFactory = { + createIcon: function( level ) { + let color; + let occupiedCells; + switch( level ) { + case 1: + color = AreaBuilderSharedConstants.ORANGISH_COLOR; + occupiedCells = [ + new Vector2( 4, 1 ), + new Vector2( 3, 2 ), + new Vector2( 4, 2 ), + new Vector2( 4, 3 ), + new Vector2( 4, 4 ), + new Vector2( 4, 5 ), + new Vector2( 4, 6 ), + new Vector2( 3, 7 ), + new Vector2( 4, 7 ), + new Vector2( 5, 7 ) + ]; + break; - case 2: - color = AreaBuilderSharedConstants.ORANGE_BROWN_COLOR; - occupiedCells = [ - new Vector2( 2, 1 ), - new Vector2( 3, 1 ), - new Vector2( 4, 1 ), - new Vector2( 5, 1 ), - new Vector2( 2, 2 ), - new Vector2( 5, 2 ), - new Vector2( 5, 3 ), - new Vector2( 2, 4 ), - new Vector2( 3, 4 ), - new Vector2( 4, 4 ), - new Vector2( 5, 4 ), - new Vector2( 2, 5 ), - new Vector2( 2, 6 ), - new Vector2( 2, 7 ), - new Vector2( 3, 7 ), - new Vector2( 4, 7 ), - new Vector2( 5, 7 ) - ]; - break; + case 2: + color = AreaBuilderSharedConstants.ORANGE_BROWN_COLOR; + occupiedCells = [ + new Vector2( 2, 1 ), + new Vector2( 3, 1 ), + new Vector2( 4, 1 ), + new Vector2( 5, 1 ), + new Vector2( 2, 2 ), + new Vector2( 5, 2 ), + new Vector2( 5, 3 ), + new Vector2( 2, 4 ), + new Vector2( 3, 4 ), + new Vector2( 4, 4 ), + new Vector2( 5, 4 ), + new Vector2( 2, 5 ), + new Vector2( 2, 6 ), + new Vector2( 2, 7 ), + new Vector2( 3, 7 ), + new Vector2( 4, 7 ), + new Vector2( 5, 7 ) + ]; + break; - case 3: - color = AreaBuilderSharedConstants.GREENISH_COLOR; - occupiedCells = [ - new Vector2( 2, 1 ), - new Vector2( 3, 1 ), - new Vector2( 4, 1 ), - new Vector2( 5, 1 ), - new Vector2( 5, 2 ), - new Vector2( 5, 3 ), - new Vector2( 3, 4 ), - new Vector2( 4, 4 ), - new Vector2( 5, 4 ), - new Vector2( 5, 5 ), - new Vector2( 5, 6 ), - new Vector2( 2, 7 ), - new Vector2( 3, 7 ), - new Vector2( 4, 7 ), - new Vector2( 5, 7 ) - ]; - break; + case 3: + color = AreaBuilderSharedConstants.GREENISH_COLOR; + occupiedCells = [ + new Vector2( 2, 1 ), + new Vector2( 3, 1 ), + new Vector2( 4, 1 ), + new Vector2( 5, 1 ), + new Vector2( 5, 2 ), + new Vector2( 5, 3 ), + new Vector2( 3, 4 ), + new Vector2( 4, 4 ), + new Vector2( 5, 4 ), + new Vector2( 5, 5 ), + new Vector2( 5, 6 ), + new Vector2( 2, 7 ), + new Vector2( 3, 7 ), + new Vector2( 4, 7 ), + new Vector2( 5, 7 ) + ]; + break; - case 4: - color = AreaBuilderSharedConstants.DARK_GREEN_COLOR; - occupiedCells = [ - new Vector2( 5, 1 ), - new Vector2( 2, 2 ), - new Vector2( 5, 2 ), - new Vector2( 2, 3 ), - new Vector2( 5, 3 ), - new Vector2( 2, 4 ), - new Vector2( 5, 4 ), - new Vector2( 2, 5 ), - new Vector2( 3, 5 ), - new Vector2( 4, 5 ), - new Vector2( 5, 5 ), - new Vector2( 6, 5 ), - new Vector2( 5, 6 ), - new Vector2( 5, 7 ) - ]; - break; + case 4: + color = AreaBuilderSharedConstants.DARK_GREEN_COLOR; + occupiedCells = [ + new Vector2( 5, 1 ), + new Vector2( 2, 2 ), + new Vector2( 5, 2 ), + new Vector2( 2, 3 ), + new Vector2( 5, 3 ), + new Vector2( 2, 4 ), + new Vector2( 5, 4 ), + new Vector2( 2, 5 ), + new Vector2( 3, 5 ), + new Vector2( 4, 5 ), + new Vector2( 5, 5 ), + new Vector2( 6, 5 ), + new Vector2( 5, 6 ), + new Vector2( 5, 7 ) + ]; + break; - case 5: - color = AreaBuilderSharedConstants.PURPLISH_COLOR; - occupiedCells = [ - new Vector2( 2, 1 ), - new Vector2( 3, 1 ), - new Vector2( 4, 1 ), - new Vector2( 5, 1 ), - new Vector2( 2, 2 ), - new Vector2( 2, 3 ), - new Vector2( 2, 4 ), - new Vector2( 3, 4 ), - new Vector2( 4, 4 ), - new Vector2( 5, 4 ), - new Vector2( 5, 5 ), - new Vector2( 5, 6 ), - new Vector2( 2, 7 ), - new Vector2( 3, 7 ), - new Vector2( 4, 7 ), - new Vector2( 5, 7 ) - ]; - break; + case 5: + color = AreaBuilderSharedConstants.PURPLISH_COLOR; + occupiedCells = [ + new Vector2( 2, 1 ), + new Vector2( 3, 1 ), + new Vector2( 4, 1 ), + new Vector2( 5, 1 ), + new Vector2( 2, 2 ), + new Vector2( 2, 3 ), + new Vector2( 2, 4 ), + new Vector2( 3, 4 ), + new Vector2( 4, 4 ), + new Vector2( 5, 4 ), + new Vector2( 5, 5 ), + new Vector2( 5, 6 ), + new Vector2( 2, 7 ), + new Vector2( 3, 7 ), + new Vector2( 4, 7 ), + new Vector2( 5, 7 ) + ]; + break; - case 6: - color = AreaBuilderSharedConstants.PINKISH_COLOR; - occupiedCells = [ - new Vector2( 2, 1 ), - new Vector2( 3, 1 ), - new Vector2( 4, 1 ), - new Vector2( 5, 1 ), - new Vector2( 2, 2 ), - new Vector2( 2, 3 ), - new Vector2( 2, 4 ), - new Vector2( 3, 4 ), - new Vector2( 4, 4 ), - new Vector2( 5, 4 ), - new Vector2( 2, 5 ), - new Vector2( 5, 5 ), - new Vector2( 2, 6 ), - new Vector2( 5, 6 ), - new Vector2( 2, 7 ), - new Vector2( 3, 7 ), - new Vector2( 4, 7 ), - new Vector2( 5, 7 ) - ]; - break; + case 6: + color = AreaBuilderSharedConstants.PINKISH_COLOR; + occupiedCells = [ + new Vector2( 2, 1 ), + new Vector2( 3, 1 ), + new Vector2( 4, 1 ), + new Vector2( 5, 1 ), + new Vector2( 2, 2 ), + new Vector2( 2, 3 ), + new Vector2( 2, 4 ), + new Vector2( 3, 4 ), + new Vector2( 4, 4 ), + new Vector2( 5, 4 ), + new Vector2( 2, 5 ), + new Vector2( 5, 5 ), + new Vector2( 2, 6 ), + new Vector2( 5, 6 ), + new Vector2( 2, 7 ), + new Vector2( 3, 7 ), + new Vector2( 4, 7 ), + new Vector2( 5, 7 ) + ]; + break; - default: - throw new Error( 'Unsupported game level: ' + level ); - } - return new GridIcon( NUM_COLUMNS, NUM_ROWS, CELL_LENGTH, color, occupiedCells, GRID_ICON_OPTIONS ); + default: + throw new Error( 'Unsupported game level: ' + level ); } - }; + return new GridIcon( NUM_COLUMNS, NUM_ROWS, CELL_LENGTH, color, occupiedCells, GRID_ICON_OPTIONS ); + } +}; - areaBuilder.register( 'GameIconFactory', GameIconFactory ); +areaBuilder.register( 'GameIconFactory', GameIconFactory ); - return GameIconFactory; -} ); \ No newline at end of file +export default GameIconFactory; \ No newline at end of file diff --git a/js/game/view/GameInfoBanner.js b/js/game/view/GameInfoBanner.js index cc11b41..763ed5f 100644 --- a/js/game/view/GameInfoBanner.js +++ b/js/game/view/GameInfoBanner.js @@ -5,213 +5,209 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const Animation = require( 'TWIXT/Animation' ); - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const ColorProportionsPrompt = require( 'AREA_BUILDER/game/view/ColorProportionsPrompt' ); - const Easing = require( 'TWIXT/Easing' ); - const Fraction = require( 'PHETCOMMON/model/Fraction' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const Property = require( 'AXON/Property' ); - const Rectangle = require( 'SCENERY/nodes/Rectangle' ); - const StringUtils = require( 'PHETCOMMON/util/StringUtils' ); - const Text = require( 'SCENERY/nodes/Text' ); - - // strings - const areaEqualsString = require( 'string!AREA_BUILDER/areaEquals' ); - const perimeterEqualsString = require( 'string!AREA_BUILDER/perimeterEquals' ); - - // constants - const TEXT_FILL_COLOR = 'white'; - const TITLE_FONT = new PhetFont( { size: 24, weight: 'bold' } ); // Font used for the title - const LARGER_FONT = new PhetFont( { size: 24 } ); // Font for single line text - const SMALLER_FONT = new PhetFont( { size: 18 } ); // Font for two-line text - const TITLE_INDENT = 15; // Empirically determined. - const ANIMATION_TIME = 0.6; // In seconds - - /** - * @param {number} width - * @param {number} height - * @param {string} backgroundColor - * @param {Object} [options] - * @constructor - */ - function GameInfoBanner( width, height, backgroundColor, options ) { - const self = this; - Rectangle.call( this, 0, 0, width, height, 0, 0, { fill: backgroundColor } ); - - // @public These properties are the main API for this class, and they control what is and isn't shown on the banner. - this.titleTextProperty = new Property( '' ); - this.buildSpecProperty = new Property( null ); - this.areaToFindProperty = new Property( null ); - - // Define the title. - const title = new Text( this.titleTextProperty.value, { - font: TITLE_FONT, - fill: TEXT_FILL_COLOR, - centerY: height / 2, - maxWidth: width * 0.3 // must be small enough that the prompt can also fit on the banner - } ); - this.addChild( title ); - - // Update the title when the title text changes. - this.titleTextProperty.link( function( titleText ) { - title.text = titleText; - title.centerY = height / 2; - if ( self.buildSpecProperty.value === null && self.areaToFindProperty.value === null ) { - // There is no build spec are area to find, so center the title in the banner. - title.centerX = width / 2; - } - else { - // There is a build spec, so the title should be on the left to make room. - title.left = TITLE_INDENT; - } - } ); - // Define the build prompt, which is shown in both the challenge prompt and the solution. - const buildPrompt = new Node(); - this.addChild( buildPrompt ); - const maxBuildPromptWidth = width / 2; // the build prompt has to fit in the banner with the title - const areaPrompt = new Text( '', { font: SMALLER_FONT, fill: TEXT_FILL_COLOR, top: 0 } ); - buildPrompt.addChild( areaPrompt ); - const perimeterPrompt = new Text( '', { font: SMALLER_FONT, fill: TEXT_FILL_COLOR, top: 0 } ); - buildPrompt.addChild( perimeterPrompt ); - const colorProportionPrompt = new ColorProportionsPrompt( 'black', 'white', - new Fraction( 1, 1 ), { - font: new PhetFont( { size: 11 } ), - textFill: TEXT_FILL_COLOR, - top: 0 - } ); - buildPrompt.addChild( colorProportionPrompt ); - - // Function that moves the title from the center of the banner to the left side if it isn't already there. - function moveTitleToSide() { - if ( title.centerX === width / 2 ) { - // Move the title over +import Property from '../../../../axon/js/Property.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import Fraction from '../../../../phetcommon/js/model/Fraction.js'; +import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Rectangle from '../../../../scenery/js/nodes/Rectangle.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import Animation from '../../../../twixt/js/Animation.js'; +import Easing from '../../../../twixt/js/Easing.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; +import ColorProportionsPrompt from './ColorProportionsPrompt.js'; + +const areaEqualsString = areaBuilderStrings.areaEquals; +const perimeterEqualsString = areaBuilderStrings.perimeterEquals; + +// constants +const TEXT_FILL_COLOR = 'white'; +const TITLE_FONT = new PhetFont( { size: 24, weight: 'bold' } ); // Font used for the title +const LARGER_FONT = new PhetFont( { size: 24 } ); // Font for single line text +const SMALLER_FONT = new PhetFont( { size: 18 } ); // Font for two-line text +const TITLE_INDENT = 15; // Empirically determined. +const ANIMATION_TIME = 0.6; // In seconds + +/** + * @param {number} width + * @param {number} height + * @param {string} backgroundColor + * @param {Object} [options] + * @constructor + */ +function GameInfoBanner( width, height, backgroundColor, options ) { + const self = this; + Rectangle.call( this, 0, 0, width, height, 0, 0, { fill: backgroundColor } ); + + // @public These properties are the main API for this class, and they control what is and isn't shown on the banner. + this.titleTextProperty = new Property( '' ); + this.buildSpecProperty = new Property( null ); + this.areaToFindProperty = new Property( null ); + + // Define the title. + const title = new Text( this.titleTextProperty.value, { + font: TITLE_FONT, + fill: TEXT_FILL_COLOR, + centerY: height / 2, + maxWidth: width * 0.3 // must be small enough that the prompt can also fit on the banner + } ); + this.addChild( title ); + + // Update the title when the title text changes. + this.titleTextProperty.link( function( titleText ) { + title.text = titleText; + title.centerY = height / 2; + if ( self.buildSpecProperty.value === null && self.areaToFindProperty.value === null ) { + // There is no build spec are area to find, so center the title in the banner. + title.centerX = width / 2; + } + else { + // There is a build spec, so the title should be on the left to make room. + title.left = TITLE_INDENT; + } + } ); + + // Define the build prompt, which is shown in both the challenge prompt and the solution. + const buildPrompt = new Node(); + this.addChild( buildPrompt ); + const maxBuildPromptWidth = width / 2; // the build prompt has to fit in the banner with the title + const areaPrompt = new Text( '', { font: SMALLER_FONT, fill: TEXT_FILL_COLOR, top: 0 } ); + buildPrompt.addChild( areaPrompt ); + const perimeterPrompt = new Text( '', { font: SMALLER_FONT, fill: TEXT_FILL_COLOR, top: 0 } ); + buildPrompt.addChild( perimeterPrompt ); + const colorProportionPrompt = new ColorProportionsPrompt( 'black', 'white', + new Fraction( 1, 1 ), { + font: new PhetFont( { size: 11 } ), + textFill: TEXT_FILL_COLOR, + top: 0 + } ); + buildPrompt.addChild( colorProportionPrompt ); + + // Function that moves the title from the center of the banner to the left side if it isn't already there. + function moveTitleToSide() { + if ( title.centerX === width / 2 ) { + // Move the title over + new Animation( { + from: title.left, + to: TITLE_INDENT, + setValue: left => { title.left = left; }, + duration: ANIMATION_TIME, + easing: Easing.CUBIC_IN_OUT + } ).start(); + + // Fade in the build prompt if it is now set to be visible. + if ( buildPrompt.visible ) { + buildPrompt.opacity = 0; new Animation( { - from: title.left, - to: TITLE_INDENT, - setValue: left => { title.left = left; }, + from: 0, + to: 1, + setValue: opacity => { buildPrompt.opacity = opacity; }, duration: ANIMATION_TIME, easing: Easing.CUBIC_IN_OUT } ).start(); - - // Fade in the build prompt if it is now set to be visible. - if ( buildPrompt.visible ) { - buildPrompt.opacity = 0; - new Animation( { - from: 0, - to: 1, - setValue: opacity => { buildPrompt.opacity = opacity; }, - duration: ANIMATION_TIME, - easing: Easing.CUBIC_IN_OUT - } ).start(); - } } } + } - // Function that positions the build prompt such that its visible bounds are centered in the space to the left of - // the title. - function positionBuildPrompt() { - const centerX = ( TITLE_INDENT + title.width + width - TITLE_INDENT ) / 2; - const centerY = height / 2; - buildPrompt.setScaleMagnitude( 1 ); - if ( buildPrompt.width > maxBuildPromptWidth ) { - // scale the build prompt to fit with the title on the banner - buildPrompt.setScaleMagnitude( maxBuildPromptWidth / buildPrompt.width ); - } - buildPrompt.left += centerX - buildPrompt.visibleBounds.centerX; - buildPrompt.top += centerY - buildPrompt.visibleBounds.centerY; + // Function that positions the build prompt such that its visible bounds are centered in the space to the left of + // the title. + function positionBuildPrompt() { + const centerX = ( TITLE_INDENT + title.width + width - TITLE_INDENT ) / 2; + const centerY = height / 2; + buildPrompt.setScaleMagnitude( 1 ); + if ( buildPrompt.width > maxBuildPromptWidth ) { + // scale the build prompt to fit with the title on the banner + buildPrompt.setScaleMagnitude( maxBuildPromptWidth / buildPrompt.width ); } + buildPrompt.left += centerX - buildPrompt.visibleBounds.centerX; + buildPrompt.top += centerY - buildPrompt.visibleBounds.centerY; + } - // Update the prompt or solution text based on the build spec. - this.buildSpecProperty.link( function( buildSpec ) { - assert && assert( self.areaToFindProperty.value === null, 'Can\'t display area to find and build spec at the same time.' ); - assert && assert( buildSpec === null || buildSpec.area, 'Area must be specified in the build spec' ); - if ( buildSpec !== null ) { - areaPrompt.text = StringUtils.format( areaEqualsString, buildSpec.area ); - areaPrompt.visible = true; - if ( !buildSpec.perimeter && !buildSpec.proportions ) { - areaPrompt.font = LARGER_FONT; + // Update the prompt or solution text based on the build spec. + this.buildSpecProperty.link( function( buildSpec ) { + assert && assert( self.areaToFindProperty.value === null, 'Can\'t display area to find and build spec at the same time.' ); + assert && assert( buildSpec === null || buildSpec.area, 'Area must be specified in the build spec' ); + if ( buildSpec !== null ) { + areaPrompt.text = StringUtils.format( areaEqualsString, buildSpec.area ); + areaPrompt.visible = true; + if ( !buildSpec.perimeter && !buildSpec.proportions ) { + areaPrompt.font = LARGER_FONT; + perimeterPrompt.visible = false; + colorProportionPrompt.visible = false; + } + else { + areaPrompt.font = SMALLER_FONT; + if ( buildSpec.perimeter ) { + perimeterPrompt.text = StringUtils.format( perimeterEqualsString, buildSpec.perimeter ); + perimeterPrompt.visible = true; + } + else { perimeterPrompt.visible = false; - colorProportionPrompt.visible = false; + } + if ( buildSpec.proportions ) { + areaPrompt.text += ','; + colorProportionPrompt.color1 = buildSpec.proportions.color1; + colorProportionPrompt.color2 = buildSpec.proportions.color2; + colorProportionPrompt.color1Proportion = buildSpec.proportions.color1Proportion; + colorProportionPrompt.visible = true; } else { - areaPrompt.font = SMALLER_FONT; - if ( buildSpec.perimeter ) { - perimeterPrompt.text = StringUtils.format( perimeterEqualsString, buildSpec.perimeter ); - perimeterPrompt.visible = true; - } - else { - perimeterPrompt.visible = false; - } - if ( buildSpec.proportions ) { - areaPrompt.text += ','; - colorProportionPrompt.color1 = buildSpec.proportions.color1; - colorProportionPrompt.color2 = buildSpec.proportions.color2; - colorProportionPrompt.color1Proportion = buildSpec.proportions.color1Proportion; - colorProportionPrompt.visible = true; - } - else { - colorProportionPrompt.visible = false; - } + colorProportionPrompt.visible = false; } - - // Update the layout - perimeterPrompt.top = areaPrompt.bottom + areaPrompt.height * 0.25; // Spacing empirically determined. - colorProportionPrompt.left = areaPrompt.right + 10; // Spacing empirically determined - colorProportionPrompt.centerY = areaPrompt.centerY; - positionBuildPrompt(); - - // Make sure the title is over on the left side. - moveTitleToSide(); } - else { - areaPrompt.visible = self.areaToFindProperty.value !== null; - perimeterPrompt.visible = false; - colorProportionPrompt.visible = false; - } - } ); - - // Update the area indication (used in solution for 'find the area' challenges). - this.areaToFindProperty.link( function( areaToFind ) { - assert && assert( self.buildSpecProperty.value === null, 'Can\'t display area to find and build spec at the same time.' ); - if ( areaToFind !== null ) { - areaPrompt.text = StringUtils.format( areaEqualsString, areaToFind ); - areaPrompt.font = LARGER_FONT; - areaPrompt.visible = true; - // The other prompts (perimeter and color proportions) are not shown in this situation. - perimeterPrompt.visible = false; - colorProportionPrompt.visible = false; + // Update the layout + perimeterPrompt.top = areaPrompt.bottom + areaPrompt.height * 0.25; // Spacing empirically determined. + colorProportionPrompt.left = areaPrompt.right + 10; // Spacing empirically determined + colorProportionPrompt.centerY = areaPrompt.centerY; + positionBuildPrompt(); - // Place the build prompt where it needs to go. - positionBuildPrompt(); + // Make sure the title is over on the left side. + moveTitleToSide(); + } + else { + areaPrompt.visible = self.areaToFindProperty.value !== null; + perimeterPrompt.visible = false; + colorProportionPrompt.visible = false; + } + } ); - // Make sure the title is over on the left side. - moveTitleToSide(); - } - else { - areaPrompt.visible = self.buildSpecProperty.value !== null; - } - } ); + // Update the area indication (used in solution for 'find the area' challenges). + this.areaToFindProperty.link( function( areaToFind ) { + assert && assert( self.buildSpecProperty.value === null, 'Can\'t display area to find and build spec at the same time.' ); + if ( areaToFind !== null ) { + areaPrompt.text = StringUtils.format( areaEqualsString, areaToFind ); + areaPrompt.font = LARGER_FONT; + areaPrompt.visible = true; - // Pass options through to parent class. - this.mutate( options ); - } + // The other prompts (perimeter and color proportions) are not shown in this situation. + perimeterPrompt.visible = false; + colorProportionPrompt.visible = false; - areaBuilder.register( 'GameInfoBanner', GameInfoBanner ); + // Place the build prompt where it needs to go. + positionBuildPrompt(); - return inherit( Rectangle, GameInfoBanner, { - reset: function() { - this.titleTextProperty.reset(); - this.buildSpecProperty.reset(); - this.areaToFindProperty.reset(); + // Make sure the title is over on the left side. + moveTitleToSide(); + } + else { + areaPrompt.visible = self.buildSpecProperty.value !== null; } } ); + + // Pass options through to parent class. + this.mutate( options ); +} + +areaBuilder.register( 'GameInfoBanner', GameInfoBanner ); + +export default inherit( Rectangle, GameInfoBanner, { + reset: function() { + this.titleTextProperty.reset(); + this.buildSpecProperty.reset(); + this.areaToFindProperty.reset(); + } } ); \ No newline at end of file diff --git a/js/game/view/StartGameLevelNode.js b/js/game/view/StartGameLevelNode.js index 3a89d71..470026a 100644 --- a/js/game/view/StartGameLevelNode.js +++ b/js/game/view/StartGameLevelNode.js @@ -7,132 +7,129 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const AreaBuilderSharedConstants = require( 'AREA_BUILDER/common/AreaBuilderSharedConstants' ); - const inherit = require( 'PHET_CORE/inherit' ); - const LevelSelectionButton = require( 'VEGAS/LevelSelectionButton' ); - const merge = require( 'PHET_CORE/merge' ); - const Node = require( 'SCENERY/nodes/Node' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const ResetAllButton = require( 'SCENERY_PHET/buttons/ResetAllButton' ); - const Text = require( 'SCENERY/nodes/Text' ); - const TimerToggleButton = require( 'SCENERY_PHET/buttons/TimerToggleButton' ); - const Vector2 = require( 'DOT/Vector2' ); - - // strings - const chooseYourLevelString = require( 'string!VEGAS/chooseYourLevel' ); - - // constants - const CONTROL_BUTTON_TOUCH_AREA_DILATION = 4; - - /** - * @param {function} startLevelFunction - Function used to initiate a game - * level, will be called with a zero-based index value. - * @param {function} resetFunction - Function to reset game and scores. - * @param {Property} timerEnabledProperty - * @param {Array} iconNodes - Set of iconNodes to use on the buttons, sizes - * should be the same, length of array must match number of levels. - * @param {Array} scores - Current scores, used to decide which stars to - * illuminate on the level start buttons, length must match number of levels. - * @param {Object} [options] - See code below for options and default values. - * @constructor - */ - function StartGameLevelNode( startLevelFunction, - resetFunction, - timerEnabledProperty, - iconNodes, - scores, - options ) { - - Node.call( this ); - - options = merge( { - - // defaults - numLevels: 4, - titleString: chooseYourLevelString, - maxTitleWidth: 500, - numStarsOnButtons: 5, - perfectScore: 10, - buttonBackgroundColor: '#A8BEFF', - numButtonRows: 1, // For layout - controlsInset: 12, - size: AreaBuilderSharedConstants.LAYOUT_BOUNDS - }, options ); - - // Verify parameters - if ( iconNodes.length !== options.numLevels || scores.length !== options.numLevels ) { - throw new Error( 'Number of game levels doesn\'t match length of provided arrays' ); - } - // Title - const title = new Text( options.titleString, { font: new PhetFont( 30 ), maxWidth: options.maxTitleWidth } ); - this.addChild( title ); +import Vector2 from '../../../../dot/js/Vector2.js'; +import inherit from '../../../../phet-core/js/inherit.js'; +import merge from '../../../../phet-core/js/merge.js'; +import ResetAllButton from '../../../../scenery-phet/js/buttons/ResetAllButton.js'; +import TimerToggleButton from '../../../../scenery-phet/js/buttons/TimerToggleButton.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Node from '../../../../scenery/js/nodes/Node.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import LevelSelectionButton from '../../../../vegas/js/LevelSelectionButton.js'; +import vegasStrings from '../../../../vegas/js/vegas-strings.js'; +import areaBuilder from '../../areaBuilder.js'; +import AreaBuilderSharedConstants from '../../common/AreaBuilderSharedConstants.js'; - // Add the buttons - function createLevelStartFunction( level ) { - return function() { startLevelFunction( level ); }; - } +const chooseYourLevelString = vegasStrings.chooseYourLevel; - const buttons = new Array( options.numLevels ); - for ( let i = 0; i < options.numLevels; i++ ) { - buttons[ i ] = new LevelSelectionButton( - iconNodes[ i ], - scores[ i ], - { - listener: createLevelStartFunction( i ), - baseColor: options.buttonBackgroundColor, - scoreDisplayOptions: { - numberOfStars: options.numStarsOnButtons, - perfectScore: options.perfectScore - } - } - ); - buttons[ i ].scale( 0.80 ); - this.addChild( buttons[ i ] ); - } +// constants +const CONTROL_BUTTON_TOUCH_AREA_DILATION = 4; - // Sound and timer controls. - const timerToggleButton = new TimerToggleButton( timerEnabledProperty, { - touchAreaXDilation: CONTROL_BUTTON_TOUCH_AREA_DILATION, - touchAreaYDilation: CONTROL_BUTTON_TOUCH_AREA_DILATION - } ); - this.addChild( timerToggleButton ); - - // Reset button. - const resetButton = new ResetAllButton( { - listener: resetFunction, - radius: AreaBuilderSharedConstants.RESET_BUTTON_RADIUS - } ); - this.addChild( resetButton ); - - // Layout - const numColumns = options.numLevels / options.numButtonRows; - const buttonSpacingX = buttons[ 0 ].width * 1.2; // Note: Assumes all buttons are the same size. - const buttonSpacingY = buttons[ 0 ].height * 1.2; // Note: Assumes all buttons are the same size. - const firstButtonOrigin = new Vector2( options.size.width / 2 - ( numColumns - 1 ) * buttonSpacingX / 2, - options.size.height * 0.5 - ( ( options.numButtonRows - 1 ) * buttonSpacingY ) / 2 ); - for ( let row = 0; row < options.numButtonRows; row++ ) { - for ( let col = 0; col < numColumns; col++ ) { - const buttonIndex = row * numColumns + col; - buttons[ buttonIndex ].centerX = firstButtonOrigin.x + col * buttonSpacingX; - buttons[ buttonIndex ].centerY = firstButtonOrigin.y + row * buttonSpacingY; - } - } - resetButton.right = options.size.width - options.controlsInset; - resetButton.bottom = options.size.height - options.controlsInset; - title.centerX = options.size.width / 2; - title.centerY = buttons[ 0 ].top / 2; - timerToggleButton.left = options.controlsInset; - timerToggleButton.bottom = options.size.height - options.controlsInset; +/** + * @param {function} startLevelFunction - Function used to initiate a game + * level, will be called with a zero-based index value. + * @param {function} resetFunction - Function to reset game and scores. + * @param {Property} timerEnabledProperty + * @param {Array} iconNodes - Set of iconNodes to use on the buttons, sizes + * should be the same, length of array must match number of levels. + * @param {Array} scores - Current scores, used to decide which stars to + * illuminate on the level start buttons, length must match number of levels. + * @param {Object} [options] - See code below for options and default values. + * @constructor + */ +function StartGameLevelNode( startLevelFunction, + resetFunction, + timerEnabledProperty, + iconNodes, + scores, + options ) { + + Node.call( this ); + + options = merge( { + + // defaults + numLevels: 4, + titleString: chooseYourLevelString, + maxTitleWidth: 500, + numStarsOnButtons: 5, + perfectScore: 10, + buttonBackgroundColor: '#A8BEFF', + numButtonRows: 1, // For layout + controlsInset: 12, + size: AreaBuilderSharedConstants.LAYOUT_BOUNDS + }, options ); + + // Verify parameters + if ( iconNodes.length !== options.numLevels || scores.length !== options.numLevels ) { + throw new Error( 'Number of game levels doesn\'t match length of provided arrays' ); } - areaBuilder.register( 'StartGameLevelNode', StartGameLevelNode ); + // Title + const title = new Text( options.titleString, { font: new PhetFont( 30 ), maxWidth: options.maxTitleWidth } ); + this.addChild( title ); - // Inherit from Node. - return inherit( Node, StartGameLevelNode ); -} ); + // Add the buttons + function createLevelStartFunction( level ) { + return function() { startLevelFunction( level ); }; + } + + const buttons = new Array( options.numLevels ); + for ( let i = 0; i < options.numLevels; i++ ) { + buttons[ i ] = new LevelSelectionButton( + iconNodes[ i ], + scores[ i ], + { + listener: createLevelStartFunction( i ), + baseColor: options.buttonBackgroundColor, + scoreDisplayOptions: { + numberOfStars: options.numStarsOnButtons, + perfectScore: options.perfectScore + } + } + ); + buttons[ i ].scale( 0.80 ); + this.addChild( buttons[ i ] ); + } + + // Sound and timer controls. + const timerToggleButton = new TimerToggleButton( timerEnabledProperty, { + touchAreaXDilation: CONTROL_BUTTON_TOUCH_AREA_DILATION, + touchAreaYDilation: CONTROL_BUTTON_TOUCH_AREA_DILATION + } ); + this.addChild( timerToggleButton ); + + // Reset button. + const resetButton = new ResetAllButton( { + listener: resetFunction, + radius: AreaBuilderSharedConstants.RESET_BUTTON_RADIUS + } ); + this.addChild( resetButton ); + + // Layout + const numColumns = options.numLevels / options.numButtonRows; + const buttonSpacingX = buttons[ 0 ].width * 1.2; // Note: Assumes all buttons are the same size. + const buttonSpacingY = buttons[ 0 ].height * 1.2; // Note: Assumes all buttons are the same size. + const firstButtonOrigin = new Vector2( options.size.width / 2 - ( numColumns - 1 ) * buttonSpacingX / 2, + options.size.height * 0.5 - ( ( options.numButtonRows - 1 ) * buttonSpacingY ) / 2 ); + for ( let row = 0; row < options.numButtonRows; row++ ) { + for ( let col = 0; col < numColumns; col++ ) { + const buttonIndex = row * numColumns + col; + buttons[ buttonIndex ].centerX = firstButtonOrigin.x + col * buttonSpacingX; + buttons[ buttonIndex ].centerY = firstButtonOrigin.y + row * buttonSpacingY; + } + } + resetButton.right = options.size.width - options.controlsInset; + resetButton.bottom = options.size.height - options.controlsInset; + title.centerX = options.size.width / 2; + title.centerY = buttons[ 0 ].top / 2; + timerToggleButton.left = options.controlsInset; + timerToggleButton.bottom = options.size.height - options.controlsInset; +} + +areaBuilder.register( 'StartGameLevelNode', StartGameLevelNode ); + +// Inherit from Node. +inherit( Node, StartGameLevelNode ); +export default StartGameLevelNode; \ No newline at end of file diff --git a/js/game/view/YouBuiltWindow.js b/js/game/view/YouBuiltWindow.js index 390011a..ade3c8a 100644 --- a/js/game/view/YouBuiltWindow.js +++ b/js/game/view/YouBuiltWindow.js @@ -6,142 +6,138 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const ColorProportionsPrompt = require( 'AREA_BUILDER/game/view/ColorProportionsPrompt' ); - const FeedbackWindow = require( 'AREA_BUILDER/game/view/FeedbackWindow' ); - const inherit = require( 'PHET_CORE/inherit' ); - const PhetFont = require( 'SCENERY_PHET/PhetFont' ); - const StringUtils = require( 'PHETCOMMON/util/StringUtils' ); - const Text = require( 'SCENERY/nodes/Text' ); - - // strings - const areaEqualsString = require( 'string!AREA_BUILDER/areaEquals' ); - const perimeterEqualsString = require( 'string!AREA_BUILDER/perimeterEquals' ); - const youBuiltString = require( 'string!AREA_BUILDER/youBuilt' ); - - // constants - const LINE_SPACING = 5; - - /** - * Constructor for the window that shows the user what they built. It is constructed with no contents, and the - * contents are added later when the build spec is set. - * - * @param maxWidth - * @param {Object} [options] - * @constructor - */ - function YouBuiltWindow( maxWidth, options ) { - - FeedbackWindow.call( this, youBuiltString, maxWidth, options ); - - // Keep a snapshot of the currently portrayed build spec so that we can only update the portions that need it. - this.currentBuildSpec = null; - - // area text - this.areaTextNode = new Text( StringUtils.format( areaEqualsString, 99 ), { - font: FeedbackWindow.NORMAL_TEXT_FONT, - top: this.titleNode.bottom + LINE_SPACING - } ); - if ( this.areaTextNode.width + 2 * FeedbackWindow.X_MARGIN > maxWidth ) { - // Scale this text to fit in the window. Not an issue in English, but could be needed in translated versions. - this.areaTextNode.scale( ( maxWidth - 2 * FeedbackWindow.X_MARGIN ) / this.areaTextNode.width ); - } - this.contentNode.addChild( this.areaTextNode ); - - // perimeter text - this.perimeterTextNode = new Text( StringUtils.format( perimeterEqualsString, 99 ), { - font: FeedbackWindow.NORMAL_TEXT_FONT - } ); - if ( this.perimeterTextNode.width + 2 * FeedbackWindow.X_MARGIN > maxWidth ) { - // Scale this text to fit in the window. Not an issue in English, but could be needed in translated versions. - this.perimeterTextNode.scale( ( maxWidth - 2 * FeedbackWindow.X_MARGIN ) / this.perimeterTextNode.width ); - } - // proportion info is initially set to null, added and removed when needed. - this.proportionsInfoNode = null; +import inherit from '../../../../phet-core/js/inherit.js'; +import StringUtils from '../../../../phetcommon/js/util/StringUtils.js'; +import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; +import ColorProportionsPrompt from './ColorProportionsPrompt.js'; +import FeedbackWindow from './FeedbackWindow.js'; + +const areaEqualsString = areaBuilderStrings.areaEquals; +const perimeterEqualsString = areaBuilderStrings.perimeterEquals; +const youBuiltString = areaBuilderStrings.youBuilt; + +// constants +const LINE_SPACING = 5; + +/** + * Constructor for the window that shows the user what they built. It is constructed with no contents, and the + * contents are added later when the build spec is set. + * + * @param maxWidth + * @param {Object} [options] + * @constructor + */ +function YouBuiltWindow( maxWidth, options ) { + + FeedbackWindow.call( this, youBuiltString, maxWidth, options ); - // Handle options, mostly those relating to position. - this.mutate( options ); + // Keep a snapshot of the currently portrayed build spec so that we can only update the portions that need it. + this.currentBuildSpec = null; + + // area text + this.areaTextNode = new Text( StringUtils.format( areaEqualsString, 99 ), { + font: FeedbackWindow.NORMAL_TEXT_FONT, + top: this.titleNode.bottom + LINE_SPACING + } ); + if ( this.areaTextNode.width + 2 * FeedbackWindow.X_MARGIN > maxWidth ) { + // Scale this text to fit in the window. Not an issue in English, but could be needed in translated versions. + this.areaTextNode.scale( ( maxWidth - 2 * FeedbackWindow.X_MARGIN ) / this.areaTextNode.width ); } + this.contentNode.addChild( this.areaTextNode ); - areaBuilder.register( 'YouBuiltWindow', YouBuiltWindow ); + // perimeter text + this.perimeterTextNode = new Text( StringUtils.format( perimeterEqualsString, 99 ), { + font: FeedbackWindow.NORMAL_TEXT_FONT + } ); + if ( this.perimeterTextNode.width + 2 * FeedbackWindow.X_MARGIN > maxWidth ) { + // Scale this text to fit in the window. Not an issue in English, but could be needed in translated versions. + this.perimeterTextNode.scale( ( maxWidth - 2 * FeedbackWindow.X_MARGIN ) / this.perimeterTextNode.width ); + } - return inherit( FeedbackWindow, YouBuiltWindow, { + // proportion info is initially set to null, added and removed when needed. + this.proportionsInfoNode = null; - // @private - proportionSpecsAreEqual: function( buildSpec1, buildSpec2 ) { + // Handle options, mostly those relating to position. + this.mutate( options ); +} - // If one of the build specs is null and the other isn't, they aren't equal. - if ( ( buildSpec1 === null && buildSpec2 !== null ) || ( buildSpec1 !== null && buildSpec2 === null ) ) { - return false; - } +areaBuilder.register( 'YouBuiltWindow', YouBuiltWindow ); - // If one has a proportions spec and the other doesn't, they aren't equal. - if ( ( buildSpec1.proportions && !buildSpec2.proportions ) || ( !buildSpec1.proportions && buildSpec2.proportions ) ) { - return false; - } +export default inherit( FeedbackWindow, YouBuiltWindow, { - // If they both don't have a proportions spec, they are equal. - if ( !buildSpec1.proportions && !buildSpec2.proportions ) { - return true; - } + // @private + proportionSpecsAreEqual: function( buildSpec1, buildSpec2 ) { - // At this point, both build specs appear to have proportions fields. Verify that the fields are correct. - assert && assert( buildSpec1.proportions.color1 && buildSpec1.proportions.color2 && buildSpec1.proportions.color1Proportion, - 'malformed proportions specification' ); - assert && assert( buildSpec2.proportions.color1 && buildSpec2.proportions.color2 && buildSpec2.proportions.color1Proportion, - 'malformed proportions specification' ); - - // Return true if all elements of both proportions specs match, false otherwise. - return ( buildSpec1.proportions.color1.equals( buildSpec2.proportions.color1 ) && - buildSpec1.proportions.color2.equals( buildSpec2.proportions.color2 ) && - buildSpec1.proportions.color1Proportion.equals( buildSpec2.proportions.color1Proportion ) ); - }, - - // @public Sets the build spec that is currently being portrayed in the window. - setBuildSpec: function( buildSpec ) { - - // Set the area value, which is always shown. - this.areaTextNode.text = StringUtils.format( areaEqualsString, buildSpec.area ); - - // If proportions have changed, update them. They sit beneath the area in the layout so that it is clear that - // they go together. - if ( !this.proportionSpecsAreEqual( buildSpec, this.currentBuildSpec ) ) { - if ( this.proportionsInfoNode ) { - this.contentNode.removeChild( this.proportionsInfoNode ); - this.proportionsInfoNode = null; - } - if ( buildSpec.proportions ) { - this.proportionsInfoNode = new ColorProportionsPrompt( buildSpec.proportions.color1, - buildSpec.proportions.color2, buildSpec.proportions.color1Proportion, { - top: this.areaTextNode.bottom + LINE_SPACING, - multiLine: true - }, { - font: new PhetFont( 14 ) - } ); - this.contentNode.addChild( this.proportionsInfoNode ); - } - } + // If one of the build specs is null and the other isn't, they aren't equal. + if ( ( buildSpec1 === null && buildSpec2 !== null ) || ( buildSpec1 !== null && buildSpec2 === null ) ) { + return false; + } + + // If one has a proportions spec and the other doesn't, they aren't equal. + if ( ( buildSpec1.proportions && !buildSpec2.proportions ) || ( !buildSpec1.proportions && buildSpec2.proportions ) ) { + return false; + } + + // If they both don't have a proportions spec, they are equal. + if ( !buildSpec1.proportions && !buildSpec2.proportions ) { + return true; + } - // If perimeter is specified, update it, otherwise hide it. - if ( typeof( buildSpec.perimeter ) !== 'undefined' ) { - if ( !this.contentNode.hasChild( this.perimeterTextNode ) ) { - this.contentNode.addChild( this.perimeterTextNode ); - } - this.perimeterTextNode.text = StringUtils.format( perimeterEqualsString, buildSpec.perimeter ); - this.perimeterTextNode.visible = true; - this.perimeterTextNode.top = ( this.proportionsInfoNode ? this.proportionsInfoNode.bottom : this.areaTextNode.bottom ) + LINE_SPACING; + // At this point, both build specs appear to have proportions fields. Verify that the fields are correct. + assert && assert( buildSpec1.proportions.color1 && buildSpec1.proportions.color2 && buildSpec1.proportions.color1Proportion, + 'malformed proportions specification' ); + assert && assert( buildSpec2.proportions.color1 && buildSpec2.proportions.color2 && buildSpec2.proportions.color1Proportion, + 'malformed proportions specification' ); + + // Return true if all elements of both proportions specs match, false otherwise. + return ( buildSpec1.proportions.color1.equals( buildSpec2.proportions.color1 ) && + buildSpec1.proportions.color2.equals( buildSpec2.proportions.color2 ) && + buildSpec1.proportions.color1Proportion.equals( buildSpec2.proportions.color1Proportion ) ); + }, + + // @public Sets the build spec that is currently being portrayed in the window. + setBuildSpec: function( buildSpec ) { + + // Set the area value, which is always shown. + this.areaTextNode.text = StringUtils.format( areaEqualsString, buildSpec.area ); + + // If proportions have changed, update them. They sit beneath the area in the layout so that it is clear that + // they go together. + if ( !this.proportionSpecsAreEqual( buildSpec, this.currentBuildSpec ) ) { + if ( this.proportionsInfoNode ) { + this.contentNode.removeChild( this.proportionsInfoNode ); + this.proportionsInfoNode = null; } - else if ( this.contentNode.hasChild( this.perimeterTextNode ) ) { - this.contentNode.removeChild( this.perimeterTextNode ); + if ( buildSpec.proportions ) { + this.proportionsInfoNode = new ColorProportionsPrompt( buildSpec.proportions.color1, + buildSpec.proportions.color2, buildSpec.proportions.color1Proportion, { + top: this.areaTextNode.bottom + LINE_SPACING, + multiLine: true + }, { + font: new PhetFont( 14 ) + } ); + this.contentNode.addChild( this.proportionsInfoNode ); } + } - // Save a reference to this build spec. - this.currentBuildSpec = buildSpec; + // If perimeter is specified, update it, otherwise hide it. + if ( typeof ( buildSpec.perimeter ) !== 'undefined' ) { + if ( !this.contentNode.hasChild( this.perimeterTextNode ) ) { + this.contentNode.addChild( this.perimeterTextNode ); + } + this.perimeterTextNode.text = StringUtils.format( perimeterEqualsString, buildSpec.perimeter ); + this.perimeterTextNode.visible = true; + this.perimeterTextNode.top = ( this.proportionsInfoNode ? this.proportionsInfoNode.bottom : this.areaTextNode.bottom ) + LINE_SPACING; } - } ); + else if ( this.contentNode.hasChild( this.perimeterTextNode ) ) { + this.contentNode.removeChild( this.perimeterTextNode ); + } + + // Save a reference to this build spec. + this.currentBuildSpec = buildSpec; + } } ); \ No newline at end of file diff --git a/js/game/view/YouEnteredWindow.js b/js/game/view/YouEnteredWindow.js index 6d424bd..7533912 100644 --- a/js/game/view/YouEnteredWindow.js +++ b/js/game/view/YouEnteredWindow.js @@ -6,52 +6,48 @@ * * @author John Blanco */ -define( require => { - 'use strict'; - - // modules - const areaBuilder = require( 'AREA_BUILDER/areaBuilder' ); - const FeedbackWindow = require( 'AREA_BUILDER/game/view/FeedbackWindow' ); - const inherit = require( 'PHET_CORE/inherit' ); - const Text = require( 'SCENERY/nodes/Text' ); - - // strings - const youEnteredString = require( 'string!AREA_BUILDER/youEntered' ); - - // constants - const LINE_SPACING = 5; - - /** - * Constructor for the window that shows the user what they built. It is constructed with no contents, and the - * contents are added later when the build spec is set. - * - * @param maxWidth - * @param {Object} [options] - * @constructor - */ - function YouEnteredWindow( maxWidth, options ) { - - FeedbackWindow.call( this, youEnteredString, maxWidth, options ); - - // value entered text - this.valueEnteredNode = new Text( ( 99 ), { - font: FeedbackWindow.NORMAL_TEXT_FONT, - top: this.titleNode.bottom + LINE_SPACING - } ); - this.contentNode.addChild( this.valueEnteredNode ); - - // Handle options, mostly those relating to position. - this.mutate( options ); - } - areaBuilder.register( 'YouEnteredWindow', YouEnteredWindow ); +import inherit from '../../../../phet-core/js/inherit.js'; +import Text from '../../../../scenery/js/nodes/Text.js'; +import areaBuilderStrings from '../../area-builder-strings.js'; +import areaBuilder from '../../areaBuilder.js'; +import FeedbackWindow from './FeedbackWindow.js'; + +const youEnteredString = areaBuilderStrings.youEntered; + +// constants +const LINE_SPACING = 5; + +/** + * Constructor for the window that shows the user what they built. It is constructed with no contents, and the + * contents are added later when the build spec is set. + * + * @param maxWidth + * @param {Object} [options] + * @constructor + */ +function YouEnteredWindow( maxWidth, options ) { - return inherit( FeedbackWindow, YouEnteredWindow, { + FeedbackWindow.call( this, youEnteredString, maxWidth, options ); - // @public - setValueEntered: function( valueEntered ) { - this.valueEnteredNode.text = valueEntered.toString(); - this.valueEnteredNode.centerX = this.titleNode.centerX; - } + // value entered text + this.valueEnteredNode = new Text( ( 99 ), { + font: FeedbackWindow.NORMAL_TEXT_FONT, + top: this.titleNode.bottom + LINE_SPACING } ); + this.contentNode.addChild( this.valueEnteredNode ); + + // Handle options, mostly those relating to position. + this.mutate( options ); +} + +areaBuilder.register( 'YouEnteredWindow', YouEnteredWindow ); + +export default inherit( FeedbackWindow, YouEnteredWindow, { + + // @public + setValueEntered: function( valueEntered ) { + this.valueEnteredNode.text = valueEntered.toString(); + this.valueEnteredNode.centerX = this.titleNode.centerX; + } } ); \ No newline at end of file