Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Tandem.GroupTandem and previous state pattern #87

Closed
8 tasks done
chrisklus opened this issue Apr 17, 2019 · 14 comments
Closed
8 tasks done

Remove Tandem.GroupTandem and previous state pattern #87

chrisklus opened this issue Apr 17, 2019 · 14 comments

Comments

@chrisklus
Copy link
Contributor

chrisklus commented Apr 17, 2019

From https://github.com/phetsims/phet-io/issues/1443. Tandem.GroupTandem was also marked as deprecated upon the completion of https://github.com/phetsims/phet-io/issues/1442.

In order for phet-io metadata file validation to work (without our current workaround), we need the usages of GroupTandem to be replaced with GroupMemberTandem.

UPDATE by @samreid: Here is a list of sims using createGroupTandem which should be converted to use PhetioGroup

@chrisklus
Copy link
Contributor Author

We added a TODO in molecules-and-light for converting to use PhetioGroup

@zepumph zepumph changed the title Remove Tandem.GroupTandem Remove Tandem.GroupTandem and previous state pattern Nov 15, 2019
@pixelzoom
Copy link
Contributor

I see this TODO in Tandem:

    getTermRegex() {
      return /^[a-zA-Z0-9~]+$/; // TODO: eliminate ~ once GroupTandem has been deleted, see https://github.com/phetsims/tandem/issues/87
    }

... and GroupTandem no longer appears to exist. Can this TODO now be addressed?

@pixelzoom
Copy link
Contributor

Ah, never mind. GroupTandem is still an inner class of Tandem.

@zepumph
Copy link
Member

zepumph commented May 29, 2020

@zepumph
Copy link
Member

zepumph commented May 29, 2020

  • Once this is complete, we can delete the addChildElementDeprecated method from PhetioStateEngine.js

samreid added a commit to phetsims/molecules-and-light that referenced this issue Sep 29, 2020
samreid added a commit to phetsims/energy-skate-park that referenced this issue Sep 29, 2020
samreid added a commit to phetsims/color-vision that referenced this issue Sep 29, 2020
samreid added a commit to phetsims/build-an-atom that referenced this issue Sep 29, 2020
samreid added a commit to phetsims/balloons-and-static-electricity that referenced this issue Dec 27, 2020
samreid added a commit to phetsims/balloons-and-static-electricity that referenced this issue Dec 28, 2020
samreid added a commit to phetsims/circuit-construction-kit-black-box-study that referenced this issue Jan 20, 2021
samreid added a commit to phetsims/circuit-construction-kit-common that referenced this issue Jan 20, 2021
samreid added a commit to phetsims/friction that referenced this issue Jan 20, 2021
@samreid
Copy link
Member

samreid commented Jan 20, 2021

@zepumph and I discussed that it may be appropriate to undeprecate GroupTandem for cases like phetsims/balloons-and-static-electricity@e0dbeaa, I'll consider this as I continue.

@samreid
Copy link
Member

samreid commented Jan 23, 2021

For Friction, @zepumph recommended uninstrumenting the Atoms. When we looked closer, we saw that the tandem passed to Atom wasn't actually used. So I uninstrumented them, but testing in the state wrapper noticed that sometimes things get out of sync:

image

But that is a pre-existing problem and seems most appropriate to address once we focus on Friction. I'll commit this change to get rid of using the deprecated pattern and useless instrumentation.

samreid added a commit to phetsims/molarity that referenced this issue Jan 23, 2021
samreid added a commit to phetsims/ohms-law that referenced this issue Jan 23, 2021
samreid added a commit to phetsims/balloons-and-static-electricity that referenced this issue Jan 23, 2021
samreid added a commit to phetsims/build-an-atom that referenced this issue Jan 23, 2021
samreid added a commit to phetsims/shred that referenced this issue Jan 23, 2021
@samreid
Copy link
Member

samreid commented Jan 23, 2021

Since energy skate park has validation: false, it is actually safe to use GroupTandem to create the control points and tracks. I considered that we may wish to wait until we revisit the sim to change this pattern, but perhaps I'll have a try at PhetioGroup to see how it goes.

UPDATE: The reason I brought it up--if I turn on validation, there are other problems upstream from the problem at-hand, making it more difficult to develop and test. That's a good argument for waiting until we revisit the sim.

UPDATE: Likewise, the state wrapper is not even working with the deprecated pattern, even with validation disabled.

samreid added a commit to phetsims/build-an-atom that referenced this issue Jan 23, 2021
samreid added a commit to phetsims/energy-skate-park that referenced this issue Jan 23, 2021
@samreid
Copy link
Member

samreid commented Jan 23, 2021

I converted EnergySkatePark to use PhetioGroup for the ControlPoints, Tracks and TrackNodes. The sim is running in standalone mode, and Studio seems to be working OK. I considered creating a branch to get reviewed before merging to master, but it is working well enough that I think we should commit to master soon.

Here's a patch of my current progress (needs self-review and cleanup)

Index: js/common/model/PremadeTracks.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/PremadeTracks.js b/js/common/model/PremadeTracks.js
--- a/js/common/model/PremadeTracks.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/PremadeTracks.js	(date 1611443758656)
@@ -16,7 +16,6 @@
 import Enumeration from '../../../../phet-core/js/Enumeration.js';
 import merge from '../../../../phet-core/js/merge.js';
 import energySkatePark from '../../energySkatePark.js';
-import ControlPoint from './ControlPoint.js';
 import Track from './Track.js';
 
 // constants
@@ -74,11 +73,11 @@
 
   /**
    * Create a set of control points that create a parabola shaped track.
-   * @param {Tandem} groupTandem
+   * @param {EnergySkateParkModel} model
    * @param {Object} [options]
    * @returns {ControlPoint[]}
    */
