diff --git a/geometric-optics-strings_en.json b/geometric-optics-strings_en.json index 555e0535..c9d00fe7 100644 --- a/geometric-optics-strings_en.json +++ b/geometric-optics-strings_en.json @@ -77,6 +77,9 @@ "valueCentimetersPattern": { "value": "{{value}} cm" }, + "arrow": { + "value": "Arrow" + }, "pencil": { "value": "Pencil" }, diff --git a/images/arrowIcon.png b/images/arrowIcon.png new file mode 100644 index 00000000..0c7d1bed Binary files /dev/null and b/images/arrowIcon.png differ diff --git a/images/arrowIcon_png.js b/images/arrowIcon_png.js new file mode 100644 index 00000000..109e70d2 --- /dev/null +++ b/images/arrowIcon_png.js @@ -0,0 +1,8 @@ +/* eslint-disable */ +import asyncLoader from '../../phet-core/js/asyncLoader.js'; + +const image = new Image(); +const unlock = asyncLoader.createLock( image ); +image.onload = unlock; +image.src = 'data:image/png;base64,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'; +export default image; \ No newline at end of file diff --git a/images/license.json b/images/license.json index 8787c21f..8da8c90a 100644 --- a/images/license.json +++ b/images/license.json @@ -1,4 +1,11 @@ { + "arrowIcon.png": { + "text": [ + "Copyright 2022 University of Colorado Boulder" + ], + "projectURL": "https://phet.colorado.edu", + "license": "contact phethelp@colorado.edu" + }, "pencilIcon.png": { "text": [ "Copyright 2021 University of Colorado Boulder" diff --git a/js/common/GOColors.ts b/js/common/GOColors.ts index 185657c8..01b2e6fa 100644 --- a/js/common/GOColors.ts +++ b/js/common/GOColors.ts @@ -90,6 +90,22 @@ const GOColors = { default: 'black' } ), + arrow1FillProperty: new ProfileColorProperty( geometricOptics, 'arrow1Fill', { + default: 'green' + } ), + + arrow1StrokeProperty: new ProfileColorProperty( geometricOptics, 'arrow1Stroke', { + default: 'black' + } ), + + arrow2FillProperty: new ProfileColorProperty( geometricOptics, 'arrow2Fill', { + default: 'rgb( 255, 51, 51 )' //TODO same as secondPointFillProperty + } ), + + arrow2StrokeProperty: new ProfileColorProperty( geometricOptics, 'arrow2Stroke', { + default: 'black' + } ), + // Rays associated with the first Optical Object rays1StrokeProperty: new ProfileColorProperty( geometricOptics, 'realRayOneStroke', { default: 'rgb( 140, 140, 140 )' diff --git a/js/common/GOConstants.ts b/js/common/GOConstants.ts index 9334cfe4..a976bfca 100644 --- a/js/common/GOConstants.ts +++ b/js/common/GOConstants.ts @@ -90,6 +90,14 @@ const GOConstants = { headWidth: 12, headHeight: 8, tailWidth: 3 + }, + + ARROW_NODE_OPTIONS: { + headWidth: 24, + headHeight: 28, + tailWidth: 7, + isHeadDynamic: true, + fractionalHeadHeight: 0.5 } }; diff --git a/js/common/model/ArrowImage.ts b/js/common/model/ArrowImage.ts new file mode 100644 index 00000000..5982e366 --- /dev/null +++ b/js/common/model/ArrowImage.ts @@ -0,0 +1,35 @@ +// Copyright 2022, University of Colorado Boulder + +/** + * ArrowImage is the model of the optical image associated with an arrow object. + * + * @author Chris Malley (PixelZoom, Inc.) + */ + +import geometricOptics from '../../geometricOptics.js'; +import OpticalImage, { OpticalImageOptions } from './OpticalImage.js'; +import Optic from './Optic.js'; +import merge from '../../../../phet-core/js/merge.js'; +import ArrowObject from './ArrowObject.js'; + +class ArrowImage extends OpticalImage { + + /** + * @param arrowObject + * @param optic + * @param providedOptions + */ + constructor( arrowObject: ArrowObject, + optic: Optic, + providedOptions: OpticalImageOptions ) { + + const options = merge( {}, providedOptions ); + + super( arrowObject.positionProperty, optic, options ); + + //TODO more? + } +} + +geometricOptics.register( 'ArrowImage', ArrowImage ); +export default ArrowImage; \ No newline at end of file diff --git a/js/common/model/ArrowObject.ts b/js/common/model/ArrowObject.ts new file mode 100644 index 00000000..38e2a6bc --- /dev/null +++ b/js/common/model/ArrowObject.ts @@ -0,0 +1,41 @@ +// Copyright 2021-2022, University of Colorado Boulder + +/** + * ArrowObject is the model for arrow objects. + * + * @author Martin Veillette + * @author Chris Malley (PixelZoom, Inc.) + */ + +import geometricOptics from '../../geometricOptics.js'; +import OpticalObject, { OpticalObjectOptions } from './OpticalObject.js'; +import merge from '../../../../phet-core/js/merge.js'; + +type ArrowObjectOptions = { + fill: ColorDef + stroke: ColorDef +} & OpticalObjectOptions; + +class ArrowObject extends OpticalObject { + + public readonly fill: ColorDef; + public readonly stroke: ColorDef; + + /** + * @param providedOptions + */ + constructor( providedOptions: ArrowObjectOptions ) { + + const options = merge( {}, providedOptions ); + + super( options ); + + this.fill = options.fill; + this.stroke = options.stroke; + + //TODO more? + } +} + +geometricOptics.register( 'ArrowObject', ArrowObject ); +export default ArrowObject; \ No newline at end of file diff --git a/js/common/model/ArrowObjectScene.ts b/js/common/model/ArrowObjectScene.ts new file mode 100644 index 00000000..c2fbd04e --- /dev/null +++ b/js/common/model/ArrowObjectScene.ts @@ -0,0 +1,161 @@ +// Copyright 2022, University of Colorado Boulder + +//TODO lots of duplication with FramedObjectScene +/** + * ArrowObjectScene is a scene in which rays from two arrows interact with an optic and produce an Image. + * + * @author Chris Malley (PixelZoom, Inc.) + */ + +import Range from '../../../../dot/js/Range.js'; +import geometricOptics from '../../geometricOptics.js'; +import Optic from './Optic.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; +import merge from '../../../../phet-core/js/merge.js'; +import { RaysType } from './RaysType.js'; +import NumberProperty from '../../../../axon/js/NumberProperty.js'; +import LightRays from './LightRays.js'; +import IReadOnlyProperty from '../../../../axon/js/IReadOnlyProperty.js'; +import Lens from '../../lens/model/Lens.js'; +import PhetioObject from '../../../../tandem/js/PhetioObject.js'; +import Guides from '../../lens/model/Guides.js'; +import ArrowObject from './ArrowObject.js'; +import ArrowImage from './ArrowImage.js'; +import GOColors from '../GOColors.js'; + +type ArrowObjectSceneOptions = { + + // initial positions of the arrow objects + arrowObject1Position: Vector2, + arrowObject2Position: Vector2, + + // phet-io options + tandem: Tandem +}; + +class ArrowObjectScene extends PhetioObject { + + readonly optic: Optic; + readonly arrowObject1: ArrowObject; + readonly arrowObject2: ArrowObject; + readonly arrowImage1: ArrowImage; + readonly arrowImage2: ArrowImage; + readonly lightRaysAnimationTimeProperty: NumberProperty; + readonly lightRays1: LightRays; + readonly lightRays2: LightRays; + readonly guides1: Guides | null; + readonly guides2: Guides | null; + private readonly resetArrowObjectScene: () => void; + + /** + * @param optic + * @param raysTypeProperty + * @param providedOptions + */ + constructor( optic: Optic, + raysTypeProperty: IReadOnlyProperty, + providedOptions: ArrowObjectSceneOptions ) { + + const options = merge( { + phetioState: false + }, providedOptions ); + + super( options ); + + this.optic = optic; + + this.addLinkedElement( optic, { + tandem: options.tandem.createTandem( 'optic' ) + } ); + + this.arrowObject1 = new ArrowObject( { + position: options.arrowObject1Position, + fill: GOColors.arrow1FillProperty, + stroke: GOColors.arrow1StrokeProperty, + tandem: options.tandem.createTandem( 'arrowObject1' ) + } ); + + this.arrowObject2 = new ArrowObject( { + position: options.arrowObject2Position, + fill: GOColors.arrow2FillProperty, + stroke: GOColors.arrow2StrokeProperty, + tandem: options.tandem.createTandem( 'arrowObject2' ) + } ); + + this.arrowImage1 = new ArrowImage( this.arrowObject1, this.optic, { + tandem: options.tandem.createTandem( 'arrowImage1' ), + phetioDocumentation: 'optical image associated with the first arrow object' + } ); + + this.arrowImage2 = new ArrowImage( this.arrowObject2, this.optic, { + tandem: options.tandem.createTandem( 'arrowImage2' ), + phetioDocumentation: 'optical image associated with the second arrow object' + } ); + + this.lightRaysAnimationTimeProperty = new NumberProperty( 0, { + units: 's', + range: new Range( 0, 10 ), // determines the duration of the light rays animation + tandem: options.tandem.createTandem( 'lightRaysAnimationTimeProperty' ), + phetioReadOnly: true + } ); + + this.lightRays1 = new LightRays( + this.lightRaysAnimationTimeProperty, + raysTypeProperty, + this.arrowObject1.positionProperty, + this.optic, + this.arrowImage1 + ); + + this.lightRays2 = new LightRays( + this.lightRaysAnimationTimeProperty, + raysTypeProperty, + this.arrowObject2.positionProperty, + this.optic, + this.arrowImage2 + ); + + // Guides + if ( optic instanceof Lens ) { + this.guides1 = new Guides( this.optic, this.arrowObject1.positionProperty, { + tandem: options.tandem.createTandem( 'guides1' ), + phetioDocumentation: 'guides associated with the first arrow object' + } ); + this.guides2 = new Guides( this.optic, this.arrowObject2.positionProperty, { + tandem: options.tandem.createTandem( 'guides2' ), + phetioDocumentation: 'guides associated with the second arrow object' + } ); + } + else { + this.guides1 = null; + this.guides2 = null; + } + + //TODO is this complete? + this.resetArrowObjectScene = () => { + this.arrowObject1.reset(); + this.arrowObject2.reset(); + this.lightRaysAnimationTimeProperty.reset(); + }; + } + + public reset(): void { + this.resetArrowObjectScene(); + } + + /** + * Steps the animation of light rays. + * @param dt - time step, in seconds + */ + public stepLightRays( dt: number ): void { + const t = Math.min( this.lightRaysAnimationTimeProperty.value + dt, this.lightRaysAnimationTimeProperty.range!.max ); + assert && assert( this.lightRaysAnimationTimeProperty.range ); // {Range|null} + if ( this.lightRaysAnimationTimeProperty.range!.contains( t ) ) { + this.lightRaysAnimationTimeProperty.value = t; + } + } +} + +geometricOptics.register( 'ArrowObjectScene', ArrowObjectScene ); +export default ArrowObjectScene; \ No newline at end of file diff --git a/js/common/model/FramedImage.ts b/js/common/model/FramedImage.ts index c0f84683..9cfdd96a 100644 --- a/js/common/model/FramedImage.ts +++ b/js/common/model/FramedImage.ts @@ -1,8 +1,7 @@ // Copyright 2021-2022, University of Colorado Boulder -//TODO this entire class needs to be reviewed/revised /** - * FramedImage is the model of an optical image associated with a framed object. + * FramedImage is the model of the optical image associated with a framed object. * * @author Martin Veillette * @author Chris Malley (PixelZoom, Inc.) diff --git a/js/common/model/FramedObjectScene.ts b/js/common/model/FramedObjectScene.ts index eaa5fd5e..31ef49ae 100644 --- a/js/common/model/FramedObjectScene.ts +++ b/js/common/model/FramedObjectScene.ts @@ -120,7 +120,6 @@ class FramedObjectScene extends PhetioObject { // Guides if ( optic instanceof Lens ) { - this.guides1 = new Guides( this.optic, this.framedObject.positionProperty, { tandem: options.tandem.createTandem( 'guides1' ), phetioDocumentation: 'guides associated with the first point-of-interest on the framed object' diff --git a/js/common/model/GOModel.ts b/js/common/model/GOModel.ts index 50715071..37afb8c3 100644 --- a/js/common/model/GOModel.ts +++ b/js/common/model/GOModel.ts @@ -20,11 +20,14 @@ import GORuler from './GORuler.js'; import Vector2 from '../../../../dot/js/Vector2.js'; import FramedObjectScene from './FramedObjectScene.js'; import OpticalObjectChoice from './OpticalObjectChoice.js'; +import ArrowObjectScene from './ArrowObjectScene.js'; -type GeometricOpticsModelOptions = { +type GOModelOptions = { - // initial position of the framed object + // initial positions of optical objects framedObjectPosition: Vector2, + arrowObject1Position: Vector2, + arrowObject2Position: Vector2, // optical object choices, in the order that they will appear in OpticalObjectChoiceComboBox opticalObjectChoices: OpticalObjectChoice[], @@ -44,6 +47,7 @@ class GOModel { readonly raysTypeProperty: Property; // scenes + readonly arrowObjectScene: ArrowObjectScene; readonly framedObjectScene: FramedObjectScene; // rulers @@ -64,7 +68,7 @@ class GOModel { * @param optic * @param providedOptions */ - constructor( optic: Optic, providedOptions: GeometricOpticsModelOptions ) { + constructor( optic: Optic, providedOptions: GOModelOptions ) { const options = merge( { //TODO @@ -85,6 +89,12 @@ class GOModel { this.scenesTandem = options.tandem.createTandem( 'scenes' ); + this.arrowObjectScene = new ArrowObjectScene( this.optic, this.raysTypeProperty, { + arrowObject1Position: options.arrowObject1Position, + arrowObject2Position: options.arrowObject2Position, + tandem: this.scenesTandem.createTandem( 'arrowObjectScene' ) + } ); + this.framedObjectScene = new FramedObjectScene( this.opticalObjectChoiceProperty, this.optic, this.raysTypeProperty, { framedObjectPosition: options.framedObjectPosition, tandem: this.scenesTandem.createTandem( 'framedObjectScene' ) @@ -111,6 +121,7 @@ class GOModel { this.optic.reset(); this.raysTypeProperty.reset(); this.framedObjectScene.reset(); + this.arrowObjectScene.reset(); this.horizontalRuler.reset(); this.verticalRuler.reset(); }; @@ -126,5 +137,4 @@ class GOModel { } geometricOptics.register( 'GOModel', GOModel ); -export default GOModel; -export type { GeometricOpticsModelOptions }; \ No newline at end of file +export default GOModel; \ No newline at end of file diff --git a/js/common/model/OpticalObjectChoice.ts b/js/common/model/OpticalObjectChoice.ts index 20b068f9..5dba6b56 100644 --- a/js/common/model/OpticalObjectChoice.ts +++ b/js/common/model/OpticalObjectChoice.ts @@ -31,6 +31,7 @@ import starRightFacingUpright_png from '../../../images/starRightFacingUpright_p import starRightFacingInverted_png from '../../../images/starRightFacingInverted_png.js'; import starLeftFacingUpright_png from '../../../images/starLeftFacingUpright_png.js'; import starLeftFacingInverted_png from '../../../images/starLeftFacingInverted_png.js'; +import arrowIcon_png from '../../../images/arrowIcon_png.js'; // Set of HTMLImageElements that depict a framed object and its associated optical image type ObjectHTMLImageElements = { @@ -42,6 +43,9 @@ type ObjectHTMLImageElements = { class OpticalObjectChoice extends EnumerationValue { + //TODO replace arrowIcon_png with new PNG file, or generate programmatically + static ARROW = new OpticalObjectChoice( geometricOpticsStrings.arrow, arrowIcon_png, 'arrow' ); + static PENCIL = new OpticalObjectChoice( geometricOpticsStrings.pencil, pencilIcon_png, 'pencil', { rightFacingUpright: pencilRightFacingUpright_png, rightFacingInverted: pencilRightFacingInverted_png, @@ -111,6 +115,14 @@ class OpticalObjectChoice extends EnumerationValue { this.objectHTMLImageElements = objectHTMLImageElements; } + /** + * Is the choice an arrow object? + * @param choice + */ + static isArrowObject( choice: OpticalObjectChoice ): boolean { + return ( choice === OpticalObjectChoice.ARROW ); + } + /** * Is the choice a framed object? * @param choice @@ -124,7 +136,7 @@ class OpticalObjectChoice extends EnumerationValue { * @param choice */ static isLightSource( choice: OpticalObjectChoice ): boolean { - return choice === OpticalObjectChoice.LIGHT; + return ( choice === OpticalObjectChoice.LIGHT ); } } diff --git a/js/common/view/ArrowImageNode.ts b/js/common/view/ArrowImageNode.ts new file mode 100644 index 00000000..0e6ce888 --- /dev/null +++ b/js/common/view/ArrowImageNode.ts @@ -0,0 +1,45 @@ +// Copyright 2022, University of Colorado Boulder + +/** + * ArrowImageNode is the visual representation of an arrow object. + * + * @author Chris Malley (PixelZoom, Inc.) + */ + +import IProperty from '../../../../axon/js/IProperty.js'; +import IReadOnlyProperty from '../../../../axon/js/IReadOnlyProperty.js'; +import merge from '../../../../phet-core/js/merge.js'; +import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js'; +import { Node } from '../../../../scenery/js/imports.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; +import geometricOptics from '../../geometricOptics.js'; +import ArrowImage from '../model/ArrowImage.js'; + +type ArrowImageNodeOptions = { + visibleProperty?: IProperty, + tandem: Tandem +}; + +class ArrowImageNode extends Node { + + /** + * @param arrowImage + * @param virtualImageVisibleProperty + * @param raysAndImagesVisibleProperty + * @param modelViewTransform + * @param providedOptions + */ + constructor( arrowImage: ArrowImage, + virtualImageVisibleProperty: IReadOnlyProperty, + raysAndImagesVisibleProperty: IReadOnlyProperty, + modelViewTransform: ModelViewTransform2, + providedOptions: ArrowImageNodeOptions ) { + + const options = merge( {}, providedOptions ); + + super( options ); + } +} + +geometricOptics.register( 'ArrowImageNode', ArrowImageNode ); +export default ArrowImageNode; \ No newline at end of file diff --git a/js/common/view/ArrowObjectNode.ts b/js/common/view/ArrowObjectNode.ts new file mode 100644 index 00000000..ad30c939 --- /dev/null +++ b/js/common/view/ArrowObjectNode.ts @@ -0,0 +1,67 @@ +// Copyright 2022, University of Colorado Boulder + +/** + * ArrowObjectNode is the visual representation of an arrow object. + * + * @author Chris Malley (PixelZoom, Inc.) + */ + +import IProperty from '../../../../axon/js/IProperty.js'; +import IReadOnlyProperty from '../../../../axon/js/IReadOnlyProperty.js'; +import Property from '../../../../axon/js/Property.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import merge from '../../../../phet-core/js/merge.js'; +import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js'; +import { Node } from '../../../../scenery/js/imports.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; +import geometricOptics from '../../geometricOptics.js'; +import ArrowObject from '../model/ArrowObject.js'; +import Optic from '../model/Optic.js'; +import GOConstants from '../GOConstants.js'; +import ArrowNode from '../../../../scenery-phet/js/ArrowNode.js'; + +type ArrowObjectNodeOptions = { + visibleProperty?: IProperty, + tandem: Tandem +}; + +class ArrowObjectNode extends Node { + + /** + * @param arrowObject + * @param optic + * @param modelBoundsProperty + * @param modelViewTransform + * @param providedOptions + */ + constructor( arrowObject: ArrowObject, + optic: Optic, + modelBoundsProperty: IReadOnlyProperty, + modelViewTransform: ModelViewTransform2, + providedOptions: ArrowObjectNodeOptions ) { + + const options = merge( {}, providedOptions ); + + super( options ); + + const arrowNode = new ArrowNode( 0, 0, 0, 1, merge( {}, GOConstants.ARROW_NODE_OPTIONS, { + fill: arrowObject.fill, + stroke: arrowObject.stroke + } ) ); + this.addChild( arrowNode ); + + Property.multilink( [ arrowObject.positionProperty, optic.positionProperty ], + ( arrowObjectPosition, opticPosition ) => { + const tipPosition = modelViewTransform.modelToViewPosition( arrowObjectPosition ); + const tailY = modelViewTransform.modelToViewY( opticPosition.y ); + arrowNode.setTailAndTip( tipPosition.x, tailY, tipPosition.x, tipPosition.y ); + } ); + } + + reset() { + //TODO + } +} + +geometricOptics.register( 'ArrowObjectNode', ArrowObjectNode ); +export default ArrowObjectNode; \ No newline at end of file diff --git a/js/common/view/ArrowObjectSceneNode.ts b/js/common/view/ArrowObjectSceneNode.ts new file mode 100644 index 00000000..cf66d4a9 --- /dev/null +++ b/js/common/view/ArrowObjectSceneNode.ts @@ -0,0 +1,233 @@ +// Copyright 2022, University of Colorado Boulder + +//TODO lots of duplication with FramedObjectScene +/** + * ArrowObjectSceneNode is the view of ArrowObjectScene, the scene that uses a arrow objects. + * + * @author Chris Malley (PixelZoom, Inc.) + */ + +import merge from '../../../../phet-core/js/merge.js'; +import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js'; +import { Node } from '../../../../scenery/js/imports.js'; +import geometricOptics from '../../geometricOptics.js'; +import VisibleProperties from './VisibleProperties.js'; +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import IReadOnlyProperty from '../../../../axon/js/IReadOnlyProperty.js'; +import OpticalAxisNode from './OpticalAxisNode.js'; +import OpticVerticalAxisNode from './OpticVerticalAxisNode.js'; +import { RaysType } from '../model/RaysType.js'; +import FocalPointNode from './FocalPointNode.js'; +import TwoFPointNode from './TwoFPointNode.js'; +import GOColors from '../GOColors.js'; +import RealLightRaysNode from './RealLightRaysNode.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; +import Optic from '../model/Optic.js'; +import BooleanProperty from '../../../../axon/js/BooleanProperty.js'; +import VirtualLightRaysNode from './VirtualLightRaysNode.js'; +import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import GuidesNode from '../../lens/view/GuidesNode.js'; +import { RulerHotkeysData } from './GORulerNode.js'; +import BooleanIO from '../../../../tandem/js/types/BooleanIO.js'; +import ArrowObjectScene from '../model/ArrowObjectScene.js'; +import ArrowObjectNode from './ArrowObjectNode.js'; +import ArrowImageNode from './ArrowImageNode.js'; + +type ArrowObjectSceneNodeOptions = { + + // Creates the Node for the optic + createOpticNode: ( optic: Optic, modelBoundsProperty: IReadOnlyProperty, modelViewTransform: ModelViewTransform2, parentTandem: Tandem ) => Node, + + dragLockedProperty: BooleanProperty, + + tandem: Tandem +}; + +class ArrowObjectSceneNode extends Node { + + public readonly rulerHotkeysData: RulerHotkeysData; + private readonly resetFrameObjectSceneNode: () => void; + + /** + * @param scene + * @param visibleProperties + * @param modelViewTransform + * @param modelVisibleBoundsProperty + * @param modelBoundsProperty + * @param raysTypeProperty + * @param providedOptions + */ + constructor( scene: ArrowObjectScene, + visibleProperties: VisibleProperties, + modelViewTransform: ModelViewTransform2, + modelVisibleBoundsProperty: IReadOnlyProperty, + modelBoundsProperty: IReadOnlyProperty, + raysTypeProperty: IReadOnlyProperty, + providedOptions: ArrowObjectSceneNodeOptions ) { + + const options = merge( { + visiblePropertyOptions: { phetioReadOnly: true } + }, providedOptions ); + + super( options ); + + const opticNode = options.createOpticNode( scene.optic, modelBoundsProperty, modelViewTransform, options.tandem ); + + const opticalAxisNode = new OpticalAxisNode( + scene.optic.positionProperty, + modelVisibleBoundsProperty, + modelViewTransform, { + visibleProperty: visibleProperties.opticalAxisVisibleProperty, + tandem: options.tandem.createTandem( 'opticalAxisNode' ) + } ); + + const opticVerticalAxisNode = new OpticVerticalAxisNode( scene.optic, raysTypeProperty, modelViewTransform ); + + // focal points (F) + const focalPointsNodeTandem = options.tandem.createTandem( 'focalPointsNode' ); + const focalPointsNode = new Node( { + children: [ + new FocalPointNode( scene.optic.leftFocalPointProperty, modelViewTransform, { + tandem: focalPointsNodeTandem.createTandem( 'leftFocalPointNode' ) + } ), + new FocalPointNode( scene.optic.rightFocalPointProperty, modelViewTransform, { + tandem: focalPointsNodeTandem.createTandem( 'rightFocalPointNode' ) + } ) + ], + visibleProperty: visibleProperties.focalPointsVisibleProperty, + tandem: focalPointsNodeTandem + } ); + + // 2F points + const twoFPointsNodeTandem = options.tandem.createTandem( 'twoFPointsNode' ); + const twoFPointsNode = new Node( { + children: [ + new TwoFPointNode( scene.optic.left2FProperty, modelViewTransform, { + tandem: twoFPointsNodeTandem.createTandem( 'left2FPointNode' ) + } ), + new TwoFPointNode( scene.optic.right2FProperty, modelViewTransform, { + tandem: twoFPointsNodeTandem.createTandem( 'right2FPointNode' ) + } ) + ], + visibleProperty: visibleProperties.twoFPointsVisibleProperty, + tandem: twoFPointsNodeTandem + } ); + + const arrowObject1Node = new ArrowObjectNode( scene.arrowObject1, scene.optic, modelBoundsProperty, modelViewTransform, { + tandem: options.tandem.createTandem( 'arrowObject1Node' ) + } ); + + const arrowObject2Node = new ArrowObjectNode( scene.arrowObject2, scene.optic, modelBoundsProperty, modelViewTransform, { + visibleProperty: visibleProperties.secondPointVisibleProperty, + tandem: options.tandem.createTandem( 'arrowObject2Node' ) + } ); + + const arrowImage1Node = new ArrowImageNode( scene.arrowImage1, visibleProperties.virtualImageVisibleProperty, + visibleProperties.raysAndImagesVisibleProperty, modelViewTransform, { + tandem: options.tandem.createTandem( 'arrowImage1Node' ) + } ); + + const arrowImage2Node = new ArrowImageNode( scene.arrowImage2, visibleProperties.virtualImageVisibleProperty, + visibleProperties.raysAndImagesVisibleProperty, modelViewTransform, { + tandem: options.tandem.createTandem( 'arrowImage2Node' ) + } ); + + // Light rays (real & virtual) associated with the first point-of-interest (the framed object's position). + const realLightRays1Options = { + stroke: GOColors.rays1StrokeProperty, + visibleProperty: visibleProperties.raysAndImagesVisibleProperty + }; + const realLightRays1Node = new RealLightRaysNode( scene.lightRays1, modelViewTransform, realLightRays1Options ); + const virtualLightRays1Node = new VirtualLightRaysNode( scene.lightRays1, modelViewTransform, { + stroke: realLightRays1Options.stroke, + visibleProperty: DerivedProperty.and( [ + visibleProperties.virtualImageVisibleProperty, + visibleProperties.raysAndImagesVisibleProperty + ] ) + } ); + + // Light rays (real & virtual) associated with the second point-of-interest (also on the framed object). + const realLightRays2Options = { + stroke: GOColors.rays2StrokeProperty, + visibleProperty: DerivedProperty.and( [ + visibleProperties.secondPointVisibleProperty, + visibleProperties.raysAndImagesVisibleProperty + ] ) + }; + const realLightRays2Node = new RealLightRaysNode( scene.lightRays2, modelViewTransform, realLightRays2Options ); + const virtualLightRays2Node = new VirtualLightRaysNode( scene.lightRays2, modelViewTransform, { + stroke: realLightRays2Options.stroke, + visibleProperty: DerivedProperty.and( [ + visibleProperties.virtualImageVisibleProperty, + visibleProperties.secondPointVisibleProperty, + visibleProperties.raysAndImagesVisibleProperty + ] ) + } ); + + this.children = [ + opticalAxisNode, + arrowObject1Node, + arrowObject2Node, + arrowImage1Node, + arrowImage2Node, + opticNode, + opticVerticalAxisNode, + focalPointsNode, + twoFPointsNode, + realLightRays1Node, + virtualLightRays1Node, + realLightRays2Node, + virtualLightRays2Node + ]; + + if ( scene.guides1 ) { + const guides1Node = new GuidesNode( scene.guides1, GOColors.guideArm1FillProperty, modelViewTransform, { + visibleProperty: visibleProperties.guidesVisibleProperty, + tandem: options.tandem.createTandem( 'guides1Node' ), + phetioDocumentation: 'guides associated with the first point-of-interest on the framed object' + } ); + this.addChild( guides1Node ); + } + + if ( scene.guides2 ) { + const guides2Tandem = options.tandem.createTandem( 'guides2Node' ); + const guides2Node = new GuidesNode( scene.guides2, GOColors.guideArm2FillProperty, modelViewTransform, { + visibleProperty: DerivedProperty.and( + [ visibleProperties.guidesVisibleProperty, visibleProperties.secondPointVisibleProperty ], { + tandem: guides2Tandem.createTandem( 'visibleProperty' ), + phetioType: DerivedProperty.DerivedPropertyIO( BooleanIO ) + } ), + tandem: guides2Tandem, + phetioDocumentation: 'guides associated with the second point-of-interest on the framed object' + } ); + this.addChild( guides2Node ); + } + + this.rulerHotkeysData = { + opticPositionProperty: scene.optic.positionProperty, + opticalObject1PositionProperty: scene.arrowObject1.positionProperty, + opticalObject2PositionProperty: scene.arrowObject2.positionProperty, + opticalObject2VisibleProperty: arrowObject2Node.visibleProperty, + opticalImage1PositionProperty: scene.arrowImage1.positionProperty, + opticalImage1VisibleProperty: arrowImage1Node.visibleProperty + }; + + this.pdomOrder = [ + arrowObject1Node, + arrowObject2Node + ]; + + //TODO is this complete? + this.resetFrameObjectSceneNode = () => { + arrowObject1Node.reset(); + arrowObject2Node.reset(); + }; + } + + public reset(): void { + this.resetFrameObjectSceneNode(); + } +} + +geometricOptics.register( 'ArrowObjectSceneNode', ArrowObjectSceneNode ); +export default ArrowObjectSceneNode; \ No newline at end of file diff --git a/js/common/view/GOScreenView.ts b/js/common/view/GOScreenView.ts index bdb3dae7..265ea321 100644 --- a/js/common/view/GOScreenView.ts +++ b/js/common/view/GOScreenView.ts @@ -39,6 +39,7 @@ import { RaysType } from '../model/RaysType.js'; import GORulerNode from './GORulerNode.js'; import RulersToolbox from './RulersToolbox.js'; import FramedObjectSceneLabelsNode from './FramedObjectSceneLabelsNode.js'; +import ArrowObjectSceneNode from './ArrowObjectSceneNode.js'; // Zoom scale factors, in ascending order. // Careful! If you add values here, you may get undesirable tick intervals on rulers. @@ -252,6 +253,13 @@ class GOScreenView extends ScreenView { this.scenesTandem = options.tandem.createTandem( 'scenes' ); + const arrowObjectSceneNode = new ArrowObjectSceneNode( model.arrowObjectScene, visibleProperties, modelViewTransform, + modelVisibleBoundsProperty, modelBoundsProperty, model.raysTypeProperty, { + createOpticNode: options.createOpticNode, + dragLockedProperty: options.dragLockedProperty, + tandem: this.scenesTandem.createTandem( 'arrowObjectSceneNode' ) + } ); + const framedObjectSceneNode = new FramedObjectSceneNode( model.framedObjectScene, visibleProperties, modelViewTransform, modelVisibleBoundsProperty, modelBoundsProperty, model.raysTypeProperty, { createOpticNode: options.createOpticNode, @@ -260,7 +268,7 @@ class GOScreenView extends ScreenView { } ); const scenesNode = new Node( { - children: [ framedObjectSceneNode ] + children: [ arrowObjectSceneNode, framedObjectSceneNode ] } ); //TODO is experimentAreaNode still needed, or does scenesNode fill that role? @@ -308,6 +316,8 @@ class GOScreenView extends ScreenView { framedObjectSceneNode.visibleProperty ] ) } ); + //TODO arrowObjectSceneLabelsNode + const controlsLayer = new Node( { children: [ opticShapeRadioButtonGroup, @@ -340,6 +350,13 @@ class GOScreenView extends ScreenView { this.addChild( screenViewRootNode ); model.opticalObjectChoiceProperty.link( opticalObjectChoice => { + + arrowObjectSceneNode.visible = ( OpticalObjectChoice.isArrowObject( opticalObjectChoice ) ); + if ( arrowObjectSceneNode.visible ) { + horizontalRulerNode.setHotkeysData( arrowObjectSceneNode.rulerHotkeysData ); + verticalRulerNode.setHotkeysData( arrowObjectSceneNode.rulerHotkeysData ); + } + framedObjectSceneNode.visible = ( OpticalObjectChoice.isFramedObject( opticalObjectChoice ) ); if ( framedObjectSceneNode.visible ) { horizontalRulerNode.setHotkeysData( framedObjectSceneNode.rulerHotkeysData ); diff --git a/js/geometricOpticsStrings.ts b/js/geometricOpticsStrings.ts index 54b139b8..f2579e30 100644 --- a/js/geometricOpticsStrings.ts +++ b/js/geometricOpticsStrings.ts @@ -38,6 +38,7 @@ type StringsType = { 'many': string, 'secondPoint': string, 'valueCentimetersPattern': string, + 'arrow': string, 'pencil': string, 'penguin': string, 'planet': string, diff --git a/js/lens/model/LensModel.ts b/js/lens/model/LensModel.ts index 41127d8c..1abb32dd 100644 --- a/js/lens/model/LensModel.ts +++ b/js/lens/model/LensModel.ts @@ -36,8 +36,12 @@ class LensModel extends GOModel { // Initial position of the framed object, empirically set so that the optical axis goes through its center. framedObjectPosition: new Vector2( -170, 27 ), + arrowObject1Position: new Vector2( -150, 50 ), + arrowObject2Position: new Vector2( -150, -50 ), + // optical object choices, in the order that they will appear in OpticalObjectChoiceComboBox opticalObjectChoices: [ + OpticalObjectChoice.ARROW, OpticalObjectChoice.PENCIL, OpticalObjectChoice.PENGUIN, OpticalObjectChoice.PLANET, diff --git a/js/lens/model/LightSourceScene.ts b/js/lens/model/LightSourceScene.ts index d44b7273..5260778b 100644 --- a/js/lens/model/LightSourceScene.ts +++ b/js/lens/model/LightSourceScene.ts @@ -1,5 +1,6 @@ // Copyright 2022, University of Colorado Boulder +//TODO lots of duplication with FramedObjectScene /** * LightSourceScene is a scene in rays from 2 light sources interact with an optic, and project light spots on * a projection screen. diff --git a/js/lens/view/LensScreenView.ts b/js/lens/view/LensScreenView.ts index b8ea8a8f..bd3949f3 100644 --- a/js/lens/view/LensScreenView.ts +++ b/js/lens/view/LensScreenView.ts @@ -73,6 +73,9 @@ class LensScreenView extends GOScreenView { tandem: this.controlsTandem.createTandem( 'dragLockedButton' ) } ); this.controlsLayer.addChild( dragLockedButton ); + model.opticalObjectChoiceProperty.link( opticalObjectChoice => { + dragLockedButton.enabled = !OpticalObjectChoice.isArrowObject( opticalObjectChoice ); + } ); const lightSourceSceneNode = new LightSourceSceneNode( model.lightSourceScene, this.visibleProperties, this.modelViewTransform, this.modelVisibleBoundsProperty, this.modelBoundsProperty, model.raysTypeProperty, { diff --git a/js/mirror/model/MirrorModel.ts b/js/mirror/model/MirrorModel.ts index 621a575b..d5d41a70 100644 --- a/js/mirror/model/MirrorModel.ts +++ b/js/mirror/model/MirrorModel.ts @@ -32,8 +32,12 @@ class MirrorModel extends GOModel { // Initial position of the framed object, empirically set so that the optical axis goes through its center. framedObjectPosition: new Vector2( -170, 72.5 ), + arrowObject1Position: new Vector2( -150, 50 ), + arrowObject2Position: new Vector2( -150, -50 ), + // optical object choices, in the order that they will appear in OpticalObjectChoiceComboBox opticalObjectChoices: [ + OpticalObjectChoice.ARROW, OpticalObjectChoice.PENCIL, OpticalObjectChoice.PENGUIN, OpticalObjectChoice.PLANET,