From 3087d542a197e2b17e78d9b1c286753e0d67f241 Mon Sep 17 00:00:00 2001 From: AgustinVallejo Date: Mon, 14 Oct 2024 20:58:16 -0500 Subject: [PATCH] Adding particle ray view, https://github.com/phetsims/quantum-measurement/issues/53 --- js/spin/model/SpinExperiment.ts | 5 ++ js/spin/view/ParticleRayPath.ts | 83 +++++++++++++++++++++++++++++ js/spin/view/ParticleSourceNode.ts | 15 ++++-- js/spin/view/SpinMeasurementArea.ts | 64 +++++++++++++++++++--- js/spin/view/SpinScreenView.ts | 4 +- js/spin/view/SternGerlachNode.ts | 17 +++--- 6 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 js/spin/view/ParticleRayPath.ts diff --git a/js/spin/model/SpinExperiment.ts b/js/spin/model/SpinExperiment.ts index 532dad2..247dd9a 100644 --- a/js/spin/model/SpinExperiment.ts +++ b/js/spin/model/SpinExperiment.ts @@ -29,21 +29,26 @@ export default class SpinExperiment extends EnumerationValue { ] ); public static readonly EXPERIMENT_3 = new SpinExperiment( 'Experiment 3 [Sz, Sx]', [ { isZOriented: true, active: true }, + { isZOriented: false, active: true }, { isZOriented: false, active: true } ] ); public static readonly EXPERIMENT_4 = new SpinExperiment( 'Experiment 4 [Sz, Sz]', [ + { isZOriented: true, active: true }, { isZOriented: true, active: true }, { isZOriented: true, active: true } ] ); public static readonly EXPERIMENT_5 = new SpinExperiment( 'Experiment 5 [Sx, Sz]', [ { isZOriented: false, active: true }, + { isZOriented: true, active: true }, { isZOriented: true, active: true } ] ); public static readonly EXPERIMENT_6 = new SpinExperiment( 'Experiment 6 [Sx, Sx]', [ + { isZOriented: false, active: true }, { isZOriented: false, active: true }, { isZOriented: false, active: true } ] ); public static readonly CUSTOM = new SpinExperiment( 'Custom', [ + { isZOriented: false, active: false }, { isZOriented: false, active: false }, { isZOriented: false, active: false } ] ); diff --git a/js/spin/view/ParticleRayPath.ts b/js/spin/view/ParticleRayPath.ts new file mode 100644 index 0000000..24c14ae --- /dev/null +++ b/js/spin/view/ParticleRayPath.ts @@ -0,0 +1,83 @@ +// Copyright 2024, University of Colorado Boulder + +/** + * ParticleRayPath is the visual representation of a particle ray path in the UI. + * Coordinates for the three possible rays are provided, and the paths are drawn accordingly. + * + * @author Agustín Vallejo + */ + +import Multilink from '../../../../axon/js/Multilink.js'; +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; +import { Shape } from '../../../../kite/js/imports.js'; +import { Node, Path, PathOptions } from '../../../../scenery/js/imports.js'; +import Tandem from '../../../../tandem/js/Tandem.js'; +import QuantumMeasurementColors from '../../common/QuantumMeasurementColors.js'; +import quantumMeasurement from '../../quantumMeasurement.js'; + +// Constants + +export default class ParticleRayPath extends Node { + + public readonly updatePaths: ( + primaryRayPoints: Vector2[], + secondaryRayPoints: Vector2[], + tertiaryRayPoints: Vector2[] + ) => void; + + public constructor( + particleAmmountProperty: TReadOnlyProperty, + tandem: Tandem ) { + + const rayPathOptions: PathOptions = { + lineWidth: 10, + stroke: QuantumMeasurementColors.downColorProperty, + lineCap: 'round' + }; + + const primaryRayPath = new Path( null, rayPathOptions ); + const secondaryRayPath = new Path( null, rayPathOptions ); + const tertiaryRayPath = new Path( null, rayPathOptions ); + + // TODO: Multilink for when we have another value in the model for the opacity of secondary and tertiary rays https://github.com/phetsims/quantum-measurement/issues/53 + Multilink.multilink( + [ particleAmmountProperty ], + particleAmmount => { + primaryRayPath.opacity = particleAmmount; + secondaryRayPath.opacity = particleAmmount; + tertiaryRayPath.opacity = particleAmmount; + } + ); + + super( { + tandem: tandem, + localBounds: null, + children: [ + primaryRayPath, + secondaryRayPath, + tertiaryRayPath + ] + } ); + + this.updatePaths = ( + primaryRayPoints: Vector2[], + secondaryRayPoints: Vector2[], + tertiaryRayPoints: Vector2[] + ) => { + const mappedPrimaryRayPoints = primaryRayPoints.map( point => this.globalToLocalPoint( point ) ); + const mappedSecondaryRayPoints = secondaryRayPoints.map( point => this.globalToLocalPoint( point ) ); + const mappedTertiaryRayPoints = tertiaryRayPoints.map( point => this.globalToLocalPoint( point ) ); + + primaryRayPath.shape = new Shape().moveTo( mappedPrimaryRayPoints[ 0 ].x, mappedPrimaryRayPoints[ 0 ].y ) + .lineTo( mappedPrimaryRayPoints[ 1 ].x, mappedPrimaryRayPoints[ 1 ].y ); + secondaryRayPath.shape = new Shape().moveTo( mappedSecondaryRayPoints[ 0 ].x, mappedSecondaryRayPoints[ 0 ].y ) + .lineTo( mappedSecondaryRayPoints[ 1 ].x, mappedSecondaryRayPoints[ 1 ].y ); + tertiaryRayPath.shape = new Shape().moveTo( mappedTertiaryRayPoints[ 0 ].x, mappedTertiaryRayPoints[ 0 ].y ) + .lineTo( mappedTertiaryRayPoints[ 1 ].x, mappedTertiaryRayPoints[ 1 ].y ); + }; + + } +} + +quantumMeasurement.register( 'ParticleRayPath', ParticleRayPath ); \ No newline at end of file diff --git a/js/spin/view/ParticleSourceNode.ts b/js/spin/view/ParticleSourceNode.ts index 8eec747..19098f5 100644 --- a/js/spin/view/ParticleSourceNode.ts +++ b/js/spin/view/ParticleSourceNode.ts @@ -1,14 +1,15 @@ // Copyright 2024, University of Colorado Boulder /** - * ParticleSourceNode contains the UI elements around the particle source, including the particle-shooting apparatus, - * and other UI elements to control the source mode. + * ParticleSourceNode contains the UI elements of the particle source, including the particle-shooter, + * and other UI elements to control the source mode between 'single particle' and 'continuous' ray. * * @author Agustín Vallejo */ import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; import Dimension2 from '../../../../dot/js/Dimension2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; import { Shape } from '../../../../kite/js/imports.js'; import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; import { LinearGradient, Node, Path, RichText, Text, VBox } from '../../../../scenery/js/imports.js'; @@ -21,14 +22,18 @@ import quantumMeasurement from '../../quantumMeasurement.js'; import SpinModel, { SourceMode } from '../model/SpinModel.js'; // Constants -const PARTICLE_SOURCE_WIDTH = 100; -const PARTICLE_SOURCE_HEIGHT = 100; +export const PARTICLE_SOURCE_WIDTH = 120; +export const PARTICLE_SOURCE_HEIGHT = 120; const PARTICLE_SOURCE_CORNER_RADIUS = 10; export default class ParticleSourceNode extends VBox { + // Global position vector for the particle ray, it is updated outside of the constructor + public particleExitGlobalPosition = new Vector2( 0, 0 ); + public constructor( model: SpinModel, tandem: Tandem ) { + // Main shape of the component const particleSourceView = new Path( new Shape().roundRect( 0, 0, PARTICLE_SOURCE_WIDTH, PARTICLE_SOURCE_HEIGHT, PARTICLE_SOURCE_CORNER_RADIUS, PARTICLE_SOURCE_CORNER_RADIUS ), { fill: new LinearGradient( 0, 0, 0, 100 ) @@ -37,6 +42,7 @@ export default class ParticleSourceNode extends VBox { .addColorStop( 1, 'blue' ) } ); + // Button for 'single' mode const shootParticleButton = new RoundMomentaryButton( model.currentlyShootingParticlesProperty, false, true, { scale: 0.7, @@ -46,6 +52,7 @@ export default class ParticleSourceNode extends VBox { tandem: tandem.createTandem( 'shootParticleButton' ) } ); + // Slider for 'continuous' mode const sliderRange = model.particleAmmountProperty.range; const particleAmmountSlider = new HSlider( model.particleAmmountProperty, sliderRange, { thumbFill: QuantumMeasurementColors.downColorProperty, diff --git a/js/spin/view/SpinMeasurementArea.ts b/js/spin/view/SpinMeasurementArea.ts index bdd59b5..7d45550 100644 --- a/js/spin/view/SpinMeasurementArea.ts +++ b/js/spin/view/SpinMeasurementArea.ts @@ -7,6 +7,8 @@ * @author Agustín Vallejo */ +import Bounds2 from '../../../../dot/js/Bounds2.js'; +import Vector2 from '../../../../dot/js/Vector2.js'; import PhetFont from '../../../../scenery-phet/js/PhetFont.js'; import { HBox, Node, Text, VBox } from '../../../../scenery/js/imports.js'; import ComboBox, { ComboBoxItem } from '../../../../sun/js/ComboBox.js'; @@ -14,12 +16,13 @@ import Tandem from '../../../../tandem/js/Tandem.js'; import quantumMeasurement from '../../quantumMeasurement.js'; import SpinExperiment from '../model/SpinExperiment.js'; import SpinModel from '../model/SpinModel.js'; -import ParticleSourceNode from './ParticleSourceNode.js'; -import SternGerlachNode from './SternGerlachNode.js'; +import ParticleRayPath from './ParticleRayPath.js'; +import ParticleSourceNode, { PARTICLE_SOURCE_HEIGHT, PARTICLE_SOURCE_WIDTH } from './ParticleSourceNode.js'; +import SternGerlachNode, { PARTICLE_HOLE_WIDTH, STERN_GERLACH_HEIGHT, STERN_GERLACH_WIDTH } from './SternGerlachNode.js'; export default class SpinMeasurementArea extends VBox { - public constructor( model: SpinModel, parentNode: Node, tandem: Tandem ) { + public constructor( model: SpinModel, parentNode: Node, layoutBounds: Bounds2, tandem: Tandem ) { const items: ComboBoxItem[] = SpinExperiment.enumeration.values.map( experiment => { return { @@ -32,6 +35,19 @@ export default class SpinMeasurementArea extends VBox { tandem: tandem.createTandem( 'experimentComboBox' ) } ); + const particleSourceNode = new ParticleSourceNode( model, tandem.createTandem( 'particleSourceNode' ) ); + + const firstSternGerlachNode = new SternGerlachNode( model.firstSternGerlachModel, tandem.createTandem( 'firstSternGerlachNode' ) ); + const secondSternGerlachNode = new SternGerlachNode( model.secondSternGerlachModel, tandem.createTandem( 'secondSternGerlachNode' ) ); + const thirdSternGerlachNode = new SternGerlachNode( model.thirdSternGerlachModel, tandem.createTandem( 'thirdSternGerlachNode' ) ); + + const particleRayPath = new ParticleRayPath( + model.particleAmmountProperty, + tandem.createTandem( 'particleRayPath' ) + ); + + parentNode.addChild( particleRayPath ); + super( { children: [ experimentComboBox, @@ -41,13 +57,13 @@ export default class SpinMeasurementArea extends VBox { new HBox( { spacing: 50, children: [ - new ParticleSourceNode( model, tandem ), - new SternGerlachNode( model.firstSternGerlachModel, tandem.createTandem( 'firstSternGerlachNode' ) ), + particleSourceNode, + firstSternGerlachNode, new VBox( { spacing: 20, children: [ - new SternGerlachNode( model.secondSternGerlachModel, tandem.createTandem( 'secondSternGerlachNode' ) ), - new SternGerlachNode( model.secondSternGerlachModel, tandem.createTandem( 'thirdSternGerlachNode' ) ) + secondSternGerlachNode, + thirdSternGerlachNode ] } ) ] @@ -57,6 +73,40 @@ export default class SpinMeasurementArea extends VBox { spacing: 20, margin: 20 } ); + + model.currentExperimentProperty.link( experiment => { + + // Adjust the global positions of the nodes + particleSourceNode.particleExitGlobalPosition = particleSourceNode.localToGlobalPoint( new Vector2( PARTICLE_SOURCE_WIDTH, PARTICLE_SOURCE_HEIGHT ) ); + + firstSternGerlachNode.entranceGlobalPosition = firstSternGerlachNode.localToGlobalPoint( new Vector2( 0, STERN_GERLACH_HEIGHT / 2 ) ); + firstSternGerlachNode.topExitGlobalPosition = firstSternGerlachNode.localToGlobalPoint( new Vector2( STERN_GERLACH_WIDTH + PARTICLE_HOLE_WIDTH, STERN_GERLACH_HEIGHT / 4 ) ); + firstSternGerlachNode.bottomExitGlobalPosition = firstSternGerlachNode.localToGlobalPoint( new Vector2( STERN_GERLACH_WIDTH + PARTICLE_HOLE_WIDTH, 3 * STERN_GERLACH_HEIGHT / 4 ) ); + + secondSternGerlachNode.entranceGlobalPosition = secondSternGerlachNode.localToGlobalPoint( new Vector2( -PARTICLE_HOLE_WIDTH, STERN_GERLACH_HEIGHT / 2 ) ); + + thirdSternGerlachNode.entranceGlobalPosition = thirdSternGerlachNode.localToGlobalPoint( new Vector2( -PARTICLE_HOLE_WIDTH, STERN_GERLACH_HEIGHT / 2 ) ); + + // Update the paths of the particle rays according to the current experiment (if 2nd and 3rd SG are not visible, the rays keep going on) + const primaryRayPoints = [ particleSourceNode.particleExitGlobalPosition, firstSternGerlachNode.entranceGlobalPosition ]; + + console.log( 'jiji', firstSternGerlachNode.topExitGlobalPosition ); + + if ( experiment.experimentSettings.length === 1 ) { + particleRayPath.updatePaths( + primaryRayPoints, + [ firstSternGerlachNode.topExitGlobalPosition, new Vector2( layoutBounds.maxX * 5, firstSternGerlachNode.topExitGlobalPosition.y ) ], + [ firstSternGerlachNode.bottomExitGlobalPosition, new Vector2( layoutBounds.maxX * 5, firstSternGerlachNode.bottomExitGlobalPosition.y ) ] + ); + } + else { + particleRayPath.updatePaths( + primaryRayPoints, + [ firstSternGerlachNode.topExitGlobalPosition, secondSternGerlachNode.entranceGlobalPosition ], + [ firstSternGerlachNode.bottomExitGlobalPosition, thirdSternGerlachNode.entranceGlobalPosition ] + ); + } + } ); } } diff --git a/js/spin/view/SpinScreenView.ts b/js/spin/view/SpinScreenView.ts index 15cc808..aee3e95 100644 --- a/js/spin/view/SpinScreenView.ts +++ b/js/spin/view/SpinScreenView.ts @@ -40,7 +40,7 @@ export default class SpinScreenView extends QuantumMeasurementScreenView { } ); this.addChild( dividingLine ); - const spinMeasurementArea = new SpinMeasurementArea( model, this, tandem.createTandem( 'spinMeasurementArea' ) ); + const spinMeasurementArea = new SpinMeasurementArea( model, this, this.layoutBounds, tandem.createTandem( 'spinMeasurementArea' ) ); spinMeasurementArea.centerX = 2 * dividingLineX; this.addChild( spinMeasurementArea ); @@ -49,6 +49,8 @@ export default class SpinScreenView extends QuantumMeasurementScreenView { spinStatePreparationArea.opacity = 1 - opacity; spinMeasurementArea.opacity = 1 - opacity; } ); + + model.currentExperimentProperty.notifyListenersStatic(); } public override reset(): void { diff --git a/js/spin/view/SternGerlachNode.ts b/js/spin/view/SternGerlachNode.ts index 812c064..f175e2f 100644 --- a/js/spin/view/SternGerlachNode.ts +++ b/js/spin/view/SternGerlachNode.ts @@ -16,19 +16,21 @@ import quantumMeasurement from '../../quantumMeasurement.js'; import SternGerlachModel from '../model/SternGerlachModel.js'; // Constants -const STERN_GERLACH_WIDTH = 150; -const STERN_GERLACH_HEIGHT = 100; -const PARTICLE_HOLE_WIDTH = 5; +export const STERN_GERLACH_WIDTH = 150; +export const STERN_GERLACH_HEIGHT = 100; +export const PARTICLE_HOLE_WIDTH = 5; const PARTICLE_HOLE_HEIGHT = 20; export default class SternGerlachNode extends Node { - public readonly entranceGlobalPosition: Vector2; - public readonly boxWidth = STERN_GERLACH_WIDTH; - public readonly boxHeight = STERN_GERLACH_HEIGHT; + // Global position vectors, they are to be updated outside of the constructor + public entranceGlobalPosition = new Vector2( 0, 0 ); + public topExitGlobalPosition = new Vector2( 0, 0 ); + public bottomExitGlobalPosition = new Vector2( 0, 0 ); public constructor( experimentModel: SternGerlachModel, tandem: Tandem ) { + // Component for the entry and exit points of the SG apparatus const createParticleHole = ( x: number, y: number ) => { return new Path( new Shape().rect( x, y, PARTICLE_HOLE_WIDTH, PARTICLE_HOLE_HEIGHT ), { @@ -42,6 +44,7 @@ export default class SternGerlachNode extends Node { return Math.pow( x, 2 ); }; + // Decoration curves that go in the front of the main rectangle const curveUpShape = new Shape().moveTo( 0, STERN_GERLACH_HEIGHT / 2 ); const curveDownShape = new Shape().moveTo( 0, STERN_GERLACH_HEIGHT / 2 ); @@ -76,8 +79,6 @@ export default class SternGerlachNode extends Node { { font: new PhetFont( 16 ), fill: 'white', center: new Vector2( 25, 80 ) } ) ] } ); - - this.entranceGlobalPosition = this.localToGlobalPoint( new Vector2( 0, STERN_GERLACH_HEIGHT / 2 ) ); } }