From 799c136d4aee166c2e946b338968863af3af7ad1 Mon Sep 17 00:00:00 2001 From: pixelzoom Date: Thu, 30 Jan 2020 11:03:17 -0700 Subject: [PATCH] instrumentation of SoluteIO, https://github.com/phetsims/ph-scale/issues/92 --- js/common/model/Solute.js | 130 +++++++-------- js/common/model/SoluteIO.js | 8 +- js/common/view/PHMeterNode.js | 2 +- .../view/graph/GraphIndicatorDragHandler.js | 1 + js/mysolution/model/MySolutionModel.js | 2 + .../ph-scale-phet-io-elements-baseline.js | 156 ++++++++++++++++++ 6 files changed, 224 insertions(+), 75 deletions(-) diff --git a/js/common/model/Solute.js b/js/common/model/Solute.js index 61f64b17..0372f044 100644 --- a/js/common/model/Solute.js +++ b/js/common/model/Solute.js @@ -16,6 +16,7 @@ define( require => { const phScale = require( 'PH_SCALE/phScale' ); const PHScaleColors = require( 'PH_SCALE/common/PHScaleColors' ); const PHScaleConstants = require( 'PH_SCALE/common/PHScaleConstants' ); + const SoluteIO = require( 'PH_SCALE/common/model/SoluteIO' ); const Tandem = require( 'TANDEM/Tandem' ); const Water = require( 'PH_SCALE/common/model/Water' ); @@ -36,41 +37,45 @@ define( require => { class Solute extends PhetioObject { /** - * @param {string} name - * @param {number} pH - * @param {stockColor:Color, [dilutedColor]:Color, colorStop:{color:{Color}, [ratio]:Number} } colorScheme + * @param {string} name - the name of the solute, displayed to the user + * @param {number} pH - the pH of the solute + * @param {Color} stockColor - color of the solute in stock solution (no dilution) * @param {Object} [options] - * - * colorScheme is an object literal with these properties: - * stockColor: color of the solute in stock solution (no dilution) - * dilutedColor: color when the solute is barely present in solution (fully diluted), optional, defaults to Water.color - * colorStop: color when soluteVolume/totalVolume === ratio, used to smooth out some color transitions if provided, optional - * ratio: ratio for the color-stop, (0,1) exclusive, optional, defaults to 0.25 + * */ - constructor( name, pH, colorScheme, options ) { + constructor( name, pH, stockColor, options ) { + + assert && assert( PHScaleConstants.PH_RANGE.contains( pH ), `invalid pH: ${pH}` ); + assert && assert( stockColor instanceof Color, 'invalid color' ); options = merge( { + // {Color} color when the solute is barely present in solution (fully diluted) + dilutedColor: Water.color, + + // {Color|null} optional color use to smooth out some color transitions + colorStopColor: null, + + // {number} ratio for the color-stop, (0,1) exclusive, ignored if colorStopColor is null + colorStopRatio: 0.25, + // phet-io - tandem: Tandem.REQUIRED + tandem: Tandem.OPTIONAL, //TODO #92 should be REQUIRED + phetioState: false, + phetioType: SoluteIO }, options ); super( options ); - if ( !PHScaleConstants.PH_RANGE.contains( pH ) ) { - throw new Error( 'Solute constructor, pH value is out of range: ' + pH ); - } - - this.name = name; // @public - this.pH = pH; // @public - - // unpack the colors to make accessing them more convenient in client code - this.stockColor = colorScheme.stockColor; // @public - this.dilutedColor = colorScheme.dilutedColor || Water.color; // @private - this.colorStop = colorScheme.colorStop; // @private, optional, color computation will ignore it if undefined - if ( this.colorStop ) { - this.colorStop.ratio = this.colorStop.ratio || 0.25; - } + // @public (read-only) + this.name = name; + this.pH = pH; + this.stockColor = stockColor; + + // @private + this.dilutedColor = options.dilutedColor; + this.colorStop = options.colorStop; + this.colorStopRatio = options.colorStopRatio; } /** @@ -79,7 +84,7 @@ define( require => { * @public */ toString() { - return 'Solution[name:' + this.name + ' pH:' + this.pH + ']'; + return `Solution[name:${this.name}, pH:${this.pH}]`; } /** @@ -92,12 +97,13 @@ define( require => { assert && assert( ratio >= 0 && ratio <= 1 ); let color; if ( this.colorStop ) { - // solute has an optional color-stop - if ( ratio > this.colorStop.ratio ) { - color = Color.interpolateRGBA( this.colorStop.color, this.stockColor, ( ratio - this.colorStop.ratio ) / ( 1 - this.colorStop.ratio) ); + if ( ratio > this.colorStopRatio ) { + color = Color.interpolateRGBA( this.colorStopColor, this.stockColor, + ( ratio - this.colorStopRatio ) / ( 1 - this.colorStopRatio) ); } else { - color = Color.interpolateRGBA( this.dilutedColor, this.colorStop.color, ratio / this.colorStop.ratio ); + color = Color.interpolateRGBA( this.dilutedColor, this.colorStopColor, + ratio / this.colorStopRatio ); } } else { @@ -113,82 +119,70 @@ define( require => { * @public */ static createCustom( pH ) { - return new Solute( choiceCustomString, pH, { stockColor: PHScaleColors.WATER } ); + return new Solute( choiceCustomString, pH, PHScaleColors.WATER ); } } - // 'real world' immutable solutions + // 'real world' immutable solutions ------------------------------------------------------- // tandem for all static instances of Solute, which are used across all screens - //TODO #92 what should this be? why is there no global.solutes in the Studio tree? - const SOLUTES_TANDEM = Tandem.GLOBAL.createTandem( 'solutes' ); + //TODO #92 is global.model.solutes the correct place for this? + const SOLUTES_TANDEM = Tandem.GLOBAL.createTandem( 'model').createTandem( 'solutes' ); - Solute.DRAIN_CLEANER = new Solute( choiceDrainCleanerString, 13, { - stockColor: new Color( 255, 255, 0 ), - colorStop: { color: new Color( 255, 255, 204 ) }, + Solute.DRAIN_CLEANER = new Solute( choiceDrainCleanerString, 13, new Color( 255, 255, 0 ), { + colorStopColor: new Color( 255, 255, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'drainCleaner' ) } ); - Solute.HAND_SOAP = new Solute( choiceHandSoapString, 10, { - stockColor: new Color( 224, 141, 242 ), - colorStop: { color: new Color( 232, 204, 255 ) }, + Solute.HAND_SOAP = new Solute( choiceHandSoapString, 10, new Color( 224, 141, 242 ), { + colorStopColor: new Color( 232, 204, 255 ), tandem: SOLUTES_TANDEM.createTandem( 'handSoap' ) } ); - Solute.BLOOD = new Solute( choiceBloodString, 7.4, { - stockColor: new Color( 211, 79, 68 ), - colorStop: { color: new Color( 255, 207, 204 ) }, + Solute.BLOOD = new Solute( choiceBloodString, 7.4, new Color( 211, 79, 68 ), { + colorStopColor: new Color( 255, 207, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'blood' ) } ); - Solute.SPIT = new Solute( choiceSpitString, 7.4, { - stockColor: new Color( 202, 240, 239 ), - tandem: SOLUTES_TANDEM.createTandem( 'SPIT' ) + Solute.SPIT = new Solute( choiceSpitString, 7.4, new Color( 202, 240, 239 ), { + tandem: SOLUTES_TANDEM.createTandem( 'spit' ) } ); - Solute.WATER = new Solute( Water.name, Water.pH, { - stockColor: Water.color, + Solute.WATER = new Solute( Water.name, Water.pH, Water.color, { tandem: SOLUTES_TANDEM.createTandem( 'water' ) } ); - Solute.MILK = new Solute( choiceMilkString, 6.5, { - stockColor: new Color( 250, 250, 250 ), + Solute.MILK = new Solute( choiceMilkString, 6.5, new Color( 250, 250, 250 ), { tandem: SOLUTES_TANDEM.createTandem( 'milk' ) } ); - Solute.CHICKEN_SOUP = new Solute( choiceChickenSoupString, 5.8, { - stockColor: new Color( 255, 240, 104 ), - colorStop: { color: new Color( 255, 250, 204 ) }, + Solute.CHICKEN_SOUP = new Solute( choiceChickenSoupString, 5.8, new Color( 255, 240, 104 ), { + colorStopColor: new Color( 255, 250, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'chickenSoup' ) } ); - Solute.COFFEE = new Solute( choiceCoffeeString, 5, { - stockColor: new Color( 164, 99, 7 ), - colorStop: { color: new Color( 255, 240, 204 ) }, + Solute.COFFEE = new Solute( choiceCoffeeString, 5, new Color( 164, 99, 7 ), { + colorStopColor: new Color( 255, 240, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'coffee' ) } ); - Solute.ORANGE_JUICE = new Solute( choiceOrangeJuiceString, 3.5, { - stockColor: new Color( 255, 180, 0 ), - colorStop: { color: new Color( 255, 242, 204 ) }, + Solute.ORANGE_JUICE = new Solute( choiceOrangeJuiceString, 3.5, new Color( 255, 180, 0 ), { + colorStopColor: new Color( 255, 242, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'orangeJuice' ) } ); - Solute.SODA = new Solute( choiceSodaString, 2.5, { - stockColor: new Color( 204, 255, 102 ), - colorStop: { color: new Color( 238, 255, 204 ) }, + Solute.SODA = new Solute( choiceSodaString, 2.5, new Color( 204, 255, 102 ), { + colorStopColor: new Color( 238, 255, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'soda' ) } ); - Solute.VOMIT = new Solute( choiceVomitString, 2, { - stockColor: new Color( 255, 171, 120 ), - colorStop: { color: new Color( 255, 224, 204 ) }, + Solute.VOMIT = new Solute( choiceVomitString, 2, new Color( 255, 171, 120 ), { + colorStopColor: new Color( 255, 224, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'vomit' ) } ); - Solute.BATTERY_ACID = new Solute( choiceBatteryAcidString, 1, { - stockColor: new Color( 255, 255, 0 ), - colorStop: { color: new Color( 255, 224, 204 ) }, + Solute.BATTERY_ACID = new Solute( choiceBatteryAcidString, 1, new Color( 255, 255, 0 ), { + colorStopColor: new Color( 255, 224, 204 ), tandem: SOLUTES_TANDEM.createTandem( 'batteryAcid' ) } ); diff --git a/js/common/model/SoluteIO.js b/js/common/model/SoluteIO.js index 83f3ba3e..e04d926a 100644 --- a/js/common/model/SoluteIO.js +++ b/js/common/model/SoluteIO.js @@ -12,18 +12,14 @@ define( require => { const ObjectIO = require( 'TANDEM/types/ObjectIO' ); const phScale = require( 'PH_SCALE/phScale' ); const ReferenceIO = require( 'TANDEM/types/ReferenceIO' ); - const Solute = require( 'PH_SCALE/common/model/Solute' ); // Objects are statically created, use reference equality to look up instances for toStateObject/fromStateObject class SoluteIO extends ReferenceIO {} SoluteIO.documentation = 'the selected solute'; SoluteIO.typeName = 'SoluteIO'; - SoluteIO.validator = { isValidValue: v => v instanceof Solute }; + SoluteIO.validator = { isValidValue: value => value instanceof Object }; //TODO #92 require(Solute) is cyclic ObjectIO.validateSubtype( SoluteIO ); //TODO #92 is this the same info as SoluteIO.validator? - //TODO #92 why does Studio show all values as "phScale.requiredTandem" ? - return phScale.register( 'SoluteIO', SoluteIO ); -} ); - +} ); \ No newline at end of file diff --git a/js/common/view/PHMeterNode.js b/js/common/view/PHMeterNode.js index d1cd3f74..54ba9453 100644 --- a/js/common/view/PHMeterNode.js +++ b/js/common/view/PHMeterNode.js @@ -189,7 +189,7 @@ define( require => { } ); pHValueProperty.link( pH => { if ( pH !== null && pH !== solution.pHProperty.get() ) { - solution.soluteProperty.set( Solute.createCustom( pH ) ); + solution.soluteProperty.set( Solute.createCustom( pH ) ); //TODO #92 a new solute is created for every pH change } upArrowNode.enabled = ( pH < PHScaleConstants.PH_RANGE.max ); downArrowNode.enabled = ( pH > PHScaleConstants.PH_RANGE.min ); diff --git a/js/common/view/graph/GraphIndicatorDragHandler.js b/js/common/view/graph/GraphIndicatorDragHandler.js index 627b5238..cd2e039e 100644 --- a/js/common/view/graph/GraphIndicatorDragHandler.js +++ b/js/common/view/graph/GraphIndicatorDragHandler.js @@ -58,6 +58,7 @@ define( require => { pH = Utils.clamp( pH, PHScaleConstants.PH_RANGE.min, PHScaleConstants.PH_RANGE.max ); // Instantiate a new 'custom' solute with the desired pH, and use it with the solution. + //TODO #92 a new solute is created for every pH change solution.soluteProperty.set( Solute.createCustom( pH ) ); } } diff --git a/js/mysolution/model/MySolutionModel.js b/js/mysolution/model/MySolutionModel.js index c75f9e98..e85d0943 100644 --- a/js/mysolution/model/MySolutionModel.js +++ b/js/mysolution/model/MySolutionModel.js @@ -30,6 +30,8 @@ define( require => { // @public Beaker, everything else is positioned relative to it. Offset constants were set by visual inspection. this.beaker = new Beaker( new Vector2( 750, 580 ), new Dimension2( 450, 300 ) ); + //TODO #92 this is problematic, we probably do not want to instrument sub-elements soluteVolumeProperty and waterVolumeProperty + //TODO #92 a new solute is created for every pH change by Solute.createCustom // @public Solution in the beaker this.solution = new Solution( new Property( Solute.createCustom( 7 ) ), 0.5, 0, this.beaker.volume, { tandem: tandem.createTandem( 'solution' ) diff --git a/js/phet-io/ph-scale-phet-io-elements-baseline.js b/js/phet-io/ph-scale-phet-io-elements-baseline.js index d6a14d06..715b11c9 100644 --- a/js/phet-io/ph-scale-phet-io-elements-baseline.js +++ b/js/phet-io/ph-scale-phet-io-elements-baseline.js @@ -2159,6 +2159,162 @@ window.phet.phetio.phetioElementsBaseline = assert && "phetioStudioControl": true, "phetioTypeName": "ActionIO" }, + "phScale.global.model.solutes.batteryAcid": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.blood": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.chickenSoup": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.coffee": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.drainCleaner": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.handSoap": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.milk": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.orangeJuice": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.soda": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.spit": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.vomit": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, + "phScale.global.model.solutes.water": { + "phetioDocumentation": "", + "phetioDynamicElement": false, + "phetioEventType": "MODEL", + "phetioFeatured": false, + "phetioHighFrequency": false, + "phetioIsArchetype": false, + "phetioPlayback": false, + "phetioReadOnly": false, + "phetioState": false, + "phetioStudioControl": true, + "phetioTypeName": "SoluteIO" + }, "phScale.homeScreen.activeProperty": { "phetioDocumentation": "Indicates whether the screen is currently displayed in the simulation. For single-screen simulations, there is only one screen and it is always active.", "phetioDynamicElement": false,