Skip to content

Commit

Permalink
Generalize the blocks, add query param to select starting block confi…
Browse files Browse the repository at this point in the history
…guration, see #216
  • Loading branch information
chrisklus committed Sep 16, 2019
1 parent 5b03dab commit dc3e4ed
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 125 deletions.
13 changes: 13 additions & 0 deletions js/common/EFACQueryParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ define( require => {
'spiral', // use the spiral algorithm, which is after than repulsive but doesn't generally look as good
'simple' // use the simple algorithm, which just puts all chunks in the center of the slice
]
},

// select the startup block configuration
// public facing for phet-io clients
blocks: {
type: 'array',
defaultValue: [ 'iron', 'brick' ],
elementSchema: { type: 'string' },
isValidValue: values => {
return values.length <= 4 && values.every( value => {
return value === 'iron' || value === 'brick';
} );
}
}

} );
Expand Down
4 changes: 4 additions & 0 deletions js/common/model/HeatTransferConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ define( require => {

// constants
const BRICK_IRON_HEAT_TRANSFER_FACTOR = 1000.0;
const BRICK_BRICK_HEAT_TRANSFER_FACTOR = 1000.0;
const BRICK_WATER_HEAT_TRANSFER_FACTOR = 1000.0;
const BRICK_OLIVE_OIL_HEAT_TRANSFER_FACTOR = 1000.0;
const BRICK_AIR_HEAT_TRANSFER_FACTOR = 30.0;
const IRON_WATER_HEAT_TRANSFER_FACTOR = 1000.0;
const IRON_IRON_HEAT_TRANSFER_FACTOR = 1000.0;
const IRON_OLIVE_OIL_HEAT_TRANSFER_FACTOR = 1000.0;
const IRON_AIR_HEAT_TRANSFER_FACTOR = 30.0;
const WATER_OLIVE_OIL_HEAT_TRANSFER_FACTOR = 1000.0;
Expand All @@ -34,12 +36,14 @@ define( require => {
const heatTransferConstantsMap = {
'IRON': {
'BRICK': BRICK_IRON_HEAT_TRANSFER_FACTOR,
'IRON' : IRON_IRON_HEAT_TRANSFER_FACTOR,
'WATER': IRON_WATER_HEAT_TRANSFER_FACTOR,
'OLIVE_OIL': IRON_OLIVE_OIL_HEAT_TRANSFER_FACTOR,
'AIR': IRON_AIR_HEAT_TRANSFER_FACTOR
},
'BRICK': {
'IRON': BRICK_IRON_HEAT_TRANSFER_FACTOR,
'BRICK': BRICK_BRICK_HEAT_TRANSFER_FACTOR,
'AIR': BRICK_AIR_HEAT_TRANSFER_FACTOR,
'WATER': BRICK_WATER_HEAT_TRANSFER_FACTOR,
'OLIVE_OIL': BRICK_OLIVE_OIL_HEAT_TRANSFER_FACTOR
Expand Down
14 changes: 13 additions & 1 deletion js/intro/EFACIntroScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ define( require => {
'use strict';

// modules
const BlockType = require( 'ENERGY_FORMS_AND_CHANGES/intro/model/BlockType' );
const EFACConstants = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACConstants' );
const EFACIntroModel = require( 'ENERGY_FORMS_AND_CHANGES/intro/model/EFACIntroModel' );
const EFACIntroScreenView = require( 'ENERGY_FORMS_AND_CHANGES/intro/view/EFACIntroScreenView' );
const EFACQueryParameters = require( 'ENERGY_FORMS_AND_CHANGES/common/EFACQueryParameters' );
const energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
const Image = require( 'SCENERY/nodes/Image' );
const Property = require( 'AXON/Property' );
Expand Down Expand Up @@ -40,7 +42,17 @@ define( require => {
};

super(
() => new EFACIntroModel( tandem.createTandem( 'model' ) ),
() => new EFACIntroModel(
EFACQueryParameters.blocks.map( blockString => {
if ( blockString === 'iron' ) {
return BlockType.IRON;
}
else if ( blockString === 'brick' ) {
return BlockType.BRICK;
}
} ),
tandem.createTandem( 'model' )
),
model => new EFACIntroScreenView( model, tandem.createTandem( 'view' ) ),
options
);
Expand Down
151 changes: 88 additions & 63 deletions js/intro/model/EFACIntroModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ define( require => {
// (a beaker) when it's sitting at one of the outer snap-to spots on the ground, in meters
const EDGE_PAD = 0.016;

// number of snap-to spots on the ground, should match number of thermal containers
// number of snap-to spots on the ground, should not be greater than the number of thermal containers
const NUMBER_OF_GROUND_SPOTS = 6;

// initial thermometer location, intended to be away from any model objects so that they don't get stuck to anything
Expand All @@ -54,9 +54,11 @@ define( require => {
class EFACIntroModel {

/**
*
* @param {BlockType[]} blocksToCreate
* @param {Tandem} tandem
*/
constructor( tandem ) {
constructor( blocksToCreate, tandem ) {

// @public {BooleanProperty} - controls whether the energy chunks are visible in the view
this.energyChunksVisibleProperty = new BooleanProperty( false, {
Expand Down Expand Up @@ -94,24 +96,46 @@ define( require => {
);
}

// @public (read-only) {Block}
this.ironBlock = new Block(
new Vector2( this.groundSpotXPositions[ 0 ], 0 ),
this.energyChunksVisibleProperty,
BlockType.IRON,
tandem.createTandem( 'ironBlock' )
);
// iterate through the blocks and count how many of each type there are
let brickCount = 0;
let ironCount = 0;
blocksToCreate.forEach( blockType => {
blockType === BlockType.BRICK && brickCount++;
blockType === BlockType.IRON && ironCount++;
} );

// @public (read-only) {Block}
this.brick = new Block(
new Vector2( this.groundSpotXPositions[ 1 ], 0 ),
this.energyChunksVisibleProperty,
BlockType.BRICK,
tandem.createTandem( 'brick' )
);
// @public (read-only) {boolean}
this.moreThanOneBrick = brickCount > 1;
this.moreThanOneIron = ironCount > 1;

// @public (read-only) {Block[]} - list of all blocks in sim
this.blocks = [ this.brick, this.ironBlock ];
this.blocks = [];

// reset counts for proper tandem names
brickCount = 0;
ironCount = 0;
let groundSpotPositionIndex = 0;

// create the blocks
blocksToCreate.forEach( blockType => {
let blockTandemName = '';
if ( blockType === BlockType.BRICK ) {
blockTandemName = BlockType.BRICK.name.toLowerCase();
blockTandemName = this.moreThanOneBrick ? blockTandemName += ++brickCount : blockTandemName;
}
else if ( blockType === BlockType.IRON ) {
blockTandemName = BlockType.IRON.name.toLowerCase() + 'Block';
blockTandemName = this.moreThanOneIron ? blockTandemName += ++ironCount : blockTandemName;
}

const block = new Block(
new Vector2( this.groundSpotXPositions[ groundSpotPositionIndex++ ], 0 ),
this.energyChunksVisibleProperty,
blockType,
tandem.createTandem( blockTandemName )
);
this.blocks.push( block );
} );

// @public (read-only) {Burner} - right and left burners
this.leftBurner = new Burner(
Expand All @@ -122,17 +146,15 @@ define( require => {
this.rightBurner = new Burner(
new Vector2( this.groundSpotXPositions[ 3 ], 0 ),
this.energyChunksVisibleProperty,
tandem.createTandem( 'rightBurner' )
);

const listOfThingsThatCanGoInBeaker = [ this.brick, this.ironBlock ];
tandem.createTandem( 'rightBurner' )
);

// @public (read-only) {BeakerContainer)
this.waterBeaker = new BeakerContainer(
new Vector2( this.groundSpotXPositions[ 4 ], 0 ),
BEAKER_WIDTH,
BEAKER_HEIGHT,
listOfThingsThatCanGoInBeaker,
this.blocks,
this.energyChunksVisibleProperty, {
majorTickMarkDistance: BEAKER_MAJOR_TICK_MARK_DISTANCE,
tandem: tandem.createTandem( 'waterBeaker' )
Expand All @@ -144,7 +166,7 @@ define( require => {
new Vector2( this.groundSpotXPositions[ 5 ], 0 ),
BEAKER_WIDTH,
BEAKER_HEIGHT,
listOfThingsThatCanGoInBeaker,
this.blocks,
this.energyChunksVisibleProperty, {
fluidColor: EFACConstants.OLIVE_OIL_COLOR_IN_BEAKER,
steamColor: EFACConstants.OLIVE_OIL_STEAM_COLOR,
Expand All @@ -161,7 +183,7 @@ define( require => {
this.beakers = [ this.waterBeaker, this.oliveOilBeaker ];

// @private {RectangularThermalMovableModelElement[]} - put all the thermal containers on a list for easy iteration
this.thermalContainers = [ this.brick, this.ironBlock, this.waterBeaker, this.oliveOilBeaker ];
this.thermalContainers = [ ...this.blocks, ...this.beakers ];

// @private {Object} - an object that is used to track which thermal containers are in contact with one another in
// each model step.
Expand All @@ -174,7 +196,7 @@ define( require => {
this.burners = [ this.rightBurner, this.leftBurner ];

// @private {ModelElement} - put all of the model elements on a list for easy iteration
this.modelElementList = [ this.leftBurner, this.rightBurner, this.brick, this.ironBlock, this.waterBeaker, this.oliveOilBeaker ];
this.modelElementList = [ this.leftBurner, this.rightBurner, ...this.thermalContainers ];

// @public (read-only) {StickyTemperatureAndColorSensor[]}
this.thermometers = [];
Expand All @@ -188,43 +210,45 @@ define( require => {
);
this.thermometers.push( thermometer );

// Add handling for a special case where the user drops something (generally a block) in the beaker behind this
// thermometer. The action is to automatically move the thermometer to a location where it continues to sense
// the beaker temperature. This was requested after interviews.
thermometer.sensedElementColorProperty.link( ( newColor, oldColor ) => {

this.beakers.forEach( beaker => {
const blockWidthIncludingPerspective = this.ironBlock.getProjectedShape().bounds.width;

const xRange = new Range(
beaker.getBounds().centerX - blockWidthIncludingPerspective / 2,
beaker.getBounds().centerX + blockWidthIncludingPerspective / 2
);

const checkBlocks = block => {

// see if one of the blocks is being sensed in the beaker
return block.color === newColor && block.positionProperty.value.y > beaker.positionProperty.value.y;
};

// if the new color matches any of the blocks (which are the only things that can go in a beaker), and the
// thermometer was previously stuck to the beaker and sensing its fluid, then move it to the side of the beaker
if ( _.some( this.blocks, checkBlocks ) &&
oldColor === beaker.fluidColor &&
!thermometer.userControlledProperty.get() &&
!beaker.userControlledProperty.get() &&
xRange.contains( thermometer.positionProperty.value.x ) ) {

// fake a movement by the user to a point in the beaker where the thermometer is not over a brick
thermometer.userControlledProperty.set( true ); // must toggle userControlled to enable element following
thermometer.positionProperty.value = new Vector2(
beaker.getBounds().maxX - 0.01,
beaker.getBounds().minY + beaker.getBounds().height * 0.33
// Add handling for a special case where the user drops a block in the beaker behind this thermometer. The
// action is to automatically move the thermometer to a location where it continues to sense the beaker
// temperature. Not needed if zero blocks are in use. This was requested after interviews.
if ( this.blocks.length ) {
thermometer.sensedElementColorProperty.link( ( newColor, oldColor ) => {

this.beakers.forEach( beaker => {
const blockWidthIncludingPerspective = this.blocks[ 0 ].getProjectedShape().bounds.width;

const xRange = new Range(
beaker.getBounds().centerX - blockWidthIncludingPerspective / 2,
beaker.getBounds().centerX + blockWidthIncludingPerspective / 2
);
thermometer.userControlledProperty.set( false );
}

const checkBlocks = block => {

// see if one of the blocks is being sensed in the beaker
return block.color === newColor && block.positionProperty.value.y > beaker.positionProperty.value.y;
};

// if the new color matches any of the blocks (which are the only things that can go in a beaker), and the
// thermometer was previously stuck to the beaker and sensing its fluid, then move it to the side of the beaker
if ( _.some( this.blocks, checkBlocks ) &&
oldColor === beaker.fluidColor &&
!thermometer.userControlledProperty.get() &&
!beaker.userControlledProperty.get() &&
xRange.contains( thermometer.positionProperty.value.x ) ) {

// fake a movement by the user to a point in the beaker where the thermometer is not over a brick
thermometer.userControlledProperty.set( true ); // must toggle userControlled to enable element following
thermometer.positionProperty.value = new Vector2(
beaker.getBounds().maxX - 0.01,
beaker.getBounds().minY + beaker.getBounds().height * 0.33
);
thermometer.userControlledProperty.set( false );
}
} );
} );
} );
}
} );

// @private {EnergyBalanceTracker} - This is used to track energy exchanges between all of the various energy
Expand Down Expand Up @@ -283,8 +307,9 @@ define( require => {
this.air.reset();
this.leftBurner.reset();
this.rightBurner.reset();
this.ironBlock.reset();
this.brick.reset();
this.blocks.forEach( block => {
block.reset();
} );
this.waterBeaker.reset();
this.oliveOilBeaker.reset();
this.thermometers.forEach( thermometer => {
Expand Down Expand Up @@ -342,7 +367,7 @@ define( require => {

// update the fluid level in the beaker, which could be displaced by one or more of the blocks
this.beakers.forEach( beaker => {
beaker.updateFluidDisplacement( [ this.brick.getBounds(), this.ironBlock.getBounds() ] );
beaker.updateFluidDisplacement( this.blocks.map( block => block.getBounds() ) );
} );

//=====================================================================
Expand Down
40 changes: 16 additions & 24 deletions js/intro/view/BeakerContainerView.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ define( require => {

// @private - These values are used for calculating the clipping caused by the presence of blocks in the beaker.
// They are computed once here so that they don't have to be recomputed every time the clipping shape is updated.
// This assumes the blocks are all the same size and do not change size.
this.blockWidthInView = modelViewTransform.modelToViewDeltaX( model.brick.width );
this.blockHeightInView = -modelViewTransform.modelToViewDeltaY( model.brick.height );
const perspectiveEdgeSize = this.blockWidthInView * BLOCK_PERSPECTIVE_EDGE_PROPORTION;
this.forwardProjectionVector = new Vector2( -perspectiveEdgeSize / 2, 0 ).rotated( -BLOCK_PERSPECTIVE_ANGLE );
// This assumes the blocks are all the same size and do not change size. Only needed if any blocks exist.
if ( model.blocks.length ) {
this.blockWidthInView = modelViewTransform.modelToViewDeltaX( model.blocks[ 0 ].width );
this.blockHeightInView = -modelViewTransform.modelToViewDeltaY( model.blocks[ 0 ].height );
const perspectiveEdgeSize = this.blockWidthInView * BLOCK_PERSPECTIVE_EDGE_PROPORTION;
this.forwardProjectionVector = new Vector2( -perspectiveEdgeSize / 2, 0 ).rotated( -BLOCK_PERSPECTIVE_ANGLE );
}

if ( EFACQueryParameters.showHelperShapes ) {
this.clipAreaHelperNode = new Path( this.untransformedBeakerClipShape, {
Expand Down Expand Up @@ -140,24 +142,22 @@ define( require => {

/**
* Add shapes corresponded to the provided blocks to the provide clip area shape, accounting for any 3D projection
* used for the blocks. This essentially creates "holes" in the clip mask preventing anything in the parent node
* (generally energy chunks) from being rendered in the same place as the blocks.
* used for the blocks. This essentially creates "holes" in the clip mask preventing anything in the parent node
* (generally energy chunks) from being rendered in the same place as the blocks. This method can handle any number
* of blocks stacked in the beaker, but only clips for the bottom two, since the beaker can only fit two blocks,
* plus a tiny bit of a third.
* @param {Block[]} blocks
* @param {Shape} clipAreaShape
* @param {ModelViewTransform2} modelViewTransform
* @private
*/
addProjectedBlocksToClipArea( blocks, clipAreaShape, modelViewTransform ) {

// Make sure there aren't more blocks than this method can deal with. There are some assumptions built in that
// would not work for more than two blocks, see the code and comments below for details.
assert && assert( blocks.length <= 2, 'number of blocks exceeds what this method is designed to handle' );

// hoisted block variable
let block;

// if neither of the blocks is in the beaker then there are no "holes" to add, use C-style loop for performance
const blocksInBeaker = [];
let blocksInBeaker = [];
for ( let i = 0; i < blocks.length; i++ ) {
block = blocks[ i ];
if ( this.beaker.getBounds().containsPoint( block.positionProperty.value ) ||
Expand All @@ -176,24 +176,16 @@ define( require => {

// determine whether the blocks are stacked upon each other
let blocksAreStacked = false;
if ( blocksInBeaker.length === 2 ) {
blocksAreStacked = blocksInBeaker[ 0 ].isStackedUpon( blocksInBeaker[ 1 ] ) ||
blocksInBeaker[ 1 ].isStackedUpon( blocksInBeaker[ 0 ] );
if ( blocksInBeaker.length > 1 ) {
blocksInBeaker = _.sortBy( blocksInBeaker, block => block.positionProperty.value.y );
blocksAreStacked = blocksInBeaker[ 1 ].isStackedUpon( blocksInBeaker[ 0 ] );
}

if ( blocksAreStacked ) {

// When the blocks are stacked, draw a single shape that encompasses both. This is necessary because if the
// shapes are drawn separately and they overlap, a space is created where the energy chunks are not occluded.
let bottomBlock;
if ( blocksInBeaker[ 0 ].isStackedUpon( blocksInBeaker[ 1 ] ) ) {
bottomBlock = blocksInBeaker[ 1 ];
}
else {
bottomBlock = blocksInBeaker[ 0 ];
}

const bottomBlockPositionInView = modelViewTransform.modelToViewPosition( bottomBlock.positionProperty.value );
const bottomBlockPositionInView = modelViewTransform.modelToViewPosition( blocksInBeaker[ 0 ].positionProperty.value );

if ( chipAreaShapeBounds.containsPoint( bottomBlockPositionInView ) ) {
clipAreaShape.moveTo(
Expand Down
Loading

0 comments on commit dc3e4ed

Please sign in to comment.