Skip to content

Commit

Permalink
an initial attempt at calibrating the device, see #18
Browse files Browse the repository at this point in the history
  • Loading branch information
jessegreenberg committed Oct 25, 2021
1 parent b48785c commit 05238d0
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 41 deletions.
7 changes: 6 additions & 1 deletion js/quadrilateral/QuadrilateralQueryParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ const QuadrilateralQueryParameters = QueryStringMachine.getAll( {
// if provided, a second screen is added to support a demo for calibrating the sim to an external device,
// giving us the information to set the positions of vertices in the sim from length and angle information
// from a device
calibrationDemo: { type: 'flag' }
calibrationDemo: { type: 'flag' },

// If provided, the model will change slightly to act more like a "physical" device. The model coordinates
// may change from arbitrary model space to example coordinates provided by the device. To be used in combination
// with calibrationDemo, since one of the screens needs to act more like the device.
calibrationDemoDevice: { type: 'flag' }
} );

quadrilateral.register( 'QuadrilateralQueryParameters', QuadrilateralQueryParameters );
Expand Down
104 changes: 76 additions & 28 deletions js/quadrilateral/model/QuadrilateralModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
* @author Jesse Greenberg (PhET Interactive Simulations)
*/

import BooleanProperty from '../../../../axon/js/BooleanProperty.js';
import DerivedProperty from '../../../../axon/js/DerivedProperty.js';
import Emitter from '../../../../axon/js/Emitter.js';
import NumberProperty from '../../../../axon/js/NumberProperty.js';
import Property from '../../../../axon/js/Property.js';
import Bounds2 from '../../../../dot/js/Bounds2.js';
import LinearFunction from '../../../../dot/js/LinearFunction.js';
import Range from '../../../../dot/js/Range.js';
import Utils from '../../../../dot/js/Utils.js';
import Vector2 from '../../../../dot/js/Vector2.js';
import merge from '../../../../phet-core/js/merge.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import BooleanIO from '../../../../tandem/js/types/BooleanIO.js';
import quadrilateral from '../../quadrilateral.js';
import QuadrilateralQueryParameters from '../QuadrilateralQueryParameters.js';
import Side from './Side.js';
import Vertex from './Vertex.js';

// the full bounds of the model, vertices are constrained to these values
const MODEL_BOUNDS = new Bounds2( -1, -1, 1, 1 );
const MODEL_BOUNDS = QuadrilateralQueryParameters.calibrationDemoDevice ?
new Bounds2( -4.5, -4.5, 4.5, 4.5 ) : new Bounds2( -1, -1, 1, 1 );

class QuadrilateralModel {

Expand Down Expand Up @@ -66,6 +70,14 @@ class QuadrilateralModel {
this.leftSide.connectToSide( this.bottomSide );
this.topSide.connectToSide( this.leftSide );

// @public {Bounds2|null} - The Bounds provided by the physical model, so we know how to map the physical model
// bounds to the model space (MODEL_BOUNDS)
this.physicalModelBoundsProperty = new Property( null );

// @public {BooleanProperty} - If true, the simulation is "calibrating" to a physical device so we don't set the
// vertex positions in response to changes from the physical device. Instead we are updating physicalModelBounds.
this.isCalibratingProperty = new BooleanProperty( false );

window.simModel = this;

// @public {NumberProperty} - A value that controls the threshold for equality when determining
Expand Down Expand Up @@ -168,33 +180,69 @@ class QuadrilateralModel {
assert && assert( this.vertex3.dragBoundsProperty.value );
assert && assert( this.vertex4.dragBoundsProperty.value );

// vertex1 and the topLine are anchored, the rest of the shape is relative to this
const vector1Position = new Vector2( MODEL_BOUNDS.minX, MODEL_BOUNDS.maxX );
const vector2Position = new Vector2( vector1Position.x + topLength, vector1Position.y );

const vector4Offset = new Vector2( Math.cos( -p1Angle ), Math.sin( -p1Angle ) ).timesScalar( leftLength );
const vector4Position = vector1Position.plus( vector4Offset );

const vector3Offset = new Vector2( Math.cos( Math.PI + p2Angle ), Math.sin( Math.PI + p2Angle ) ).timesScalar( rightLength );
const vector3Position = vector2Position.plus( vector3Offset );

// make sure that the proposed positions are within bounds defined in the simulation model
const shapePosition1 = this.vertex1.dragBoundsProperty.value.closestPointTo( vector1Position );
const shapePosition2 = this.vertex2.dragBoundsProperty.value.closestPointTo( vector2Position );
const shapePosition3 = this.vertex3.dragBoundsProperty.value.closestPointTo( vector3Position );
const shapePosition4 = this.vertex4.dragBoundsProperty.value.closestPointTo( vector4Position );
const shapePositions = [ shapePosition1, shapePosition2, shapePosition3, shapePosition4 ];

// we have the vertex positions to recreate the shape, but shift them so that the centroid of the quadrilateral is
// in the center of the model space
const centroidPosition = this.getCentroidFromPositions( shapePositions );
const centroidOffset = centroidPosition.negated();
const shiftedPositions = _.map( shapePositions, shapePosition => shapePosition.plus( centroidOffset ) );

this.vertex1.positionProperty.set( shiftedPositions[ 0 ] );
this.vertex2.positionProperty.set( shiftedPositions[ 1 ] );
this.vertex3.positionProperty.set( shiftedPositions[ 2 ] );
this.vertex4.positionProperty.set( shiftedPositions[ 3 ] );
// you must calibrate before setting positions from a physical device
if ( this.physicalModelBoundsProperty.value !== null && !this.isCalibratingProperty.value ) {


// the physical device lengths can only become half as long as the largest length, so map to the sim model
// with that constraint as well so that the smallest shape on the physical device doesn't bring vertices
// all the way to the center of the screen (0, 0).
const deviceLengthToSimLength = new LinearFunction( 0, this.physicalModelBoundsProperty.value.width, 0, MODEL_BOUNDS.width );

const mappedTopLength = deviceLengthToSimLength( topLength );
const mappedRightLength = deviceLengthToSimLength( rightLength );
const mappedLeftLength = deviceLengthToSimLength( leftLength );

// vertex1 and the topLine are anchored, the rest of the shape is relative to this
const vector1Position = new Vector2( MODEL_BOUNDS.minX, MODEL_BOUNDS.maxX );
const vector2Position = new Vector2( vector1Position.x + mappedTopLength, vector1Position.y );

const vector4Offset = new Vector2( Math.cos( -p1Angle ), Math.sin( -p1Angle ) ).timesScalar( mappedLeftLength );
const vector4Position = vector1Position.plus( vector4Offset );

const vector3Offset = new Vector2( Math.cos( Math.PI + p2Angle ), Math.sin( Math.PI + p2Angle ) ).timesScalar( mappedRightLength );
const vector3Position = vector2Position.plus( vector3Offset );

// make sure that the proposed positions are within bounds defined in the simulation model
const shapePosition1 = this.vertex1.dragBoundsProperty.value.closestPointTo( vector1Position );
const shapePosition2 = this.vertex2.dragBoundsProperty.value.closestPointTo( vector2Position );
const shapePosition3 = this.vertex3.dragBoundsProperty.value.closestPointTo( vector3Position );
const shapePosition4 = this.vertex4.dragBoundsProperty.value.closestPointTo( vector4Position );
const shapePositions = [ shapePosition1, shapePosition2, shapePosition3, shapePosition4 ];

// we have the vertex positions to recreate the shape, but shift them so that the centroid of the quadrilateral is
// in the center of the model space
const centroidPosition = this.getCentroidFromPositions( shapePositions );
const centroidOffset = centroidPosition.negated();
const shiftedPositions = _.map( shapePositions, shapePosition => shapePosition.plus( centroidOffset ) );

this.vertex1.positionProperty.set( shiftedPositions[ 0 ] );
this.vertex2.positionProperty.set( shiftedPositions[ 1 ] );
this.vertex3.positionProperty.set( shiftedPositions[ 2 ] );
this.vertex4.positionProperty.set( shiftedPositions[ 3 ] );
}
}

/**
* Set the physical model bounds from the device bounds. For now, we assume that the devices is like
* one provided by CHROME lab, where sides are created from a socket and arm such that the largest length
* of one side is when the arm is as far out of the socket as possible and the smallest length is when the
* arm is fully inserted into the socket. In this case the smallest length will be half of the largest length.
* When calibrating, we ask for the largest shape possible, so the minimum lengths are just half these
* provided values. There is also an assumption that the sides are the same and the largest possible shape is a
* square. We create a Bounds2 defined by these constraints
* @public
*
* @param {number} topLength
* @param {number} rightLength
* @param {number} bottomLength
* @param {number} leftLength
*/
setPhysicalModelBounds( topLength, rightLength, bottomLength, leftLength ) {

// assuming a square shape for extrema - we may need a mapping function for each individual side if this cannot be assumed
const maxLength = _.max( [ topLength, rightLength, bottomLength, leftLength ] );
this.physicalModelBoundsProperty.value = new Bounds2( 0, 0, maxLength, maxLength );
}

/**
Expand Down
81 changes: 81 additions & 0 deletions js/quadrilateral/view/CalibrationContentNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2021, University of Colorado Boulder

/**
* @author Jesse Greenberg (PhET Interactive Simulations)
*/

import Bounds2 from '../../../../dot/js/Bounds2.js';
import Utils from '../../../../dot/js/Utils.js';
import merge from '../../../../phet-core/js/merge.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import Circle from '../../../../scenery/js/nodes/Circle.js';
import Line from '../../../../scenery/js/nodes/Line.js';
import Rectangle from '../../../../scenery/js/nodes/Rectangle.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import VBox from '../../../../scenery/js/nodes/VBox.js';
import quadrilateral from '../../quadrilateral.js';

class CalibrationContentNode extends VBox {
constructor( model, options ) {

options = merge( {
align: 'center'
}, options );

const calibrateHintText = new Text( 'Make the device as large as you can, then close the dialog.', {
font: new PhetFont( 24 )
} );

// create a square shape to display the values provided by the quadrilateral model
const viewBounds = new Bounds2( 0, 0, 300, 300 );
const calibrationRectangle = new Rectangle( viewBounds, {
stroke: 'grey'
} );

// vertices
const vertex1Circle = new Circle( 5, {
center: viewBounds.leftTop
} );
const vertex2Circle = new Circle( 5, {
center: viewBounds.rightTop
} );
const vertex3Circle = new Circle( 5, {
center: viewBounds.rightBottom
} );
const vertex4Circle = new Circle( 5, {
center: viewBounds.leftBottom
} );
calibrationRectangle.children = [ vertex1Circle, vertex2Circle, vertex3Circle, vertex4Circle ];

// display of coordinates
const dimensionLineOptions = { stroke: 'grey' };
const heightTickLine = new Line( 0, 0, 0, 300, dimensionLineOptions );
const bottomTickLine = new Line( 0, 0, 10, 0, dimensionLineOptions );
const topTickLine = new Line( 0, 0, 10, 0, dimensionLineOptions );
const leftSideLengthText = new Text( 'null', { font: new PhetFont( { size: 24 } ), rotation: -Math.PI / 2 } );

bottomTickLine.centerTop = heightTickLine.centerBottom;
topTickLine.centerBottom = heightTickLine.centerTop;
heightTickLine.rightCenter = calibrationRectangle.leftCenter.minusXY( 15, 0 );
leftSideLengthText.rightCenter = heightTickLine.leftCenter;

heightTickLine.children = [ bottomTickLine, topTickLine, leftSideLengthText ];

calibrationRectangle.addChild( heightTickLine );

options.children = [
calibrateHintText,
calibrationRectangle
];
super( options );

model.physicalModelBoundsProperty.link( physicalModelBounds => {
if ( physicalModelBounds !== null ) {
leftSideLengthText.text = Utils.toFixed( physicalModelBounds.height, 2 );
}
} );
}
}

quadrilateral.register( 'CalibrationContentNode', CalibrationContentNode );
export default CalibrationContentNode;
24 changes: 19 additions & 5 deletions js/quadrilateral/view/QuadrilateralNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Node from '../../../../scenery/js/nodes/Node.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import quadrilateral from '../../quadrilateral.js';
import quadrilateralStrings from '../../quadrilateralStrings.js';
import QuadrilateralModel from '../model/QuadrilateralModel.js';
import QuadrilateralQueryParameters from '../QuadrilateralQueryParameters.js';
import SideNode from './SideNode.js';
import VertexNode from './VertexNode.js';

Expand Down Expand Up @@ -151,11 +153,23 @@ class QuadrilateralNode extends Node {
// -0.05, -0.05
// );

// TODO: For this test we are not constraining the vertex positions
this.model.vertex1.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex2.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex3.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex4.dragBoundsProperty.value = Bounds2.EVERYTHING;
if ( QuadrilateralQueryParameters.calibrationDemoDevice ) {

// don't allow the device to go out of model bounds
this.model.vertex1.dragBoundsProperty.value = QuadrilateralModel.MODEL_BOUNDS;
this.model.vertex2.dragBoundsProperty.value = QuadrilateralModel.MODEL_BOUNDS;
this.model.vertex3.dragBoundsProperty.value = QuadrilateralModel.MODEL_BOUNDS;
this.model.vertex4.dragBoundsProperty.value = QuadrilateralModel.MODEL_BOUNDS;
}
else {

// TODO: For this test we are not constraining the vertex positions in the sim because we want to see how
// move when attached to a physical model without being bound
this.model.vertex1.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex2.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex3.dragBoundsProperty.value = Bounds2.EVERYTHING;
this.model.vertex4.dragBoundsProperty.value = Bounds2.EVERYTHING;
}
}
}

Expand Down
89 changes: 87 additions & 2 deletions js/quadrilateral/view/QuadrilateralScreenView.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@
*/

import Bounds2 from '../../../../dot/js/Bounds2.js';
import Utils from '../../../../dot/js/Utils.js';
import ScreenView from '../../../../joist/js/ScreenView.js';
import merge from '../../../../phet-core/js/merge.js';
import ModelViewTransform from '../../../../phetcommon/js/view/ModelViewTransform2.js';
import ResetAllButton from '../../../../scenery-phet/js/buttons/ResetAllButton.js';
import PhetColorScheme from '../../../../scenery-phet/js/PhetColorScheme.js';
import PhetFont from '../../../../scenery-phet/js/PhetFont.js';
import Text from '../../../../scenery/js/nodes/Text.js';
import VBox from '../../../../scenery/js/nodes/VBox.js';
import TextPushButton from '../../../../sun/js/buttons/TextPushButton.js';
import Dialog from '../../../../sun/js/Dialog.js';
import Tandem from '../../../../tandem/js/Tandem.js';
import QuadrilateralConstants from '../../common/QuadrilateralConstants.js';
import quadrilateral from '../../quadrilateral.js';
import QuadrilateralModel from '../model/QuadrilateralModel.js';
import QuadrilateralQueryParameters from '../QuadrilateralQueryParameters.js';
import CalibrationContentNode from './CalibrationContentNode.js';
import QuadrilateralNode from './QuadrilateralNode.js';
import QuadrilateralSoundView from './QuadrilateralSoundView.js';
import SideDemonstrationNode from './SideDemonstrationNode.js';
Expand All @@ -40,6 +48,8 @@ class QuadrilateralScreenView extends ScreenView {

super( options );

// the model bounds may or may not be centered (especially when using ?calibrationDemoDevice), but we want
// the model origin (0, 0) to be in the center of the ScreenView
const viewHeight = this.layoutBounds.height - 2 * QuadrilateralConstants.SCREEN_VIEW_Y_MARGIN;
const modelViewTransform = ModelViewTransform.createRectangleInvertedYMapping(
QuadrilateralModel.MODEL_BOUNDS,
Expand Down Expand Up @@ -80,10 +90,85 @@ class QuadrilateralScreenView extends ScreenView {
this.addChild( resetAllButton );

// if in the calibration demo and we are pretending to be a device, make some modifications to the sim
// so that it doesn't look like a sim
if ( options.calibrationDemoDevice ) {
// so that it doesn't look like a sim and add some additional "device" data to the view
if ( QuadrilateralQueryParameters.calibrationDemoDevice ) {
resetAllButton.visible = false;
phet.joist.sim.navigationBar.visible = false;

// add text displaying the data provided by the device
const labelOptions = { font: new PhetFont( { size: 24 } ) };
const topSideText = new Text( '', labelOptions );
const rightSideText = new Text( '', labelOptions );
const bottomSideText = new Text( '', labelOptions );
const leftSideText = new Text( '', labelOptions );

const labelsVBox = new VBox( {
children: [ topSideText, rightSideText, bottomSideText, leftSideText ],
spacing: 15,
align: 'left'
} );
this.addChild( labelsVBox );

const formatLengthText = ( label, lengthValue ) => {
return `${label}: ${Utils.toFixed( lengthValue, 2 )}`;
};

model.topSide.lengthProperty.link( length => {
topSideText.text = formatLengthText( 'Top Side', length );
} );
model.rightSide.lengthProperty.link( length => {
rightSideText.text = formatLengthText( 'Right Side', length );
} );
model.bottomSide.lengthProperty.link( length => {
bottomSideText.text = formatLengthText( 'Bottom Side', length );
} );
model.leftSide.lengthProperty.link( length => {
leftSideText.text = formatLengthText( 'Left Side', length );
} );
}
else {

const calibrationDialog = new Dialog( new CalibrationContentNode( model ), {
title: new Text( 'Calibrate with device', {
font: new PhetFont( { size: 36 } )
} )
} );

calibrationDialog.isShowingProperty.link( ( isShowing, wasShowing ) => {
model.isCalibratingProperty.value = isShowing;

if ( !isShowing && wasShowing !== null ) {

const physicalModelBounds = model.physicalModelBoundsProperty.value;
model.setPositionsFromLengthAndAngleData(
physicalModelBounds.width,
physicalModelBounds.width,
physicalModelBounds.width,
physicalModelBounds.width,
Math.PI / 2,
Math.PI / 2,
Math.PI / 2,
Math.PI / 2
);
}
} );

// this is the "sim", add a button to start calibration
const calibrationButton = new TextPushButton( 'Calibrate Device', {
listener: () => {
calibrationDialog.show();
},

textNodeOptions: {
font: new PhetFont( { size: 36 } )
},
baseColor: PhetColorScheme.BUTTON_YELLOW,

// position is relative to the ResetAllButton for now
rightBottom: resetAllButton.rightTop.minusXY( 0, 15 )
} );

this.addChild( calibrationButton );
}
}

Expand Down
Loading

0 comments on commit 05238d0

Please sign in to comment.