-  createParabolaControlPoints: ( groupTandem, options ) => {
+  createParabolaControlPoints: ( model, options ) => {
     options = merge( {
       trackHeight: 6, // largest height for the parabola
       trackWidth: 8, // width from the left most control point to the right most control point
@@ -98,22 +97,19 @@
     const p3Bounds = createCenteredLimitBounds( p3, END_BOUNDS_WIDTH, END_BOUNDS_HEIGHT );
 
     return [
-      new ControlPoint( p1.x, p1.y, {
+      model.controlPointGroup.createNextElement( p1.x, p1.y, {
         visible: options.p1Visible,
         limitBounds: p1Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p2.x, p2.y, {
+      model.controlPointGroup.createNextElement( p2.x, p2.y, {
         visible: options.p2Visible,
         limitBounds: p2Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p3.x, p3.y, {
+      model.controlPointGroup.createNextElement( p3.x, p3.y, {
         visible: options.p3Visible,
         limitBounds: p3Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } )
     ];
@@ -122,10 +118,11 @@
   /**
    * Create a set of control points which create a slope shaped track, touching the ground on the right side.
    *
-   * @param {Tandem} groupTandem
+   * @param {EnergySkateParkModel} model
+   * @param {Object} [options]
    * @returns {ControlPoint[]}
    */
-  createSlopeControlPoints: ( groupTandem, options ) => {
+  createSlopeControlPoints: ( model,  options ) => {
 
     options = merge( {
       trackWidth: 6,
@@ -143,19 +140,16 @@
     const p3Bounds = createRelativeSpaceBounds( p3, 0.5, 2.5, 3, 0 );
 
     return [
-      new ControlPoint( p1.x, p1.y, {
+      model.controlPointGroup.createNextElement( p1.x, p1.y, {
         limitBounds: p1Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p2.x, p2.y, {
+      model.controlPointGroup.createNextElement( p2.x, p2.y, {
         limitBounds: p2Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p3.x, p3.y, {
+      model.controlPointGroup.createNextElement( p3.x, p3.y, {
         limitBounds: p3Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } )
     ];
@@ -166,11 +160,11 @@
    * a but since the interpolation moves it down by that much and we don't want the skater to go below ground
    * while on the track. Numbers determined by trial and error.
    *
-   * @param {Tandem} groupTandem
+   * @param {EnergySkateParkModel} model
    * @param {Object} [options]
    * @returns {ControlPoint[]}
    */
-  createDoubleWellControlPoints: ( groupTandem, options ) => {
+  createDoubleWellControlPoints: ( model, options ) => {
     options = merge( {
       trackHeight: 5, // largest height for the well
       trackWidth: 8, // width from the left most control point to the right most control point
@@ -210,35 +204,30 @@
     const p5Bounds = createRelativeSpaceBounds( p5, 1.5, 1.0, options.p5UpSpacing, options.p5DownSpacing );
 
     return [
-      new ControlPoint( p1.x, p1.y, {
+      model.controlPointGroup.createNextElement( p1.x, p1.y, {
         limitBounds: p1Bounds,
         visible: options.p1Visible,
-        phetioState: false,
-        tandem: groupTandem.createNextTandem()
+        phetioState: false
       } ),
-      new ControlPoint( p2.x, p2.y, {
+      model.controlPointGroup.createNextElement( p2.x, p2.y, {
         limitBounds: p2Bounds,
         visible: options.p2Visible,
-        phetioState: false,
-        tandem: groupTandem.createNextTandem()
+        phetioState: false
       } ),
-      new ControlPoint( p3.x, p3.y, {
+      model.controlPointGroup.createNextElement( p3.x, p3.y, {
         limitBounds: p3Bounds,
         visible: options.p3Visible,
-        phetioState: false,
-        tandem: groupTandem.createNextTandem()
+        phetioState: false
       } ),
-      new ControlPoint( p4.x, p4.y, {
+      model.controlPointGroup.createNextElement( p4.x, p4.y, {
         limitBounds: p4Bounds,
         visible: options.p4Visible,
-        phetioState: false,
-        tandem: groupTandem.createNextTandem()
+        phetioState: false
       } ),
-      new ControlPoint( p5.x, p5.y, {
+      model.controlPointGroup.createNextElement( p5.x, p5.y, {
         limitBounds: p5Bounds,
         visible: options.p5Visible,
-        phetioState: false,
-        tandem: groupTandem.createNextTandem()
+        phetioState: false
       } )
     ];
   },
@@ -246,10 +235,11 @@
   /**
    * Create a set of control points that will form a track that takes the shape of a loop.
    *
-   * @param  {Tandem} groupTandem
+   * @param {EnergySkateParkModel} model
+   * @param {Object} [options]
    * @returns {Array.<ControlPoint>}
    */
-  createLoopControlPoints: ( groupTandem, options ) => {
+  createLoopControlPoints: ( model,  options ) => {
     options = merge( {
       trackWidth: 9,
       trackHeight: 6,
@@ -284,39 +274,32 @@
     const p7Bounds = createRelativeSpaceBounds( p7, 1.5, 0.5, 2, 3 );
 
     return [
-      new ControlPoint( p1.x, p1.y, {
+      model.controlPointGroup.createNextElement( p1.x, p1.y, {
         limitBounds: p1Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p2.x, p2.y, {
+      model.controlPointGroup.createNextElement( p2.x, p2.y, {
         limitBounds: p2Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p3.x, p3.y, {
+      model.controlPointGroup.createNextElement( p3.x, p3.y, {
         limitBounds: p3Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p4.x, p4.y, {
+      model.controlPointGroup.createNextElement( p4.x, p4.y, {
         limitBounds: p4Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p5.x, p5.y, {
+      model.controlPointGroup.createNextElement( p5.x, p5.y, {
         limitBounds: p5Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p6.x, p6.y, {
+      model.controlPointGroup.createNextElement( p6.x, p6.y, {
         limitBounds: p6Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } ),
-      new ControlPoint( p7.x, p7.y, {
+      model.controlPointGroup.createNextElement( p7.x, p7.y, {
         limitBounds: p7Bounds,
-        tandem: groupTandem.createNextTandem(),
         phetioState: false
       } )
     ];
@@ -325,11 +308,9 @@
   /**
    * Create a track from the provided control points.
    *
-   * @param  {EnergySkateParkModel} model
-   * @param  {Array.<Track>} modelTracks
-   * @param  {Array.<ControlPoint>} controlPoints
-   * @param  {Property.<Bounds2>} availableBoundsProperty
-   * @param  {object} options
+   * @param {EnergySkateParkModel} model
+   * @param {Array.<ControlPoint>} controlPoints
+   * @param {Object} [options]
    * @returns {Track}
    */
   createTrack( model, controlPoints, options ) {
@@ -337,8 +318,7 @@
   }
 };
 
-// @public
-// @static
+// @public @static
 PremadeTracks.TrackType = TrackType;
 
 energySkatePark.register( 'PremadeTracks', PremadeTracks );
Index: js/common/model/ControlPoint.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/ControlPoint.js b/js/common/model/ControlPoint.js
--- a/js/common/model/ControlPoint.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/ControlPoint.js	(date 1611444490687)
@@ -18,7 +18,6 @@
 import Tandem from '../../../../tandem/js/Tandem.js';
 import IOType from '../../../../tandem/js/types/IOType.js';
 import NullableIO from '../../../../tandem/js/types/NullableIO.js';
-import ReferenceIO from '../../../../tandem/js/types/ReferenceIO.js';
 import energySkatePark from '../../energySkatePark.js';
 
 class ControlPoint extends PhetioObject {
@@ -117,22 +116,27 @@
 
   /**
    * Create a new control point, identical to this one.
+   * TODO: https://github.com/phetsims/tandem/issues/87 is this reversed?  Maybe call on the model
    * @public
    *
-   * @param {Tandem} tandem
+   * @param {EnergySkateParkModel} model
    * @returns {ControlPoint}
    */
-  copy( tandem ) {
-    return new ControlPoint( this.positionProperty.value.x, this.positionProperty.value.y, {
-      tandem: tandem
-    } );
+  copy( model ) {
+    return model.controlPointGroup.createNextElement( this.positionProperty.value.x, this.positionProperty.value.y );
   }
 }
 
 ControlPoint.ControlPointIO = new IOType( 'ControlPointIO', {
   valueType: ControlPoint,
   documentation: 'A control point that can manipulate the track.',
-  supertype: ReferenceIO( IOType.ObjectIO )
+  toStateObject: controlPoint => {
+    return { x: controlPoint.positionProperty.value.x, y: controlPoint.positionProperty.value.y };
+  },
+  stateToArgsForConstructor: stateObject => {
+    assert && assert( typeof stateObject.x === 'number' );
+    return [ stateObject.x, stateObject.y ];
+  }
 } );
 
 energySkatePark.register( 'ControlPoint', ControlPoint );
Index: js/common/model/Track.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/Track.js b/js/common/model/Track.js
--- a/js/common/model/Track.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/Track.js	(date 1611443365033)
@@ -77,11 +77,11 @@
     const tandem = options.tandem;
 
     // @private
-    this.model = model;
     this.parents = parents;
     this.trackTandem = tandem;
 
     // @public (read-only) - see options
+    this.model = model;
     this.draggable = options.draggable;
     this.configurable = options.configurable;
     this.splittable = options.splittable;
Index: js/common/model/DebugTracks.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/DebugTracks.js b/js/common/model/DebugTracks.js
--- a/js/common/model/DebugTracks.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/DebugTracks.js	(date 1611443745611)
@@ -10,7 +10,6 @@
 import Vector2 from '../../../../dot/js/Vector2.js';
 import energySkatePark from '../../energySkatePark.js';
 import EnergySkateParkQueryParameters from '../EnergySkateParkQueryParameters.js';
-import ControlPoint from './ControlPoint.js';
 import Track from './Track.js';
 
 class DebugTracks {
@@ -22,16 +21,10 @@
    * @public
    *
    * @param {EnergySkateParkModel} model
-   * @param {Tandem} controlPointGroupTandem
-   * @param {Tandem} trackGroupTandem
    */
-  static init( model, controlPointGroupTandem, trackGroupTandem ) {
+  static init( model) {
     // Tracks to help demonstrate issues
 
-    const createControlPoint = ( x, y ) => {
-      return new ControlPoint( x, y, { tandem: controlPointGroupTandem.createNextTandem() } );
-    };
-
     let controlPoints = null;
     let track = null;
     if ( EnergySkateParkQueryParameters.testTrackIndex === 1 ) {
@@ -41,8 +34,8 @@
       model.skater.positionProperty.set( new Vector2( -5, 8 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 3.9238282647584946, 3.1917866726296955 ), createControlPoint( 2.043971377459748, 4.847851073345259 ), createControlPoint( -1.116994633273702, 3.686296958855098 ), createControlPoint( -3.5806797853309487, 1.8639512522361352 ), createControlPoint( -5.982719141323793, 6.235364490161 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 3.9238282647584946, 3.1917866726296955 ), model.controlPointGroup.createNextElement( 2.043971377459748, 4.847851073345259 ), model.controlPointGroup.createNextElement( -1.116994633273702, 3.686296958855098 ), model.controlPointGroup.createNextElement( -3.5806797853309487, 1.8639512522361352 ), model.controlPointGroup.createNextElement( -5.982719141323793, 6.235364490161 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -55,8 +48,8 @@
       model.skater.positionProperty.set( new Vector2( -5, 7.7 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 3.9238282647584946, 3.1917866726296955 ), createControlPoint( 2.043971377459748, 4.847851073345259 ), createControlPoint( -1.116994633273702, 3.686296958855098 ), createControlPoint( -3.5806797853309487, 1.8639512522361352 ), createControlPoint( -5.982719141323793, 6.235364490161 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 3.9238282647584946, 3.1917866726296955 ), model.controlPointGroup.createNextElement( 2.043971377459748, 4.847851073345259 ), model.controlPointGroup.createNextElement( -1.116994633273702, 3.686296958855098 ), model.controlPointGroup.createNextElement( -3.5806797853309487, 1.8639512522361352 ), model.controlPointGroup.createNextElement( -5.982719141323793, 6.235364490161 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -69,8 +62,8 @@
       model.skater.positionProperty.set( new Vector2( -5, 7.7 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( -1.8031842576028616, 3.53633273703041 ), createControlPoint( 1.7306618962432907, 2.8187991949910547 ), createControlPoint( 1.9246153846153842, 4.3405881037567084 ), createControlPoint( 3.834311270125223, 4.907529069767442 ), createControlPoint( 3.491162790697672, 1.0732177996422188 ), createControlPoint( -2.760107334525939, 1.461124776386404 ), createControlPoint( -5.162146690518783, 5.832538014311269 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( -1.8031842576028616, 3.53633273703041 ), model.controlPointGroup.createNextElement( 1.7306618962432907, 2.8187991949910547 ), model.controlPointGroup.createNextElement( 1.9246153846153842, 4.3405881037567084 ), model.controlPointGroup.createNextElement( 3.834311270125223, 4.907529069767442 ), model.controlPointGroup.createNextElement( 3.491162790697672, 1.0732177996422188 ), model.controlPointGroup.createNextElement( -2.760107334525939, 1.461124776386404 ), model.controlPointGroup.createNextElement( -5.162146690518783, 5.832538014311269 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -83,8 +76,8 @@
       model.skater.positionProperty.set( new Vector2( -5, 7.7 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 4.639964221824686, 6.68294946332737 ), createControlPoint( 1.4173524150268335, 0.938942307692308 ), createControlPoint( -3.207692307692308, 3.997439624329159 ), createControlPoint( 3.2524508050089445, 3.9079226296958858 ), createControlPoint( 3.491162790697672, 1.0732177996422188 ), createControlPoint( -2.760107334525939, 1.461124776386404 ), createControlPoint( -5.162146690518783, 5.832538014311269 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 4.639964221824686, 6.68294946332737 ), model.controlPointGroup.createNextElement( 1.4173524150268335, 0.938942307692308 ), model.controlPointGroup.createNextElement( -3.207692307692308, 3.997439624329159 ), model.controlPointGroup.createNextElement( 3.2524508050089445, 3.9079226296958858 ), model.controlPointGroup.createNextElement( 3.491162790697672, 1.0732177996422188 ), model.controlPointGroup.createNextElement( -2.760107334525939, 1.461124776386404 ), model.controlPointGroup.createNextElement( -5.162146690518783, 5.832538014311269 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -97,8 +90,8 @@
       model.skater.positionProperty.set( new Vector2( -5, 7.7 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 4.431091234347049, 7.9252447313977665 ), createControlPoint( 2.4169588550983896, 7.975935759156005 ), createControlPoint( -1.9874106197862114, 4.75700797278857 ), createControlPoint( 0.13992761930286512, 6.207060140642635 ), createControlPoint( 1.447191413237924, 1.0090653610430707 ), createControlPoint( -1.7008228980322002, 1.0717102008522177 ), createControlPoint( -5.37101967799642, 7.0748332823816655 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 4.431091234347049, 7.9252447313977665 ), model.controlPointGroup.createNextElement( 2.4169588550983896, 7.975935759156005 ), model.controlPointGroup.createNextElement( -1.9874106197862114, 4.75700797278857 ), model.controlPointGroup.createNextElement( 0.13992761930286512, 6.207060140642635 ), model.controlPointGroup.createNextElement( 1.447191413237924, 1.0090653610430707 ), model.controlPointGroup.createNextElement( -1.7008228980322002, 1.0717102008522177 ), model.controlPointGroup.createNextElement( -5.37101967799642, 7.0748332823816655 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -111,8 +104,8 @@
       model.skater.positionProperty.set( new Vector2( 5, 7.9 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 5.147227191413236, 6.57851296958855 ), createControlPoint( 0.05887058823529401, 1.0476264705882334 ), createControlPoint( -1.9427294117647067, 2.637132352941175 ), createControlPoint( -3.1201411764705886, 6.404849999999999 ), createControlPoint( 0.5690823529411766, 6.071249999999999 ), createControlPoint( -2.3940705882352944, 1.3419794117647044 ), createControlPoint( -5.474964705882353, 6.5029676470588225 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 5.147227191413236, 6.57851296958855 ), model.controlPointGroup.createNextElement( 0.05887058823529401, 1.0476264705882334 ), model.controlPointGroup.createNextElement( -1.9427294117647067, 2.637132352941175 ), model.controlPointGroup.createNextElement( -3.1201411764705886, 6.404849999999999 ), model.controlPointGroup.createNextElement( 0.5690823529411766, 6.071249999999999 ), model.controlPointGroup.createNextElement( -2.3940705882352944, 1.3419794117647044 ), model.controlPointGroup.createNextElement( -5.474964705882353, 6.5029676470588225 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -125,8 +118,8 @@
       model.skater.positionProperty.set( new Vector2( 5, 7.9 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 5.147227191413236, 6.57851296958855 ), createControlPoint( -0.43896196231781204, 1.7569427657305372 ), createControlPoint( -1.1787355229664587, 2.807585005572261 ), createControlPoint( -3.1201411764705886, 6.404849999999999 ), createControlPoint( 0.5690823529411766, 6.071249999999999 ), createControlPoint( -2.3940705882352944, 1.3419794117647044 ), createControlPoint( -5.474964705882353, 6.5029676470588225 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 5.147227191413236, 6.57851296958855 ), model.controlPointGroup.createNextElement( -0.43896196231781204, 1.7569427657305372 ), model.controlPointGroup.createNextElement( -1.1787355229664587, 2.807585005572261 ), model.controlPointGroup.createNextElement( -3.1201411764705886, 6.404849999999999 ), model.controlPointGroup.createNextElement( 0.5690823529411766, 6.071249999999999 ), model.controlPointGroup.createNextElement( -2.3940705882352944, 1.3419794117647044 ), model.controlPointGroup.createNextElement( -5.474964705882353, 6.5029676470588225 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -139,8 +132,8 @@
       model.skater.positionProperty.set( new Vector2( 5, 7.9 ) );
       model.skater.released( null, 0 );
 
-      controlPoints = [ createControlPoint( 5.07086859688196, 6.925682071269487 ), createControlPoint( 2.061781737193762, 0.7625271732714408 ), createControlPoint( 0.09287305122494338, 0.7625271732714408 ), createControlPoint( -3.287706013363029, 3.0472042334050697 ), createControlPoint( -2.2289532293986642, 4.399535077951003 ), createControlPoint( -0.6129621380846331, 4.306662026726059 ), createControlPoint( 0.7429844097995542, 3.3629726075698803 ), createControlPoint( 0.14859688195991083, 2.3227944338505053 ), createControlPoint( -1.4302449888641426, 1.4159674088426304 ), createControlPoint( -4.532204899777283, 0.580109947818132 ), createControlPoint( -6.1185746102449885, 7.75698912376468 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 5.07086859688196, 6.925682071269487 ), model.controlPointGroup.createNextElement( 2.061781737193762, 0.7625271732714408 ), model.controlPointGroup.createNextElement( 0.09287305122494338, 0.7625271732714408 ), model.controlPointGroup.createNextElement( -3.287706013363029, 3.0472042334050697 ), model.controlPointGroup.createNextElement( -2.2289532293986642, 4.399535077951003 ), model.controlPointGroup.createNextElement( -0.6129621380846331, 4.306662026726059 ), model.controlPointGroup.createNextElement( 0.7429844097995542, 3.3629726075698803 ), model.controlPointGroup.createNextElement( 0.14859688195991083, 2.3227944338505053 ), model.controlPointGroup.createNextElement( -1.4302449888641426, 1.4159674088426304 ), model.controlPointGroup.createNextElement( -4.532204899777283, 0.580109947818132 ), model.controlPointGroup.createNextElement( -6.1185746102449885, 7.75698912376468 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -154,8 +147,8 @@
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0;
 
-      controlPoints = [ createControlPoint( 5.516659242761692, 5.458287861915368 ), createControlPoint( 2.061781737193762, 0.7625271732714408 ), createControlPoint( 0.09287305122494338, 0.7625271732714408 ), createControlPoint( -3.287706013363029, 3.0472042334050697 ), createControlPoint( -2.2289532293986642, 4.399535077951003 ), createControlPoint( -0.6129621380846331, 4.306662026726059 ), createControlPoint( 0.7429844097995542, 3.3629726075698803 ), createControlPoint( 0.14859688195991083, 2.3227944338505053 ), createControlPoint( -1.4302449888641426, 1.4159674088426304 ), createControlPoint( -4.532204899777283, 0.580109947818132 ), createControlPoint( -6.1185746102449885, 7.75698912376468 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 5.516659242761692, 5.458287861915368 ), model.controlPointGroup.createNextElement( 2.061781737193762, 0.7625271732714408 ), model.controlPointGroup.createNextElement( 0.09287305122494338, 0.7625271732714408 ), model.controlPointGroup.createNextElement( -3.287706013363029, 3.0472042334050697 ), model.controlPointGroup.createNextElement( -2.2289532293986642, 4.399535077951003 ), model.controlPointGroup.createNextElement( -0.6129621380846331, 4.306662026726059 ), model.controlPointGroup.createNextElement( 0.7429844097995542, 3.3629726075698803 ), model.controlPointGroup.createNextElement( 0.14859688195991083, 2.3227944338505053 ), model.controlPointGroup.createNextElement( -1.4302449888641426, 1.4159674088426304 ), model.controlPointGroup.createNextElement( -4.532204899777283, 0.580109947818132 ), model.controlPointGroup.createNextElement( -6.1185746102449885, 7.75698912376468 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -168,8 +161,8 @@
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0.0363651226158039;
 
-      controlPoints = [ createControlPoint( 5.07086859688196, 6.925682071269487 ), createControlPoint( 2.061781737193762, 0.7625271732714408 ), createControlPoint( 0.09287305122494338, 0.7625271732714408 ), createControlPoint( -3.287706013363029, 3.0472042334050697 ), createControlPoint( -2.2289532293986642, 4.399535077951003 ), createControlPoint( -0.6129621380846331, 4.306662026726059 ), createControlPoint( 0.7429844097995542, 3.3629726075698803 ), createControlPoint( 0.14859688195991083, 2.3227944338505053 ), createControlPoint( -1.4302449888641426, 1.4159674088426304 ), createControlPoint( -4.532204899777283, 0.580109947818132 ), createControlPoint( -6.1185746102449885, 7.75698912376468 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 5.07086859688196, 6.925682071269487 ), model.controlPointGroup.createNextElement( 2.061781737193762, 0.7625271732714408 ), model.controlPointGroup.createNextElement( 0.09287305122494338, 0.7625271732714408 ), model.controlPointGroup.createNextElement( -3.287706013363029, 3.0472042334050697 ), model.controlPointGroup.createNextElement( -2.2289532293986642, 4.399535077951003 ), model.controlPointGroup.createNextElement( -0.6129621380846331, 4.306662026726059 ), model.controlPointGroup.createNextElement( 0.7429844097995542, 3.3629726075698803 ), model.controlPointGroup.createNextElement( 0.14859688195991083, 2.3227944338505053 ), model.controlPointGroup.createNextElement( -1.4302449888641426, 1.4159674088426304 ), model.controlPointGroup.createNextElement( -4.532204899777283, 0.580109947818132 ), model.controlPointGroup.createNextElement( -6.1185746102449885, 7.75698912376468 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -182,8 +175,8 @@
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0.0363651226158039;
 
-      controlPoints = [ createControlPoint( 7.049477756286265, 5.232410541586074 ), createControlPoint( 1.8198088164974369, 1.7349575399795614 ), createControlPoint( -0.14909986947138165, 1.7349575399795614 ), createControlPoint( 0.5162088974854928, 1.8286581237911035 ), createControlPoint( -0.4516827852998073, 11.657297387984716 ), createControlPoint( 2.0970986460348158, 5.6886320108087025 ), createControlPoint( -1.8000003436635232, 4.708138438138744 ), createControlPoint( -0.43555125725338684, 5.914473403458605 ), createControlPoint( -2.500386847195358, 4.849792552394775 ), createControlPoint( -4.774177820473608, 1.5525403145262526 ), createControlPoint( -6.339690522243714, 8.797478239845262 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 7.049477756286265, 5.232410541586074 ), model.controlPointGroup.createNextElement( 1.8198088164974369, 1.7349575399795614 ), model.controlPointGroup.createNextElement( -0.14909986947138165, 1.7349575399795614 ), model.controlPointGroup.createNextElement( 0.5162088974854928, 1.8286581237911035 ), model.controlPointGroup.createNextElement( -0.4516827852998073, 11.657297387984716 ), model.controlPointGroup.createNextElement( 2.0970986460348158, 5.6886320108087025 ), model.controlPointGroup.createNextElement( -1.8000003436635232, 4.708138438138744 ), model.controlPointGroup.createNextElement( -0.43555125725338684, 5.914473403458605 ), model.controlPointGroup.createNextElement( -2.500386847195358, 4.849792552394775 ), model.controlPointGroup.createNextElement( -4.774177820473608, 1.5525403145262526 ), model.controlPointGroup.createNextElement( -6.339690522243714, 8.797478239845262 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -196,8 +189,8 @@
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0.0363651226158039;
 
-      controlPoints = [ createControlPoint( 0.8301088646967347, 3.5809234059097967 ), createControlPoint( 3.411228615863142, 2.4784350699844477 ), createControlPoint( 5.29194401244168, 5.928575038880248 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 0.8301088646967347, 3.5809234059097967 ), model.controlPointGroup.createNextElement( 3.411228615863142, 2.4784350699844477 ), model.controlPointGroup.createNextElement( 5.29194401244168, 5.928575038880248 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -210,8 +203,8 @@
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0.0363651226158039;
 
-      controlPoints = [ createControlPoint( 0.8301088646967347, 3.5809234059097967 ), createControlPoint( 3.411228615863142, 2.4784350699844477 ), createControlPoint( 5.29194401244168, 5.928575038880248 ) ];
-      track = new Track( model, controlPoints, null, { tandem: trackGroupTandem.createNextTandem() } );
+      controlPoints = [ model.controlPointGroup.createNextElement( 0.8301088646967347, 3.5809234059097967 ), model.controlPointGroup.createNextElement( 3.411228615863142, 2.4784350699844477 ), model.controlPointGroup.createNextElement( 5.29194401244168, 5.928575038880248 ) ];
+      track = model.trackGroup.createNextElement( controlPoints, null );
       track.physicalProperty.value = true;
       model.tracks.add( track );
     }
@@ -223,31 +216,31 @@
       model.frictionProperty.value = 0.05;
 
       const controlPoints1 = [
-        createControlPoint( -6.23, -0.85 ),
-        createControlPoint( -5.23, -0.85 ),
-        createControlPoint( -4.23, -0.85 )
+        model.controlPointGroup.createNextElement( -6.23, -0.85 ),
+        model.controlPointGroup.createNextElement( -5.23, -0.85 ),
+        model.controlPointGroup.createNextElement( -4.23, -0.85 )
       ];
-      const track1 = new Track( model, controlPoints1, null, { tandem: trackGroupTandem.createNextTandem() } );
+      const track1 = model.trackGroup.createNextElement( controlPoints1, null );
       track1.physicalProperty.value = false;
       model.tracks.add( track1 );
 
       const controlPoints2 = [
-        createControlPoint( -6.23, -0.85 ),
-        createControlPoint( -5.23, -0.85 ),
-        createControlPoint( -4.23, -0.85 )
+        model.controlPointGroup.createNextElement( -6.23, -0.85 ),
+        model.controlPointGroup.createNextElement( -5.23, -0.85 ),
+        model.controlPointGroup.createNextElement( -4.23, -0.85 )
       ];
-      const track2 = new Track( model, controlPoints2, null, { tandem: trackGroupTandem.createNextTandem() } );
+      const track2 = model.trackGroup.createNextElement( controlPoints2, null );
       track2.physicalProperty.value = false;
       model.tracks.add( track2 );
 
       const controlPoints3 = [
-        createControlPoint( -0.720977917981072, 1.6368312846731214 ),
-        createControlPoint( 0.279022082018928, 1.6368312846731214 ),
-        createControlPoint( 3.8511345589035137, 7.315696725769607 ),
-        createControlPoint( -1.1916066572392037, 2.911932992494288 ),
-        createControlPoint( -9.170190362232134, 6.469483302512781 )
+        model.controlPointGroup.createNextElement( -0.720977917981072, 1.6368312846731214 ),
+        model.controlPointGroup.createNextElement( 0.279022082018928, 1.6368312846731214 ),
+        model.controlPointGroup.createNextElement( 3.8511345589035137, 7.315696725769607 ),
+        model.controlPointGroup.createNextElement( -1.1916066572392037, 2.911932992494288 ),
+        model.controlPointGroup.createNextElement( -9.170190362232134, 6.469483302512781 )
       ];
-      const track3 = new Track( model, controlPoints3, null, { tandem: trackGroupTandem.createNextTandem() } );
+      const track3 = model.trackGroup.createNextElement( controlPoints3, null );
       track3.physicalProperty.value = true;
       model.tracks.add( track3 );
     }
@@ -258,13 +251,13 @@
       model.skater.positionProperty.set( new Vector2( -6.698445595854922, 6.5278756476683935 ) );
       model.skater.released( null, 0 );
       model.frictionProperty.value = 0;
-      const track15 = new Track( model, [
-        createControlPoint( 0.9873551637279601, 7.856892317380353 ),
-        createControlPoint( -0.4621662468513845, 5.9031895465994975 ),
-        createControlPoint( -3.0250881612090676, 5.735129093198994 ),
-        createControlPoint( -4.705692695214106, 0.9454061712846356 ),
-        createControlPoint( -7.310629722921914, 7.457748740554157 )
-      ], null, { tandem: trackGroupTandem.createNextTandem() } );
+      const track15 = model.trackGroup.createNextElement( [
+        model.controlPointGroup.createNextElement( 0.9873551637279601, 7.856892317380353 ),
+        model.controlPointGroup.createNextElement( -0.4621662468513845, 5.9031895465994975 ),
+        model.controlPointGroup.createNextElement( -3.0250881612090676, 5.735129093198994 ),
+        model.controlPointGroup.createNextElement( -4.705692695214106, 0.9454061712846356 ),
+        model.controlPointGroup.createNextElement( -7.310629722921914, 7.457748740554157 )
+      ], null );
       track15.physicalProperty.value = true;
       model.tracks.add( track15 );
     }
@@ -273,20 +266,19 @@
       model.frictionProperty.value = 0;
 
       const controlPoints = [
-        createControlPoint( -0.29564715581203593, 5.349320898598515 ),
-        createControlPoint( 0.5844187963726313, 5.266814715581202 ),
-        createControlPoint( 1.5469909315746087, 4.771777617477328 ),
-        createControlPoint( 2.0145259686727126, 3.1629070486397355 ),
-        createControlPoint( 1.2307172300082456, 1.7878039983511949 ),
-        createControlPoint( -1.2994723825226702, 1.7740529678483092 ),
-        createControlPoint( -2.1382852431986805, 3.575437963726298 ),
-        createControlPoint( -1.0382028029678487, 5.280565746084088 ),
-        createControlPoint( 0.6944270403957145, 6.806930131904369 ),
-        createControlPoint( -0.9831986809563062, 7.962016694146743 )
+        model.controlPointGroup.createNextElement( -0.29564715581203593, 5.349320898598515 ),
+        model.controlPointGroup.createNextElement( 0.5844187963726313, 5.266814715581202 ),
+        model.controlPointGroup.createNextElement( 1.5469909315746087, 4.771777617477328 ),
+        model.controlPointGroup.createNextElement( 2.0145259686727126, 3.1629070486397355 ),
+        model.controlPointGroup.createNextElement( 1.2307172300082456, 1.7878039983511949 ),
+        model.controlPointGroup.createNextElement( -1.2994723825226702, 1.7740529678483092 ),
+        model.controlPointGroup.createNextElement( -2.1382852431986805, 3.575437963726298 ),
+        model.controlPointGroup.createNextElement( -1.0382028029678487, 5.280565746084088 ),
+        model.controlPointGroup.createNextElement( 0.6944270403957145, 6.806930131904369 ),
+        model.controlPointGroup.createNextElement( -0.9831986809563062, 7.962016694146743 )
       ];
 
-      const track16 = new Track( model, controlPoints, null, {
-        tandem: trackGroupTandem.createNextTandem(),
+      const track16 = model.trackGroup.createNextElement( controlPoints, null, {
         configurable: true
       } );
       track16.physicalProperty.value = true;
Index: js/graphs/model/GraphsModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/graphs/model/GraphsModel.js b/js/graphs/model/GraphsModel.js
--- a/js/graphs/model/GraphsModel.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/graphs/model/GraphsModel.js	(date 1611443745590)
@@ -308,13 +308,12 @@
    * @returns {Track[]}
    */
   createGraphsTrackSet( tandem ) {
-    const groupTandem = this.controlPointGroupTandem;
 
     // all tracks in graphs screen are bound by these dimensions (in meters)
     const trackHeight = GraphsConstants.TRACK_HEIGHT;
     const trackWidth = GraphsConstants.TRACK_WIDTH;
 
-    const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( groupTandem, {
+    const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( this, {
       trackHeight: trackHeight,
       trackWidth: trackWidth,
       p1Visible: false,
@@ -327,7 +326,7 @@
       phetioState: false
     } );
 
-    const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( groupTandem, {
+    const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( this, {
       trackHeight: 4,
       trackWidth: 10,
       trackMidHeight: 1.5,
Index: js/common/view/TrackNode.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/TrackNode.js b/js/common/view/TrackNode.js
--- a/js/common/view/TrackNode.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/view/TrackNode.js	(date 1611443421787)
@@ -26,14 +26,14 @@
 class TrackNode extends Node {
 
   /**
-   * @param {EnergySkateParkModel} model the entire model.
    * @param {Track} track the track for this track node
    * @param {ModelViewTransform2} modelViewTransform the model view transform for the view
    * @param {Property.<Bounds2>} availableBoundsProperty
    * @param {Tandem} tandem
    * @param {Object} [options]
    */
-  constructor( model, track, modelViewTransform, availableBoundsProperty, tandem, options ) {
+  constructor( track, modelViewTransform, availableBoundsProperty, tandem, options ) {
+    const model = track.model;
 
     options = merge( {
 
Index: js/common/view/EnergySkateParkTrackSetScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/EnergySkateParkTrackSetScreenView.js b/js/common/view/EnergySkateParkTrackSetScreenView.js
--- a/js/common/view/EnergySkateParkTrackSetScreenView.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/view/EnergySkateParkTrackSetScreenView.js	(date 1611443745615)
@@ -23,7 +23,7 @@
     super( model, tandem, options );
 
     const trackNodes = model.tracks.map( track => {
-      return new TrackNode( model, track, this.modelViewTransform, this.availableModelBoundsProperty, this.trackNodeGroupTandem.createNextTandem() );
+      return this.trackNodeGroup.createNextElement( track, this.modelViewTransform, this.availableModelBoundsProperty);
     } );
 
     trackNodes.forEach( trackNode => {
Index: js/common/view/TrackToolboxPanel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/TrackToolboxPanel.js b/js/common/view/TrackToolboxPanel.js
--- a/js/common/view/TrackToolboxPanel.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/view/TrackToolboxPanel.js	(date 1611443953966)
@@ -35,7 +35,7 @@
         interactive: false
       }
     } );
-    const iconNode = new TrackNode( model, iconTrack, view.modelViewTransform, model.availableModelBoundsProperty, tandem.createTandem( 'iconNode' ), {
+    const iconNode = new TrackNode( iconTrack, view.modelViewTransform, model.availableModelBoundsProperty, tandem.createTandem( 'iconNode' ), {
 
       // want the icon to look pickable, even though it isn't really draggable (forwarding listener makes the new
       // TrackNode draggable)
Index: js/common/view/SceneSelectionRadioButtonGroup.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/SceneSelectionRadioButtonGroup.js b/js/common/view/SceneSelectionRadioButtonGroup.js
--- a/js/common/view/SceneSelectionRadioButtonGroup.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/view/SceneSelectionRadioButtonGroup.js	(date 1611443919879)
@@ -69,7 +69,7 @@
 
       let track = null;
       if ( trackType === PremadeTracks.TrackType.PARABOLA ) {
-        const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( model.controlPointGroupTandem, merge( {
+        const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( model, merge( {
           trackMidHeight: 1
         }, controlPointOptions ) );
         track = EnergySkateParkTrackSetModel.createPremadeTrack( model, parabolaControlPoints, {
@@ -77,19 +77,19 @@
         } );
       }
       else if ( trackType === PremadeTracks.TrackType.SLOPE ) {
-        const slopeControlPoints = PremadeTracks.createSlopeControlPoints( model.controlPointGroupTandem, controlPointOptions );
+        const slopeControlPoints = PremadeTracks.createSlopeControlPoints( model, controlPointOptions );
         track = EnergySkateParkTrackSetModel.createPremadeTrack( model, slopeControlPoints, {
           tandem: tandem.createTandem( 'slopeTrackIcon' )
         } );
       }
       else if ( trackType === PremadeTracks.TrackType.DOUBLE_WELL ) {
-        const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( model.controlPointGroupTandem, controlPointOptions);
+        const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( model, controlPointOptions);
         track = EnergySkateParkTrackSetModel.createPremadeTrack( model, doubleWellControlPoints, {
           tandem: tandem.createTandem( 'doubleWellTrackIcon' )
         } );
       }
       else if ( trackType === PremadeTracks.TrackType.LOOP ) {
-        const loopControlPoints = PremadeTracks.createLoopControlPoints( model.controlPointGroupTandem, merge( {
+        const loopControlPoints = PremadeTracks.createLoopControlPoints( model, merge( {
           innerLoopWidth: 2.5,
           innerLoopTop: 3.5
         }, controlPointOptions ) );
@@ -114,9 +114,8 @@
         children.push( background );
       }
 
-      // const track = model.tracks.get( index );
       const track = createIconTrack( model.trackTypes[ index ] );
-      const trackNode = new TrackNode( model, track, view.modelViewTransform, new Property(), tandem.createTandem( 'trackNode' + index ), {
+      const trackNode = new TrackNode( track, view.modelViewTransform, new Property(), tandem.createTandem( 'trackNode' + index ), {
         isIcon: true
       } );
 
Index: js/common/view/EnergySkateParkScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/EnergySkateParkScreenView.js b/js/common/view/EnergySkateParkScreenView.js
--- a/js/common/view/EnergySkateParkScreenView.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/view/EnergySkateParkScreenView.js	(date 1611444753146)
@@ -29,10 +29,12 @@
 import Path from '../../../../scenery/js/nodes/Path.js';
 import Text from '../../../../scenery/js/nodes/Text.js';
 import RectangularPushButton from '../../../../sun/js/buttons/RectangularPushButton.js';
+import PhetioGroup from '../../../../tandem/js/PhetioGroup.js';
 import skaterIconImage from '../../../images/skater1_left_png.js';
 import energySkatePark from '../../energySkatePark.js';
 import energySkateParkStrings from '../../energySkateParkStrings.js';
 import EnergySkateParkConstants from '../EnergySkateParkConstants.js';
+import Track from '../model/Track.js';
 import AttachDetachToggleButtons from './AttachDetachToggleButtons.js';
 import BackgroundNode from './BackgroundNode.js';
 import EnergyBarGraphAccordionBox from './EnergyBarGraphAccordionBox.js';
@@ -44,6 +46,7 @@
 import ReferenceHeightLine from './ReferenceHeightLine.js';
 import SkaterNode from './SkaterNode.js';
 import ToolboxPanel from './ToolboxPanel.js';
+import TrackNode from './TrackNode.js';
 import VisibilityControlsPanel from './VisibilityControlsPanel.js';
 
 const controlsRestartSkaterString = energySkateParkStrings.skaterControls.restartSkater;
@@ -109,8 +112,37 @@
       tandem: tandem
     } );
 
-    // @protected
-    this.trackNodeGroupTandem = tandem.createGroupTandem( 'trackNode' );
+    const modelPoint = new Vector2( 0, 0 );
+
+    // earth is 86px high in stage coordinates
+    const viewPoint = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height - BackgroundNode.earthHeight );
+
+    // scale chosen so that displayed model is the same as it was for energy-skate-park-basics when that sim
+    // used non-default layout bounds
+    const scale = 61.40;
+    const modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( modelPoint, viewPoint, scale );
+    this.modelViewTransform = modelViewTransform;
+
+    this.availableModelBoundsProperty = new Property( new Bounds2( 0, 0, 0, 0 ), {
+      valueType: [ Bounds2 ]
+    } );
+    this.availableModelBoundsProperty.link( bounds => {
+      model.availableModelBoundsProperty.set( bounds );
+    } );
+
+    // @public {PhetioGroup.<Track>} group of TrackNodes
+    this.trackNodeGroup = new PhetioGroup( ( tandem, track, modelViewTransform, availableBoundsProperty, options ) => {
+      assert && options && assert( !options.hasOwnProperty( 'tandem' ), 'tandem is managed by the PhetioGroup' );
+      return new TrackNode( track, modelViewTransform, availableBoundsProperty, tandem, options );
+    }, [ model.trackGroup.archetype, modelViewTransform, this.availableModelBoundsProperty, {} ], {
+      tandem: tandem.createTandem( 'trackNodes' ),
+      phetioType: PhetioGroup.PhetioGroupIO( Node.NodeIO ),
+      phetioDynamicElementName: 'trackNode',
+
+      // These elements are not created by the PhET-IO state engine, they can just listen to the model for supporting
+      // state in the same way they do for sim logic.
+      supportsDynamicState: false
+    } );
 
     // @protected
     this.model = model;
@@ -136,24 +168,6 @@
     this.topLayer = new Node();
     this.children = [ this.bottomLayer, this.topLayer ];
 
-    const modelPoint = new Vector2( 0, 0 );
-
-    // earth is 86px high in stage coordinates
-    const viewPoint = new Vector2( this.layoutBounds.width / 2, this.layoutBounds.height - BackgroundNode.earthHeight );
-
-    // scale chosen so that displayed model is the same as it was for energy-skate-park-basics when that sim
-    // used non-default layout bounds
-    const scale = 61.40;
-    const modelViewTransform = ModelViewTransform2.createSinglePointScaleInvertedYMapping( modelPoint, viewPoint, scale );
-    this.modelViewTransform = modelViewTransform;
-
-    this.availableModelBoundsProperty = new Property( new Bounds2( 0, 0, 0, 0 ), {
-      valueType: [ Bounds2 ]
-    } );
-    this.availableModelBoundsProperty.link( bounds => {
-      model.availableModelBoundsProperty.set( bounds );
-    } );
-
     // @protected (read-only)
     this.skaterNode = new SkaterNode(
       model.skater,
Index: js/playground/view/EnergySkateParkPlaygroundScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/playground/view/EnergySkateParkPlaygroundScreenView.js b/js/playground/view/EnergySkateParkPlaygroundScreenView.js
--- a/js/playground/view/EnergySkateParkPlaygroundScreenView.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/playground/view/EnergySkateParkPlaygroundScreenView.js	(date 1611443758669)
@@ -84,7 +84,7 @@
    * @returns {TrackNode}
    */
   addTrackNode( track ) {
-    const trackNode = new TrackNode( this.model, track, this.modelViewTransform, this.availableModelBoundsProperty, this.trackNodeGroupTandem.createTandem( track.tandem.name ) );
+    const trackNode = this.trackNodeGroup.createNextElement( track, this.modelViewTransform, this.availableModelBoundsProperty );
     this.trackNodes.push( trackNode );
     this.trackLayer.addChild( trackNode );
 
Index: js/playground/model/EnergySkateParkPlaygroundModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/playground/model/EnergySkateParkPlaygroundModel.js b/js/playground/model/EnergySkateParkPlaygroundModel.js
--- a/js/playground/model/EnergySkateParkPlaygroundModel.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/playground/model/EnergySkateParkPlaygroundModel.js	(date 1611443745578)
@@ -10,7 +10,6 @@
 
 import merge from '../../../../phet-core/js/merge.js';
 import energySkatePark from '../../energySkatePark.js';
-import ControlPoint from '../../common/model/ControlPoint.js';
 import EnergySkateParkModel from '../../common/model/EnergySkateParkModel.js';
 import Track from '../../common/model/Track.js';
 
@@ -52,18 +51,17 @@
       trackOptions: null
     }, options );
 
-    const controlPointGroupTandem = this.controlPointGroupTandem;
-    const trackGroupTandem = this.trackGroupTandem;
-
     const controlPoints = [
-      new ControlPoint( -1, 0, merge( { tandem: controlPointGroupTandem.createNextTandem() }, options.controlPointOptions ) ),
-      new ControlPoint( 0, 0, merge( { tandem: controlPointGroupTandem.createNextTandem() }, options.controlPointOptions ) ),
-      new ControlPoint( 1, 0, merge( { tandem: controlPointGroupTandem.createNextTandem() }, options.controlPointOptions ) )
+      this.controlPointGroup.createNextElement( -1, 0, options.controlPointOptions ),
+      this.controlPointGroup.createNextElement( 0, 0, options.controlPointOptions ),
+      this.controlPointGroup.createNextElement( 1, 0, options.controlPointOptions )
     ];
 
-    return new Track( this, controlPoints, null, merge( {
-        tandem: trackGroupTandem.createNextTandem()
-      }, Track.FULLY_INTERACTIVE_OPTIONS, options.trackOptions )
+    return this.trackGroup.createNextElement( controlPoints, null, merge(
+      {},
+      Track.FULLY_INTERACTIVE_OPTIONS,
+      options.trackOptions
+      )
     );
   }
 
Index: js/common/model/EnergySkateParkTrackSetModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/EnergySkateParkTrackSetModel.js b/js/common/model/EnergySkateParkTrackSetModel.js
--- a/js/common/model/EnergySkateParkTrackSetModel.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/EnergySkateParkTrackSetModel.js	(date 1611442362804)
@@ -118,7 +118,7 @@
 
     this.trackTypes.forEach( trackType => {
       if ( trackType === PremadeTracks.TrackType.PARABOLA ) {
-        const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( this.controlPointGroupTandem, options.parabolaControlPointOptions );
+        const parabolaControlPoints = PremadeTracks.createParabolaControlPoints( this,options.parabolaControlPointOptions );
         const parabolaTrack = EnergySkateParkTrackSetModel.createPremadeTrack( this, parabolaControlPoints, merge( {
           tandem: tandem.createTandem( 'parabolaTrack' )
         }, options.parabolaTrackOptions ) );
@@ -126,7 +126,7 @@
         tracks.push( parabolaTrack );
       }
       else if ( trackType === PremadeTracks.TrackType.SLOPE ) {
-        const slopeControlPoints = PremadeTracks.createSlopeControlPoints( this.controlPointGroupTandem, options.slopeControlPointOptions );
+        const slopeControlPoints = PremadeTracks.createSlopeControlPoints( this, options.slopeControlPointOptions );
         const slopeTrack = EnergySkateParkTrackSetModel.createPremadeTrack( this, slopeControlPoints, merge( {
 
           // Flag to indicate whether the skater transitions from the right edge of this track directly to the ground
@@ -137,14 +137,14 @@
         tracks.push( slopeTrack );
       }
       else if ( trackType === PremadeTracks.TrackType.DOUBLE_WELL ) {
-        const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( this.controlPointGroupTandem, options.doubleWellControlPointOptions );
+        const doubleWellControlPoints = PremadeTracks.createDoubleWellControlPoints( this, options.doubleWellControlPointOptions );
         const doubleWellTrack = EnergySkateParkTrackSetModel.createPremadeTrack( this, doubleWellControlPoints, merge( {
           tandem: tandem.createTandem( 'doubleWellTrack' )
         }, options.doubleWellTrackOptions ) );
         tracks.push( doubleWellTrack );
       }
       else if ( trackType === PremadeTracks.TrackType.LOOP ) {
-        const loopControlPoints = PremadeTracks.createLoopControlPoints( this.controlPointGroupTandem, options.loopControlPointOptions );
+        const loopControlPoints = PremadeTracks.createLoopControlPoints( this, options.loopControlPointOptions );
         const loopTrack = EnergySkateParkTrackSetModel.createPremadeTrack( this, loopControlPoints, merge( {
           draggable: this.tracksDraggable,
           tandem: tandem.createTandem( 'loopTrack' )
Index: js/common/model/EnergySkateParkModel.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/EnergySkateParkModel.js b/js/common/model/EnergySkateParkModel.js
--- a/js/common/model/EnergySkateParkModel.js	(revision 8b95f1ed3aa26c30f73757afe9d56d2d75f3933f)
+++ b/js/common/model/EnergySkateParkModel.js	(date 1611445427334)
@@ -39,6 +39,7 @@
 import merge from '../../../../phet-core/js/merge.js';
 import Stopwatch from '../../../../scenery-phet/js/Stopwatch.js';
 import TimeSpeed from '../../../../scenery-phet/js/TimeSpeed.js';
+import PhetioGroup from '../../../../tandem/js/PhetioGroup.js';
 import PhetioObject from '../../../../tandem/js/PhetioObject.js';
 import Tandem from '../../../../tandem/js/Tandem.js';
 import IOType from '../../../../tandem/js/types/IOType.js';
@@ -106,19 +107,41 @@
       skaterOptions: null
     }, options );
 
+    // @public - emits an event whenever a track changes in some way (control points dragged, track split apart,
+    // track dragged, track deleted or scene changed, etc...)
+    this.trackChangedEmitter = new Emitter();
+
     // @public (read-only)
     this.tracksDraggable = options.tracksDraggable;
     this.tracksConfigurable = options.tracksConfigurable;
     this.defaultFriction = options.defaultFriction;
 
-    const controlPointGroupTandem = tandem.createGroupTandem( 'controlPoint' );
-    const trackGroupTandem = tandem.createGroupTandem( 'track' );
+    // @public - Will be filled in by the view, used to prevent control points from moving outside the visible model
+    // bounds when adjusted, see #195
+    this.availableModelBoundsProperty = new Property( new Bounds2( 0, 0, 0, 0 ), {
+      tandem: tandem.createTandem( 'availableModelBoundsProperty' ),
+      phetioType: Property.PropertyIO( Bounds2.Bounds2IO )
+    } );
 
-    // @protected
-    this.controlPointGroupTandem = controlPointGroupTandem;
+    // @public {PhetioGroup.<ControlPoint>} group of control points
+    this.controlPointGroup = new PhetioGroup( ( tandem, x, y, options ) => {
+      assert && options && assert( !options.hasOwnProperty( 'tandem' ), 'tandem is managed by the PhetioGroup' );
+      return new ControlPoint( x, y, merge( {}, options, { tandem: tandem } ) );
+    }, [ 0, 0, {} ], {
+      tandem: tandem.createTandem( 'controlPointGroup' ),
+      phetioType: PhetioGroup.PhetioGroupIO( ControlPoint.ControlPointIO ),
+      phetioDynamicElementName: 'controlPoint'
+    } );
 
-    // @public
-    this.trackGroupTandem = trackGroupTandem;
+    // @public {PhetioGroup.<Track>} group of tracks
+    this.trackGroup = new PhetioGroup( ( tandem, controlPoints, parents, options ) => {
+      assert && options && assert( !options.hasOwnProperty( 'tandem' ), 'tandem is managed by the PhetioGroup' );
+      return new Track( this, controlPoints, parents, merge( {}, options, { tandem: tandem } ) );
+    }, [ [ this.controlPointGroup.createNextElement( 0, 0 ), this.controlPointGroup.createNextElement( 100, 0 ) ], [], {} ], {
+      tandem: tandem.createTandem( 'trackGroup' ),
+      phetioType: PhetioGroup.PhetioGroupIO( Track.TrackIO ),
+      phetioDynamicElementName: 'track'
+    } );
 
     // {boolean} - Temporary flag that keeps track of whether the track was changed in the step before the physics
     // update. True if the skater's track is being dragged by the user, so that energy conservation no longer applies.
@@ -206,13 +229,6 @@
       tandem: tandem.createTandem( 'stickingToTrackProperty' )
     } );
 
-    // @public - Will be filled in by the view, used to prevent control points from moving outside the visible model
-    // bounds when adjusted, see #195
-    this.availableModelBoundsProperty = new Property( new Bounds2( 0, 0, 0, 0 ), {
-      tandem: tandem.createTandem( 'availableModelBoundsProperty' ),
-      phetioType: Property.PropertyIO( Bounds2.Bounds2IO )
-    } );
-
     // @public {UserControlledPropertySet} - collection of Properties that indicate that a user is
     // modifying some variable that will change physical system and modify all saved energy data
     this.userControlledPropertySet = new UserControlledPropertySet();
@@ -259,10 +275,6 @@
       track.dispose();
     } );
 
-    // @public - emits an event whenever a track changes in some way (control points dragged, track split apart,
-    // track dragged, track deleted or scene changed, etc...)
-    this.trackChangedEmitter = new Emitter();
-
     // Determine when to show/hide the track edit buttons (cut track or delete control point)
     const updateTrackEditingButtonProperties = () => {
       let editEnabled = false;
@@ -290,7 +302,7 @@
     this.eventTimer = new EventTimer( new EventTimer.ConstantEventModel( FRAME_RATE ), this.constantStep.bind( this ) );
 
     if ( EnergySkateParkQueryParameters.testTrackIndex > 0 ) {
-      DebugTracks.init( this, tandem.createGroupTandem( 'debugTrackControlPoint' ), tandem.createGroupTandem( 'track' ) );
+      DebugTracks.init( this );
     }
   }
 
@@ -1544,15 +1556,12 @@
 
     track.removeEmitter.emit();
     this.tracks.remove( track );
-    const trackGroupTandem = this.trackGroupTandem;
 
     if ( track.controlPoints.length > 2 ) {
       const controlPointToDelete = track.controlPoints[ controlPointIndex ];
       const points = _.without( track.controlPoints, controlPointToDelete );
       controlPointToDelete.dispose();
-      const newTrack = new Track( this, points, track.getParentsOrSelf(), merge( {
-        tandem: trackGroupTandem.createNextTandem()
-      }, Track.FULLY_INTERACTIVE_OPTIONS ) );
+      const newTrack = this.trackGroup.createNextElement( points, track.getParentsOrSelf(), Track.FULLY_INTERACTIVE_OPTIONS );
       newTrack.physicalProperty.value = true;
       newTrack.droppedProperty.value = true;
 
@@ -1596,18 +1605,14 @@
     assert && assert( track.splittable, 'trying to split a track that is not splittable!' );
     const controlPointToSplit = track.controlPoints[ controlPointIndex ];
 
-    const trackGroupTandem = this.trackGroupTandem;
-
     const vector = Vector2.createPolar( 0.5, modelAngle );
-    const newPoint1 = new ControlPoint(
+    const newPoint1 = this.controlPointGroup.createNextElement(
       track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.x - vector.x,
-      track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.y - vector.y,
-      { tandem: this.controlPointGroupTandem.createNextTandem() }
+      track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.y - vector.y
     );
-    const newPoint2 = new ControlPoint(
+    const newPoint2 = this.controlPointGroup.createNextElement(
       track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.x + vector.x,
-      track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.y + vector.y,
-      { tandem: this.controlPointGroupTandem.createNextTandem() }
+      track.controlPoints[ controlPointIndex ].sourcePositionProperty.value.y + vector.y
     );
 
     const points1 = track.controlPoints.slice( 0, controlPointIndex );
@@ -1616,14 +1621,10 @@
     points1.push( newPoint1 );
     points2.unshift( newPoint2 );
 
-    const newTrack1 = new Track( this, points1, track.getParentsOrSelf(), merge( {
-      tandem: trackGroupTandem.createNextTandem()
-    }, Track.FULLY_INTERACTIVE_OPTIONS ) );
+    const newTrack1 = this.trackGroup.createNextElement( points1, track.getParentsOrSelf(), Track.FULLY_INTERACTIVE_OPTIONS );
     newTrack1.physicalProperty.value = true;
     newTrack1.droppedProperty.value = true;
-    const newTrack2 = new Track( this, points2, track.getParentsOrSelf(), merge( {
-      tandem: trackGroupTandem.createNextTandem()
-    }, Track.FULLY_INTERACTIVE_OPTIONS ) );
+    const newTrack2 = this.trackGroup.createNextElement( points2, track.getParentsOrSelf(), Track.FULLY_INTERACTIVE_OPTIONS );
     newTrack2.physicalProperty.value = true;
     newTrack2.droppedProperty.value = true;
 
@@ -1669,27 +1670,25 @@
   joinTrackToTrack( a, b ) {
     const points = [];
     let i;
-    const controlPointGroupTandem = this.controlPointGroupTandem;
-    const trackGroupTandem = this.trackGroupTandem;
 
     const firstTrackForward = () => {
       for ( i = 0; i < a.controlPoints.length; i++ ) {
-        points.push( a.controlPoints[ i ].copy( controlPointGroupTandem.createNextTandem() ) );
+        points.push( a.controlPoints[ i ].copy( this ) );
       }
     };
     const firstTrackBackward = () => {
       for ( i = a.controlPoints.length - 1; i >= 0; i-- ) {
-        points.push( a.controlPoints[ i ].copy( controlPointGroupTandem.createNextTandem() ) );
+        points.push( a.controlPoints[ i ].copy( this ) );
       }
     };
     const secondTrackForward = () => {
       for ( i = 1; i < b.controlPoints.length; i++ ) {
-        points.push( b.controlPoints[ i ].copy( controlPointGroupTandem.createNextTandem() ) );
+        points.push( b.controlPoints[ i ].copy( this ) );
       }
     };
     const secondTrackBackward = () => {
       for ( i = b.controlPoints.length - 2; i >= 0; i-- ) {
-        points.push( b.controlPoints[ i ].copy( controlPointGroupTandem.createNextTandem() ) );
+        points.push( b.controlPoints[ i ].copy( this ) );
       }
     };
 
@@ -1718,21 +1717,19 @@
       secondTrackBackward();
     }
 
-    const newTrack = new Track( this, points, a.getParentsOrSelf().concat( b.getParentsOrSelf() ), merge( {
-      tandem: trackGroupTandem.createNextTandem()
-    }, Track.FULLY_INTERACTIVE_OPTIONS ) );
+    const newTrack = this.trackGroup.createNextElement( points, a.getParentsOrSelf().concat( b.getParentsOrSelf() ), Track.FULLY_INTERACTIVE_OPTIONS );
     newTrack.physicalProperty.value = true;
     newTrack.droppedProperty.value = true;
 
     a.disposeControlPoints();
     a.removeEmitter.emit();
-    if (this.tracks.includes(a)) {
+    if ( this.tracks.includes( a ) ) {
       this.tracks.remove( a );
     }
 
     b.disposeControlPoints();
     b.removeEmitter.emit();
-    if (this.tracks.includes(b)) {
+    if ( this.tracks.includes( b ) ) {
       this.tracks.remove( b );
     }
 
@@ -1866,15 +1863,15 @@
    * @param {Tandem} tandem
    * @param {boolean} draggable
    * @param {boolean} configurable
-   * @param [number[]} controlPointTandemIDs
+   * @param {number[]} controlPointTandemIDs
    */
   addTrack( tandem, draggable, configurable, controlPointTandemIDs ) {
 
     assert && assert( controlPointTandemIDs, 'controlPointTandemIDs should exist' );
     const controlPoints = controlPointTandemIDs.map( ( id, index ) => {
-      return new ControlPoint( index, 0, { tandem: Tandem.createFromPhetioID( id ) } ); // TODO: create with correct initial x & y values.
+      return this.controlPointGroup.createNextElement( index, 0, { tandem: Tandem.createFromPhetioID( id ) } ); // TODO: create with correct initial x & y values.
     } );
-    const newTrack = new Track( this, controlPoints, [], {
+    const newTrack = this.trackGroup.createNextElement( controlPoints, [], {
       draggable: draggable,
       configurable: configurable,
       tandem: tandem

@samreid
Copy link
Member

samreid commented Jan 24, 2021

I'll continue on Energy Skate Park in phetsims/energy-skate-park#123. But it revealed another deprecated method that should be eliminated as part of this issue: createFromPhetioID.

samreid added a commit to phetsims/energy-skate-park that referenced this issue Jan 24, 2021
samreid added a commit to phetsims/color-vision that referenced this issue Jan 24, 2021
samreid added a commit that referenced this issue Jan 24, 2021
@samreid
Copy link
Member

samreid commented Jan 24, 2021

Summarizing work done in this issue:

  • Forbid '~' in Tandem names, and remove special handling for it.
  • Remove deprecated method createFromPhetioID
  • Remove clearChildInstances and addChildElementDeprecated
  • Addressed and eliminated all TODOs that referenced this issue
  • For the sims listed in the issue description, many parts just needed to be uninstrumented. Other cases used static indices.
  • Undeprecate GroupTandem for cases where indexed PhetioObjects are created statically. It is typically used like:
const plusChargesGroupTandem = tandem.createTandem('plusCharges').createGroupTandem( 'plusCharge' );
...
plusChargesGroupTandem.createNextTandem();

@zepumph can you please review?

@samreid samreid removed their assignment Jan 24, 2021
@zepumph
Copy link
Member

zepumph commented Jan 28, 2021

This is some really great stuff. Thanks for taking the lead here. It's great to see this old pattern gone!

For the sims listed in the issue description, many parts just needed to be uninstrumented. Other cases used static indices.

For color vision I saw that you just uninstrumented but didn't replace. I noticed phetsims/color-vision#134, so I made a note there. Perhaps there are more like that but I didn't see.

I can't think of anything else here. Great work.

@zepumph zepumph assigned samreid and unassigned zepumph Jan 28, 2021
@samreid
Copy link
Member

samreid commented Feb 2, 2021

For color vision I saw that you just uninstrumented but didn't replace.

Yes, as described in #87 (comment):

For the sims listed in the issue description, many parts just needed to be uninstrumented. Other cases used static indices.

I also added a comment about color vision: phetsims/color-vision#134

It was unclear that the photons need to be instrumented, we can decide when we revisit this simulation for phet-io.

Thanks, closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants