From dba3ffc316a06ebb9827d1453d3d9e048f1b2bde Mon Sep 17 00:00:00 2001 From: Martin Veillette Date: Tue, 27 Jul 2021 10:55:29 -0400 Subject: [PATCH] add positionOffset and dimensions to the Representation (see #24 and #132) --- js/common/model/GeometricOpticsModel.js | 4 +- js/common/model/Representation.js | 14 +-- js/common/model/SourceObject.js | 19 ++-- js/common/model/Target.js | 46 +++++++++- js/common/view/LabelsNode.js | 15 ++-- js/common/view/SourceObjectNode.js | 25 ++++-- js/common/view/TargetNode.js | 114 +++++++++--------------- 7 files changed, 129 insertions(+), 108 deletions(-) diff --git a/js/common/model/GeometricOpticsModel.js b/js/common/model/GeometricOpticsModel.js index 886b39c7..3ab82d6c 100644 --- a/js/common/model/GeometricOpticsModel.js +++ b/js/common/model/GeometricOpticsModel.js @@ -100,10 +100,10 @@ class GeometricOpticsModel { this.secondFocalPoint = new FocalPoint( this.optic.positionProperty, this.optic.focalLengthProperty, tandem, { multiplicativeFactor: -1 } ); // @public {Target} target/ image - this.firstTarget = new Target( this.sourceObject.firstPositionProperty, this.optic, tandem ); + this.firstTarget = new Target( this.sourceObject.firstPositionProperty, this.optic, this.representationProperty, tandem ); // @public {Target} target/ image associated with the second source - this.secondTarget = new Target( this.sourceObject.secondPositionProperty, this.optic, tandem ); + this.secondTarget = new Target( this.sourceObject.secondPositionProperty, this.optic, this.representationProperty, tandem ); // @public {ProjectorScreen} this.projectorScreen = new ProjectorScreen( diff --git a/js/common/model/Representation.js b/js/common/model/Representation.js index 39b8c467..cc87b117 100644 --- a/js/common/model/Representation.js +++ b/js/common/model/Representation.js @@ -21,13 +21,13 @@ import pencilLogoImage from '../../../images/pencil-logo_png.js'; import projectorScreen3dImage from '../../../images/projector-screen-3d_png.js'; import rocket3dLeftFacingInvertedImage from '../../../images/rocket-3d-left-facing-inverted_png.js'; import rocket3dLeftFacingUprightImage from '../../../images/rocket-3d-left-facing-upright_png.js'; -import rocket3dRightFacingUprightImage from '../../../images/rocket-3d-right-facing-upright_png.js'; import rocket3dRightFacingInvertedImage from '../../../images/rocket-3d-right-facing-inverted_png.js'; +import rocket3dRightFacingUprightImage from '../../../images/rocket-3d-right-facing-upright_png.js'; import rocketLogoImage from '../../../images/rocket-logo_png.js'; import tree3dLeftFacingInvertedImage from '../../../images/tree-3d-left-facing-inverted_png.js'; import tree3dLeftFacingUprightImage from '../../../images/tree-3d-left-facing-upright_png.js'; -import tree3dRightFacingUprightImage from '../../../images/tree-3d-right-facing-upright_png.js'; import tree3dRightFacingInvertedImage from '../../../images/tree-3d-right-facing-inverted_png.js'; +import tree3dRightFacingUprightImage from '../../../images/tree-3d-right-facing-upright_png.js'; import treeLogoImage from '../../../images/tree-logo_png.js'; import geometricOptics from '../../geometricOptics.js'; import geometricOpticsStrings from '../../geometricOpticsStrings.js'; @@ -45,7 +45,7 @@ class RepresentationGenerator { * @param {HTMLImageElement} rightFacingInverted * @param {HTMLImageElement} leftFacingUpright * @param {HTMLImageElement} leftFacingInverted - * @param {Dimensions2} dimensions + * @param {Dimension2} dimensions * @param {Vector2} leftFacingUprightOffsetPosition * @param {string} label * @param {boolean} isObject @@ -87,7 +87,7 @@ const Representation = Enumeration.byMap( { pencil3dLeftFacingUprightImage, pencil3dLeftFacingInvertedImage, new Dimension2( 111, 365 ), - new Vector2( -32, 35 ), + new Vector2( -64, 70 ), pencilString, true ), TREE: new RepresentationGenerator( treeLogoImage, tree3dRightFacingUprightImage, @@ -95,7 +95,7 @@ const Representation = Enumeration.byMap( { tree3dLeftFacingUprightImage, tree3dLeftFacingInvertedImage, new Dimension2( 135, 391 ), - new Vector2( -40, 44 ), + new Vector2( -80, 88 ), treeString, true ), ROCKET: new RepresentationGenerator( rocketLogoImage, rocket3dRightFacingUprightImage, @@ -103,7 +103,7 @@ const Representation = Enumeration.byMap( { rocket3dLeftFacingUprightImage, rocket3dLeftFacingInvertedImage, new Dimension2( 116, 414 ), - new Vector2( -34, 56 ), + new Vector2( -68, 112 ), rocketString, true ), LIGHT: new RepresentationGenerator( lampBlueLogoImage, lampBlueImage, @@ -111,7 +111,7 @@ const Representation = Enumeration.byMap( { projectorScreen3dImage, projectorScreen3dImage, new Dimension2( 100, 100 ), - new Vector2( -33, 14 ), + new Vector2( -66, 28 ), lightString, false, { source: lampRedImage } ) } ); diff --git a/js/common/model/SourceObject.js b/js/common/model/SourceObject.js index 898d6398..4045a6bf 100644 --- a/js/common/model/SourceObject.js +++ b/js/common/model/SourceObject.js @@ -20,8 +20,8 @@ import GeometricOpticsConstants from '../GeometricOpticsConstants.js'; const DEFAULT_SOURCE_POINT_1 = GeometricOpticsConstants.DEFAULT_SOURCE_POINT_1; const DEFAULT_SOURCE_POINT_2 = GeometricOpticsConstants.DEFAULT_SOURCE_POINT_2; const verticalOffsetRange = new RangeWithValue( -50, 0, -30 ); // in centimeters -const OBJECT_SCALE_FACTOR = 2; -const SOURCE_SCALE_FACTOR = 1; +const OBJECT_SCALE_FACTOR = 4; +const SOURCE_SCALE_FACTOR = 2; class SourceObject { @@ -33,7 +33,6 @@ class SourceObject { constructor( opticPositionProperty, representationProperty, tandem ) { assert && assert( tandem instanceof Tandem, 'invalid tandem' ); - const scale = representationProperty.value.isObject ? OBJECT_SCALE_FACTOR : SOURCE_SCALE_FACTOR; // @public {Vector2} displacement vector from the firstPosition to the left top - value depends on representation @@ -48,15 +47,11 @@ class SourceObject { return leftTop.minus( this.offsetPosition ); } ); - this.imageDimensionsProperty = new DerivedProperty( [ representationProperty ], representation => { - const scale = representation.isObject ? OBJECT_SCALE_FACTOR : SOURCE_SCALE_FACTOR; - return new Dimension2( representation.dimensions.width / scale, - representation.dimensions.height / scale ); - } ); - - - this.boundsProperty = new DerivedProperty( [ this.leftTopProperty, this.imageDimensionsProperty ], - ( leftTop, dimensions ) => { + this.boundsProperty = new DerivedProperty( [ this.leftTopProperty, representationProperty ], + ( leftTop, representation ) => { + const scale = representation.isObject ? OBJECT_SCALE_FACTOR : SOURCE_SCALE_FACTOR; + const dimensions = new Dimension2( representation.dimensions.width / scale, + representation.dimensions.height / scale ); return dimensions.toBounds( leftTop.x, leftTop.y - dimensions.height ); } ); diff --git a/js/common/model/Target.js b/js/common/model/Target.js index 71478cf0..850b570c 100644 --- a/js/common/model/Target.js +++ b/js/common/model/Target.js @@ -8,17 +8,21 @@ */ import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import geometricOptics from '../../geometricOptics.js'; +const OBJECT_SCALE_FACTOR = 4; + class Target { /** * @param {Property.} objectPositionProperty * @param {Optic} optic + * @param {Property.} representationProperty * @param {Tandem} tandem */ - constructor( objectPositionProperty, optic, tandem ) { + constructor( objectPositionProperty, optic, representationProperty, tandem ) { assert && assert( tandem instanceof Tandem, 'invalid tandem' ); // @private {Property.} @@ -56,7 +60,6 @@ class Target { return this.getPosition( objectPosition, opticPosition, focalLength ); } ); - // @public (read-only) {Property.} this.scaleProperty = new DerivedProperty( [ objectPositionProperty, optic.positionProperty, @@ -81,6 +84,33 @@ class Target { return this.isVirtual(); } ); + this.boundsProperty = new DerivedProperty( [ + this.positionProperty, + representationProperty, + this.scaleProperty, + this.isInvertedProperty ], + ( position, representation, scale ) => { + + // @public {Vector2} displacement vector from the firstPosition to the left top - value depends on representation + // values are in centimeters + const initialOffsetPosition = representation.offsetPosition.timesScalar( 1 / OBJECT_SCALE_FACTOR ); + const initialWidth = representation.dimensions.width / OBJECT_SCALE_FACTOR; + const initialHeight = representation.dimensions.height / OBJECT_SCALE_FACTOR; + + const offsetPosition = initialOffsetPosition.timesScalar( scale ); + const width = initialWidth * scale; + const height = initialHeight * scale; + + const x1 = ( offsetPosition.x ) * this.opticGetTypeSign(); + const x2 = ( offsetPosition.x + width ) * this.opticGetTypeSign(); + const y1 = offsetPosition.y; + const y2 = offsetPosition.y - height; + + const bounds = new Bounds2( Math.min( x1, x2 ), Math.min( y1, y2 ), Math.max( x1, x2 ), Math.max( y1, y2 ) ); + + return bounds.shifted( position ); + } ); + // light intensity of the image (Hollywood) - a value between 0 and 1 // @public (read-only) {Property.} @@ -89,6 +119,18 @@ class Target { const diameterFactor = optic.getNormalizedDiameter( diameter ); return distanceFactor * diameterFactor; } ); + + + // {Property.} + this.imageProperty = new DerivedProperty( [ representationProperty, this.isVirtualProperty ], + ( representation, isVirtual ) => { + const realImage = optic.isLens() ? representation.leftFacingInverted : + representation.rightFacingInverted; + const virtualImage = optic.isLens() ? representation.rightFacingUpright : + representation.leftFacingUpright; + return isVirtual ? virtualImage : realImage; + } ); + } /** diff --git a/js/common/view/LabelsNode.js b/js/common/view/LabelsNode.js index d6b478dc..418025a1 100644 --- a/js/common/view/LabelsNode.js +++ b/js/common/view/LabelsNode.js @@ -82,21 +82,26 @@ class LabelsNode extends Node { } ); // define image label position - const imageLabelPositionProperty = new DerivedProperty( [ model.firstTarget.positionProperty ], - position => position.minusXY( 0, 14 ) ); + const imageLabelPositionProperty = new DerivedProperty( [ model.firstTarget.boundsProperty ], + bounds => bounds.centerTop ); // create image label const imageLabel = new LabelNode( imageString, imageLabelPositionProperty, new BooleanProperty( true ), modelViewTransformProperty ); // define object label position - const objectLabelPositionProperty = new DerivedProperty( [ model.sourceObject.firstPositionProperty ], - position => position.minusXY( 0, 66 ) ); + const objectLabelPositionProperty = new DerivedProperty( [ model.sourceObject.boundsProperty ], + // because the we use a Y inverted reference frame, the bottom of the image is the top of the model bounds. + bounds => bounds.centerTop ); // create object label const objectLabel = new LabelNode( objectString, objectLabelPositionProperty, new BooleanProperty( true ), modelViewTransformProperty ); // update the visibility of the object and image labels - Property.multilink( [ model.representationProperty, model.enableFirstTargetProperty, model.firstTarget.isVirtualProperty, visibleProperties.visibleVirtualImageProperty ], + Property.multilink( [ + model.representationProperty, + model.enableFirstTargetProperty, + model.firstTarget.isVirtualProperty, + visibleProperties.visibleVirtualImageProperty ], ( representation, isEnabled, isVirtual, showVirtual ) => { objectLabel.visible = representation.isObject; imageLabel.visible = isEnabled && ( isVirtual ? showVirtual : true ) && representation.isObject; diff --git a/js/common/view/SourceObjectNode.js b/js/common/view/SourceObjectNode.js index 4a675689..24ffd7cd 100644 --- a/js/common/view/SourceObjectNode.js +++ b/js/common/view/SourceObjectNode.js @@ -29,7 +29,7 @@ const SECOND_SOURCE_POINT_OPTIONS = GeometricOpticsConstants.SECOND_SOURCE_POINT const SECOND_SOURCE_POINT_FILL = geometricOpticsColorProfile.secondSourcePointFillProperty; const SECOND_SOURCE_POINT_STROKE = geometricOpticsColorProfile.secondSourcePointStrokeProperty; -const OVERALL_SCALE_FACTOR = 0.5; +const OVERALL_SCALE_FACTOR = 1; const LIGHT_OFFSET_VECTOR = new Vector2( 50, -23 ); // in model coordinates const CUEING_ARROW_LENGTH = 20; const CUEING_ARROW_OPTIONS = { @@ -68,14 +68,18 @@ class SourceObjectNode extends Node { this.addChild( sourceObjectImage ); /** - * scale image to bounds + * scale image to size of model bounds * @param {Node} image * @param {Bounds2} bounds */ const scaleFunction = ( image, bounds ) => { const initialWidth = sourceObjectImage.width; const initialHeight = sourceObjectImage.height; - image.scale( bounds.width / initialWidth, bounds.height / initialHeight ); + + // bounds that we want for the image + const viewBounds = modelViewTransform.modelToViewBounds( bounds ); + image.scale( viewBounds.width / initialWidth, + viewBounds.height / initialHeight ); }; /** @@ -87,14 +91,15 @@ class SourceObjectNode extends Node { }; // keep at least half of the projector screen within visible bounds and right of the optic - const dragBoundsProperty = new DerivedProperty( [ visibleModelBoundsProperty ], + const dragBoundsProperty = new DerivedProperty( [ visibleModelBoundsProperty, representationProperty ], visibleBounds => { return new Bounds2( visibleBounds.minX, - visibleBounds.minY - sourceObject.boundsProperty.value.height / 2, - sourceObject.getOpticPosition().x - sourceObject.boundsProperty.value.width / 2, + visibleBounds.minY + sourceObject.boundsProperty.value.height, + sourceObject.getOpticPosition().x - sourceObject.boundsProperty.value.width, visibleBounds.maxY ); } ); + // create drag listener for source const sourceObjectDragListener = new DragListener( { positionProperty: sourceObject.leftTopProperty, @@ -110,6 +115,12 @@ class SourceObjectNode extends Node { setImagePosition( sourceObjectImage, position ); } ); + + dragBoundsProperty.link( dragBounds => { + sourceObject.leftTopProperty.value = dragBounds.closestPointTo( sourceObject.leftTopProperty.value ); + } ); + + // create a node to hold the second source const secondNode = new Node(); this.addChild( secondNode ); @@ -183,8 +194,6 @@ class SourceObjectNode extends Node { secondNode.touchArea = circleIcon.bounds.dilated( 10 ); secondNode.addChild( this.cueingArrowsLayer ); - sourceObjectImage.setScaleMagnitude( OVERALL_SCALE_FACTOR ); - // address position of source of light #79 } else { diff --git a/js/common/view/TargetNode.js b/js/common/view/TargetNode.js index b409586e..d7442653 100644 --- a/js/common/view/TargetNode.js +++ b/js/common/view/TargetNode.js @@ -8,12 +8,11 @@ * @author Martin Veillette */ -import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; import Image from '../../../../scenery/js/nodes/Image.js'; import Node from '../../../../scenery/js/nodes/Node.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import geometricOptics from '../../geometricOptics.js'; -import Representation from '../model/Representation.js'; class TargetNode extends Node { @@ -37,83 +36,55 @@ class TargetNode extends Node { super( { tandem: tandem } ); - // {Property.} - const imageProperty = new DerivedProperty( [ representationProperty, target.isVirtualProperty ], - ( representation, isVirtual ) => { - const realImage = optic.isLens() ? representation.leftFacingInverted : - representation.rightFacingInverted; - const virtualImage = optic.isLens() ? representation.rightFacingUpright : - representation.leftFacingUpright; - return isVirtual ? virtualImage : realImage; - } ); - // creates the target image - const targetImage = new Image( imageProperty.value, { scale: 0.5 } ); - - function updateScale() { - const position = target.positionProperty.value; - const scale = Math.abs( target.scaleProperty.value ); - let verticalOffset; - let horizontalOffset; - if ( representationProperty.value === Representation.PENCIL ) { - if ( optic.isLens() ) { - verticalOffset = target.isVirtual() ? -37 : -145; - horizontalOffset = target.isVirtual() ? -31 : -22; - } - else { - verticalOffset = target.isVirtual() ? -37 : -145; - horizontalOffset = target.isVirtual() ? -24 : -31; - } - } - else if ( representationProperty.value === Representation.TREE ) { - if ( optic.isLens() ) { - verticalOffset = target.isVirtual() ? -40 : -150; - horizontalOffset = target.isVirtual() ? -32 : -22; - } - else { - verticalOffset = target.isVirtual() ? -40 : -150; - horizontalOffset = target.isVirtual() ? -22 : -30; - } - } + const targetImage = new Image( target.imageProperty.value ); - else if ( representationProperty.value === Representation.ROCKET ) { - if ( optic.isLens() ) { - verticalOffset = target.isVirtual() ? -40 : -165; - horizontalOffset = target.isVirtual() ? -30 : -22; - } - else { - verticalOffset = target.isVirtual() ? -40 : -165; - horizontalOffset = target.isVirtual() ? -22 : -30; - } - } - else { - verticalOffset = target.isVirtual() ? -40 : -145; - horizontalOffset = target.isVirtual() ? -30 : -22; - } + /** + * update the size as well as the position of the image. + */ + const updateScaleAndPosition = () => { + // desired bounds for the image + const viewBounds = modelViewTransform.modelToViewBounds( target.boundsProperty.value ); - targetImage.translation = modelViewTransform.modelToViewPosition( position ).plusXY( horizontalOffset * scale, verticalOffset * scale ); - targetImage.setScaleMagnitude( scale * 0.5 ); - } + // current values for width and height + const initialWidth = targetImage.width; + const initialHeight = targetImage.height; - function updateVisibility() { + // scale image appropriately + targetImage.scale( viewBounds.width / initialWidth, viewBounds.height / initialHeight ); + + // move the image + targetImage.translation = new Vector2( viewBounds.minX, viewBounds.minY ); + }; + + + /** + * update the visibility of this node based on: + * is the image virtual? + * is the checkbox show virtual is on? + * has the image been targeted by the rays? + * is the object to optic distance positive ? + */ + const updateVisibility = () => { + + // {boolean} state of the virtual image checkbox const showVirtualImage = visibleVirtualImageProperty.value; + const isObjectDistancePositive = target.isObjectOpticDistancePositive(); - targetImage.visible = ( ( target.isVirtual() ) ? showVirtualImage : true ) && isObjectDistancePositive + targetImage.visible = ( ( target.isVirtual() ) ? showVirtualImage : true ) + && isObjectDistancePositive && enableImageProperty.value; - } + }; - // see #94 - target.scaleProperty.link( updateScale ); - - target.positionProperty.link( () => { - updateScale(); + target.boundsProperty.link( () => { + updateScaleAndPosition(); updateVisibility(); } ); optic.curveProperty.link( () => { - updateScale(); + updateScaleAndPosition(); updateVisibility(); } ); @@ -130,8 +101,12 @@ class TargetNode extends Node { targetImage.opacity = intensity; } ); + enableImageProperty.link( () => { + updateVisibility(); + } ); + // update the image and its visibility - imageProperty.link( image => { + target.imageProperty.link( image => { // make this entire node invisible if the representation is not an object. this.visible = representationProperty.value.isObject; @@ -143,18 +118,13 @@ class TargetNode extends Node { targetImage.image = image; // update the scale of the image - updateScale(); + updateScaleAndPosition(); } } ); this.addChild( targetImage ); - - enableImageProperty.link( () => { - updateVisibility(); - } ); } - } geometricOptics.register( 'TargetNode', TargetNode );