Пример #1
0
define(function (require, exports, module) {

    'use strict';

    var _ = require('underscore');

    var FixedIntervalSimulation = require('common/simulation/fixed-interval-simulation');

    /**
     * Base simulation model for quantum physics simulations
     */
    var QuantumSimulation = FixedIntervalSimulation.extend({

        defaults: _.extend(FixedIntervalSimulation.prototype.defaults, {
            photonSpeedScale: 1,
            elementProperties: undefined
        }),

        getGroundState: function() {
            return this.get('elementProperties').getGroundState();
        },

        getCurrentElementProperties: function() {
            return this.get('elementProperties');
        },

        setCurrentElementProperties: function(elementProperties) {
            this.set('elementProperties', elementProperties);
        }

    });

    return QuantumSimulation;
});
Пример #2
0
define(function (require, exports, module) {

    'use strict';

    var _        = require('underscore');
    var Backbone = require('backbone');

    var FixedIntervalSimulation = require('common/simulation/fixed-interval-simulation');
    var Vector2    = require('common/math/vector2');
    var Rectangle  = require('common/math/rectangle');

    var Body            = require('models/body');
    var BodyStateRecord = require('models/body-state-record');

    var Constants = require('constants');
    var Scenarios = require('scenarios');

     /* PhET explanation: "
      *    Subdivide DT intervals by this factor to improve smoothing, 
      *    otherwise some orbits look too non-smooth (you can see 
      *    their corners). "
      */
    var SMOOTHING_STEPS = 5;

    /**
     * 
     */
    var GOSimulation = FixedIntervalSimulation.extend({

        defaults: _.extend(FixedIntervalSimulation.prototype.defaults, {
            scenario: Scenarios.Friendly[0],
            gravityEnabled: true,
            secondCounter: 0,
            speedScale: Constants.DEFAULT_SPEED_SCALE, 
            deltaTimePerStep: Constants.DT_PER_TICK
        }),
        
        /**
         *
         */
        initialize: function(attributes, options) {
            options = _.extend({
                framesPerSecond: Constants.FRAME_RATE
            }, options);

            this.bodies = new Backbone.Collection([], {
                model: Body
            });

            this.bounds = new Rectangle();

            this._sum = new Vector2();
            this._pos = new Vector2();
            this._vel = new Vector2();
            this._acc = new Vector2();
            this._force = new Vector2();
            this._sourceForce = new Vector2();
            this._nextVelocityHalf = new Vector2();

            FixedIntervalSimulation.prototype.initialize.apply(this, [attributes, options]);

            this.on('change:scenario', this.scenarioChanged);

            this.scenarioChanged(this, this.get('scenario'));
        },
        
        /**
         * Loads a scenario. Sets up the bodies, applies simulation
         *   attributes, and resets the simulation.
         */
        scenarioChanged: function(simulation, scenario) {
            this.pause();
            
            this.bodies.reset(_.map(scenario.bodies, function(body) {
                return body.clone();
            }));

            this.initSavedState();
            this.initScratchStates();
            this.resetScenario();
            this.set(scenario.simulationAttributes);
            this.updateForceVectors();
        },

        /**
         * Reset only the things that need to be reset when
         *   switching scenarios.
         */
        resetScenario: function() {
            this.time = 0;
            this.set({
                secondCounter: 0
            });
        },

        /**
         * Creates an array of individual body state records
         *   under the property "savedState" to use when saving
         *   and applying states later on a rewind.
         */
        initSavedState: function() {
            this.savedState = [];
            for (var j = 0; j < this.bodies.length; j++)
                this.savedState.push(new BodyStateRecord());
        },

        /**
         * Creates scratch states that are arrays of individual
         *   body states to be written to during the execution
         *   of a step (which has many sub-steps before a final
         *   state is reached).
         */
        initScratchStates: function() {
            this.scratchStates = [];

            // Creates two states, which are arrays of BodyStateRecord
            //   instances that will represent each body in the system.
            for (var i = 0; i < 2; i++) {
                var state = [];
                for (var j = 0; j < this.bodies.length; j++)
                    state.push(new BodyStateRecord());
                this.scratchStates.push(state);
            }

            // Used to determine which scratch state will be used next
            this.currentScratchStateIndex = 0;
        },

        /**
         * Returns the next scratch state. Note that this assumes
         *   we will ever only need two at a time.
         */
        getScratchState: function() {
            this.currentScratchStateIndex++;
            if (this.currentScratchStateIndex >= this.scratchStates.length)
                this.currentScratchStateIndex = 0;
            return this.scratchStates[this.currentScratchStateIndex];
        },

        /**
         *
         */
        reset: function() {
            this.scenarioChanged(this, this.get('scenario'));
        },

        /**
         *
         */
        play: function() {
            // Save the current state to apply later with the rewind button
            for (var i = 0; i < this.savedState.length; i++)
                this.savedState[i].saveState(this.bodies.at(i));

            FixedIntervalSimulation.prototype.play.apply(this);
        },

        /**
         *
         */
        rewind: function() {
            // Apply the saved state
            for (var i = 0; i < this.savedState.length; i++)
                this.savedState[i].applyState(this.bodies.at(i));
        },

        /**
         *
         */
        clearSecondCounter: function() {
            this.set('secondCounter', 0);
        },

        /**
         * Updates initial force vectors so the body views can show
         *   something for force before the simulation starts.
         */
        updateForceVectors: function() {
            this.performSubstep(0);
        },

        /**
         * Only runs if simulation isn't currently paused.
         * If we're recording, it saves state
         */
        _update: function(time, deltaTime) {
            // Split up the delta time into steps to smooth out the orbit
            deltaTime  = this.get('deltaTimePerStep');
            deltaTime *= this.get('speedScale');
            for (var i = 0; i < SMOOTHING_STEPS; i++)
                this.performSubstep(deltaTime / SMOOTHING_STEPS);

            this.set('secondCounter', this.get('secondCounter') + deltaTime);
        },

        /**
         * Runs through the physics algorithms, updates the models,
         *   and checks for collisions.  It actually loops through
         *   even smaller sub-substeps while doing the main physics
         *   calculations in order to smooth out the results even
         *   further.
         */
        performSubstep: function(deltaTime) {
            var i, j, s;

            // Perform many tiny sub-substeps before doing any
            //   collision detection or updating, because those
            //   operations are expensive, and we've got too
            //   much work to do in such little time.
            var state = this.getScratchState();
            for (i = 0; i < state.length; i++)
                state[i].saveState(this.bodies.at(i));

            var subSubSteps = 400 / SMOOTHING_STEPS;
            var dtPerSubSubstep = deltaTime / subSubSteps;

            for (s = 0; s < subSubSteps; s++)
                state = this.performSubSubStep(dtPerSubSubstep, state);

            // We've kept cheap copies of the real models for 
            //   making quick calculations, so now we must
            //   update those real models.
            for (i = 0; i < state.length; i++)
                state[i].applyState(this.bodies.at(i));
            
            // Check for collisions between bodies
            for (i = 0; i < this.bodies.length; i++) {
                for (j = i + 1; j < this.bodies.length; j++) {
                    if (this.bodies.at(i).collidesWith(this.bodies.at(j))) {
                        var smaller = this.smallerBody(this.bodies.at(i), this.bodies.at(j));
                        if (!smaller.get('exploded')) {
                            smaller.explode();
                            this.trigger('collision', this, smaller, this.largerBody(this.bodies.at(i), this.bodies.at(j)));
                        }
                    }
                }
            }

            // Check for out-of-bounds bodies
            this.detectOutOfBoundsBodies();
        },

        /**
         * Performs a sub-substep which is going from one state to
         *   the next according to the velocity Verlet algorithm.
         */
        performSubSubStep: function(deltaTime, state) {
            var nextState = this.getScratchState();
            var nextBodyState;
            var bodyState;

            var pos   = this._pos;
            var vel   = this._vel;
            var acc   = this._acc;
            var force = this._force;
            var nextVelocityHalfStep = this._nextVelocityHalf;

            for (var i = 0; i < state.length; i++) {
                bodyState     = state[i];
                nextBodyState = nextState[i];

                nextBodyState.position.set(
                    pos
                        .set(bodyState.position)
                        .add(vel.set(bodyState.velocity).scale(deltaTime))
                        .add(acc.set(bodyState.acceleration).scale(deltaTime * deltaTime / 2))
                );
                nextVelocityHalfStep.set(
                    vel
                        .set(bodyState.velocity)
                        .add(acc.set(bodyState.acceleration).scale(deltaTime / 2))
                );
                nextBodyState.acceleration.set(
                    force
                        .set(this.getForce(bodyState, nextBodyState.position, state))
                        .scale(-1.0 / bodyState.mass)
                );
                nextBodyState.velocity.set(
                    nextVelocityHalfStep.add(
                        acc
                            .set(nextBodyState.acceleration)
                            .scale(deltaTime / 2)
                    )
                );
                nextBodyState.mass = bodyState.mass;
                nextBodyState.exploded = bodyState.exploded;
            }

            return nextState;
        },

        /**
         * Returns the sum of all forces on body at its proposed
         *   new position from all potential sources.
         */
        getForce: function(target, newTargetPosition, sources) {
            var sum = this._sum.set(0, 0);

            if (this.get('gravityEnabled')) {
                for (var i = 0; i < sources.length; i++) {
                    if (sources[i] !== target) {
                        sum.add(
                            this.getForceFromSource(target, newTargetPosition, sources[i])
                        );
                    }
                }
            }

            return sum;
        },

        /**
         * Returns the force on body at its proposed new position
         *   from a single source.
         */
        getForceFromSource: function(target, newTargetPosition, source) {
            if (source.position.equals(newTargetPosition)) {
                // If they are on top of each other, force should be 
                //   infinite, but ignore it since we want to have 
                //   semi-realistic behavior.
                return this._sourceForce.set(0, 0);
            }
            else if (source.exploded) {
                // Ignore in the computation if that body has exploded
                return this._sourceForce.set(0, 0);
            }
            else {
                return this._sourceForce
                    .set(newTargetPosition)
                    .sub(source.position)
                    .normalize()
                    .scale(
                        Constants.G * source.mass * target.mass / source.position.distanceSq(newTargetPosition)
                    );
            }
        },

        /**
         * Returns the smaller of two bodies.
         */
        smallerBody: function(body1, body2) {
            if (body1.get('mass') < body2.get('mass'))
                return body1;
            else
                return body2;
        },

        /**
         * Returns the larger of two bodies.
         */
        largerBody: function(body1, body2) {
            if (body1.get('mass') > body2.get('mass'))
                return body1;
            else
                return body2;
        },

        /**
         * Returns every body that is out of bounds to its last
         *   saved state.
         */
        returnAllOutOfBoundsBodies: function() {
            for (var i = 0; i < this.bodies.length; i++) {
                if (this.bodyOutOfBounds(this.bodies.at(i)))
                    this.returnBody(this.bodies.at(i));
            }
        },

        /**
         * Returns an out-of-bounds body to its last saved state.
         */
        returnBody: function(body) {
            var bodyIndex = this.bodies.indexOf(body);
            this.savedState[bodyIndex].applyState(this.bodies.at(bodyIndex));
        },

        /**
         * Returns whether or not a body is out of bounds.
         */
        bodyOutOfBounds: function(body) {
            return !this.bounds.contains(body.get('position'));
        },

        /**
         * Looks for out-of-bounds bodies and triggers an event
         *   if there is one.
         */
        detectOutOfBoundsBodies: function() {
            for (var i = 0; i < this.bodies.length; i++) {
                if (this.bodyOutOfBounds(this.bodies.at(i))) {
                    this.trigger('body-out-of-bounds');
                    return;
                }
            }
        },

        /**
         * Sets a bounding box around the scene from the min
         *   and max x and y values.  It's easier to give
         *   these values because one can simply reverse the
         *   model-view-transform with screen coordinates.
         */
        setBounds: function(minX, minY, maxX, maxY) {
            var width = maxX - minX;
            var height = maxY - minY;
            this.bounds.set(minX, minY, width, height);
        },

        /**
         * Returns a bounding rectangle of the scene.
         */
        getBounds: function() {
            return this.bounds;
        }

    });

    return GOSimulation;
});
Пример #3
0
define(function (require, exports, module) {

    'use strict';

    // Libraries
    var _         = require('underscore');
    var Vector2   = require('common/math/vector2');
    var Rectangle = require('common/math/rectangle');

    // Project dependiencies
    var FixedIntervalSimulation     = require('common/simulation/fixed-interval-simulation');
    var PiecewiseCurve              = require('common/math/piecewise-curve');
    var Air                         = require('models/air');
    var Beaker                      = require('models/element/beaker');
    var BeakerContainer             = require('models/element/beaker-container');
    var Burner                      = require('models/element/burner');
    var Block                       = require('models/element/block');
    var Brick                       = require('models/element/brick');
    var IronBlock                   = require('models/element/iron-block');
    var ElementFollowingThermometer = require('models/element/element-following-thermometer');

    // Constants
    var Constants = require('constants');

    /**
     * 
     */
    var IntroSimulation = FixedIntervalSimulation.extend({

        defaults: _.extend(FixedIntervalSimulation.prototype.defaults, {}),
        
        
        initialize: function(attributes, options) {

            options = options || {};
            options.framesPerSecond = Constants.FRAMES_PER_SECOND;

            FixedIntervalSimulation.prototype.initialize.apply(this, arguments);
        },

        /**
         * Sets up all the models necessary for the simulation.
         */
        initComponents: function() {
            // Burners
            this.rightBurner = new Burner({ position: new Vector2(0.18, 0) });
            this.leftBurner  = new Burner({ position: new Vector2(0.08, 0) });

            // Moveable thermal model objects
            this.brick     = new Brick(    { position: new Vector2(-0.1,   0) });
            this.ironBlock = new IronBlock({ position: new Vector2(-0.175, 0) });

            this.beaker = new BeakerContainer({ 
                position: new Vector2(-0.015, 0), 
                potentiallyContainedObjects: [
                    this.brick,
                    this.ironBlock
                ],
                width:  IntroSimulation.BEAKER_WIDTH,
                height: IntroSimulation.BEAKER_HEIGHT
            });

            // Thermometers
            this.thermometers = [];
            for (var i = 0; i < IntroSimulation.NUM_THERMOMETERS; i++) {
                var thermometer = new ElementFollowingThermometer({
                    position: IntroSimulation.INITIAL_THERMOMETER_LOCATION,
                    active: false
                }, {
                    elementLocator: this
                });

                this.listenTo(thermometer, 'change:sensedElement', this.thermometerSensedElementChanged);

                this.thermometers.push(thermometer);
            }

            // Air
            this.air = new Air();

            // Element groups
            this.movableElements = [
                this.ironBlock,
                this.brick,
                this.beaker
            ];
            this.supportingSurfaces = [
                this.leftBurner,
                this.rightBurner,
                this.brick,
                this.ironBlock,
                this.beaker
            ];
            this.burners = [
                this.leftBurner,
                this.rightBurner
            ];
            this.blocks = [
                this.brick,
                this.ironBlock
            ];

            // List of all objects that need updating
            this.models = _.flatten([
                this.air,
                this.supportingSurfaces,
                this.thermometers
            ]);

            // Cached objects
            this._location   = new Vector2();
            this._pointAbove = new Vector2();
            this._translation = new Vector2();
            this._allowedTranslation = new Vector2();
            this._initialMotionConstraints = new Rectangle();
            this._burnerBlockingRect = new Rectangle();
            this._beakerLeftSide = new Rectangle();
            this._beakerRightSide = new Rectangle();
            this._beakerBottom = new Rectangle();
            this._testRect = new Rectangle();

            // Just for debugging
            this.leftBurner.cid = 'leftBurner';
            this.rightBurner.cid = 'rightBurner';
            this.brick.cid = 'brick';
            this.ironBlock.cid = 'ironBlock';
            this.beaker.cid = 'beaker';
            this.air.cid = 'air';
        },

        /**
         * This is called on a reset to set the simulation
         *   components back to defaults.  The inherited 
         *   behavior is to just call initComponents, but
         *   since we want to manually reset each component 
         *   in this simulation instead of clearing them 
         *   out and starting over, we override this
         *   function.
         */
        resetComponents: function() {
            this.air.reset();
            this.leftBurner.reset();
            this.rightBurner.reset();
            this.ironBlock.reset();
            this.brick.reset();
            this.beaker.reset();
            _.each(this.thermometers, function(thermometer){
                thermometer.reset();
            });
        },

        /**
         * Resets the playback speed to normal.
         */
        resetTimeScale: function() {
            this.set('timeScale', 1);
        },

        /**
         * 
         */
        fastForward: function() {
            this.set('timeScale', Constants.FAST_FORWARD_TIMESCALE);
        },

        /**
         * 
         */
        _update: function(time, deltaTime) {
            // For the time slider and anything else relying on time
            this.set('time', time);

            // Reposition elements with physics/snapping
            this._findSupportingSurfaces(time, deltaTime);

            // Update the fluid level in the beaker, which could be displaced by
            //   one or more of the blocks.
            this.beaker.updateFluidLevel([
                this.brick.getRect(), 
                this.ironBlock.getRect()
            ]);

            // Exchange energy between objects
            this._exchangeEnergy(time, deltaTime);

            for (var i = 0; i < this.models.length; i++)
                this.models[i].update(time, deltaTime);
        },

        /**
         * PhET Original explanation: 
         *   "Cause any user-movable model elements that are not supported by a
         *      surface to fall (or, in some cases, jump up) towards the nearest
         *      supporting surface."
         */
        _findSupportingSurfaces: function(time, deltaTime) {
            for (var i = 0; i < this.movableElements.length; i++) {
                var element = this.movableElements[i];

                // If the user is moving it, do nothing; if it's already at rest, do nothing.
                if (!element.get('userControlled') && !element.getSupportingSurface() && element.get('position').y !== 0) {
                    var minYPos = 0;
                    
                    // Determine whether there is something below this element that
                    //   it can land upon.
                    var potentialSupportingSurface = this.findBestSupportSurface(element);
                    if (potentialSupportingSurface) {
                        minYPos = potentialSupportingSurface.yPos;

                        // Center the element above its new parent
                        var targetX = potentialSupportingSurface.getCenterX();
                        //console.log('setting x');
                        element.setX(targetX);
                        //console.log('done setting x');
                    }
                    
                    // Calculate a proposed Y position based on gravitational falling.
                    var acceleration = -9.8; // meters/s*s
                    var velocity = element.get('verticalVelocity') + acceleration * deltaTime;
                    var proposedYPos = element.get('position').y + velocity * deltaTime;
                    if (proposedYPos < minYPos) {
                        // The element has landed on the ground or some other surface.
                        proposedYPos = minYPos;
                        element.set('verticalVelocity', 0);
                        if (potentialSupportingSurface) {
                            element.setSupportingSurface(potentialSupportingSurface);
                            potentialSupportingSurface.addElementToSurface(element);
                        }
                    }
                    else {
                        element.set('verticalVelocity', velocity);
                    }
                    element.setY(proposedYPos);
                }
            }
        },

        /**
         * 
         */
        _exchangeEnergy: function(time, deltaTime) {
            var i;
            var j;
            var burner;
            var element;
            var otherElement;
            var chunk;

            /**
             *  Note: The original intent was to design all the energy containers
             *   such that the order of the exchange didn't matter, nor who was
             *   exchanging with whom.  This turned out to be a lot of extra work to
             *   maintain, and was eventually abandoned.  So, the order and nature of
             *   the exchanged below should be maintained unless there is a good
             *   reason not to, and any changes should be well tested.
             */

            // Loop through all the movable thermal energy containers and have them
            //   exchange energy with one another.
            for (i = 0; i < this.movableElements.length - 1; i++) {
                for (j = i + 1; j < this.movableElements.length; j++) {
                    this.movableElements[i].exchangeEnergyWith(this.movableElements[j], deltaTime);
                }
            }

            // Exchange thermal energy between the burners and the other thermal
            //   model elements, including air.
            for (i = 0; i < this.burners.length; i++) {
                burner = this.burners[i];
                if (burner.areAnyOnTop(this.movableElements)) {
                    for (j = 0; j < this.movableElements.length; j++) {
                        burner.addOrRemoveEnergyToFromObject(this.movableElements[j], deltaTime);
                    }
                }
                else {
                    burner.addOrRemoveEnergyToFromAir(this.air, deltaTime);
                }
            }

            // Exchange energy chunks between burners and non-air energy containers.
            for (i = 0; i < this.burners.length; i++) {
                burner = this.burners[i];
                for (j = 0; j < this.movableElements.length; j++) {
                    element = this.movableElements[j];
                    if (burner.inContactWith(element)) {
                        if (burner.canSupplyEnergyChunk() && (burner.getEnergyChunkBalanceWithObjects() > 0 || element.getEnergyChunkBalance() < 0)) {
                            // Push an energy chunk into the item on the burner.
                            element.addEnergyChunk(burner.extractClosestEnergyChunk(element.getCenterPoint()));
                        }
                        else if (burner.canAcceptEnergyChunk() && (burner.getEnergyChunkBalanceWithObjects() < 0 || element.getEnergyChunkBalance() > 0)) {
                            // Extract an energy chunk from the model element.
                            chunk = element.extractClosestEnergyChunk(burner.getFlameIceRect());
                            if (chunk)
                                burner.addEnergyChunk(chunk);
                        }
                    }
                }
            }

            // Exchange energy chunks between movable thermal energy containers.
            var elem1;
            var elem2;
            for (i = 0; i < this.movableElements.length - 1; i++) {
                for (j = i + 1; j < this.movableElements.length; j++) {
                    elem1 = this.movableElements[i];
                    elem2 = this.movableElements[j];
                    if (elem1.getThermalContactArea().getThermalContactLength(elem2.getThermalContactArea()) > 0) {
                        // Exchange chunks if appropriate
                        if (elem1.getEnergyChunkBalance() > 0 && elem2.getEnergyChunkBalance() < 0)
                            elem2.addEnergyChunk(elem1.extractClosestEnergyChunk(elem2.getThermalContactArea().getBounds()));
                        else if (elem1.getEnergyChunkBalance() < 0 && elem2.getEnergyChunkBalance() > 0)
                            elem1.addEnergyChunk(elem2.extractClosestEnergyChunk(elem1.getThermalContactArea().getBounds()));
                    }
                }
            }

            // Patrick's note: I have no idea why we're exchanging chunks between movable elements twice.

            // Exchange energy and energy chunks between the movable thermal
            //   energy containers and the air.
            for (i = 0; i < this.movableElements.length; i++) {
                element = this.movableElements[i];
                // Set up some variables that are used to decide whether or not
                //   energy should be exchanged with air.
                var contactWithOtherMovableElement = false;
                var immersedInBeaker = false;
                var maxTemperatureDifference = 0;

                // Figure out the max temperature difference between touching
                //   energy containers.
                for (j = 0; j < this.movableElements.length; j++) {
                    otherElement = this.movableElements[j];

                    if (element === otherElement)
                        break;

                    if (element.getThermalContactArea().getThermalContactLength(otherElement.getThermalContactArea()) > 0) {
                        contactWithOtherMovableElement = true;
                        maxTemperatureDifference = Math.max(Math.abs(element.getTemperature() - otherElement.getTemperature()), maxTemperatureDifference);
                    }
                }

                if (this.beaker.getThermalContactArea().getBounds().contains(element.getRect())) {
                    // This model element is immersed in the beaker.
                    immersedInBeaker = true;
                }

                // Exchange energy and energy chunks with the air if appropriate
                //   conditions met.
                if (!contactWithOtherMovableElement || (
                        !immersedInBeaker && (
                            maxTemperatureDifference < IntroSimulation.MIN_TEMPERATURE_DIFF_FOR_MULTI_BODY_AIR_ENERGY_EXCHANGE ||
                            element.getEnergyBeyondMaxTemperature() > 0
                        )
                    )
                ) {
                    this.air.exchangeEnergyWith(element, deltaTime);
                    if (element.getEnergyChunkBalance() > 0) {
                        var pointAbove = this._pointAbove.set(
                            Math.random() * element.getRect().w + element.getRect().left(),
                            element.getRect().top()
                        );
                        chunk = element.extractClosestEnergyChunk(pointAbove);
                        if (chunk) {
                            //console.log('(' + element.cid + ') giving chunk to air');
                            var initialMotionConstraints = null;
                            if (element instanceof Beaker) {
                                // Constrain the energy chunk's motion so that it
                                // doesn't go through the edges of the beaker.
                                // There is a bit of a fudge factor in here to
                                // make sure that the sides of the energy chunk,
                                // and not just the center, stay in bounds.
                                var energyChunkWidth = 0.01;
                                initialMotionConstraints = this._initialMotionConstraints.set( 
                                    element.getRect().x + energyChunkWidth / 2,
                                    element.getRect().y,
                                    element.getRect().w - energyChunkWidth,
                                    element.getRect().h 
                                );
                            }
                            this.air.addEnergyChunk(chunk, initialMotionConstraints);
                        }
                    }
                    else if (element.getEnergyChunkBalance() < 0 && element.getTemperature() < this.air.getTemperature()) {
                        element.addEnergyChunk(this.air.requestEnergyChunk(element.getCenterPoint()));
                    }
                }
            }

            // Exchange energy chunks between the air and the burners.
            for (i = 0; i < this.burners.length; i++) {
                burner = this.burners[i];
                var energyChunkCountForAir = burner.getEnergyChunkCountForAir();
                if (energyChunkCountForAir > 0)
                    this.air.addEnergyChunk(burner.extractClosestEnergyChunk(burner.getCenterPoint()), null);
                else if (energyChunkCountForAir < 0)
                    burner.addEnergyChunk(this.air.requestEnergyChunk(burner.getCenterPoint()));
            }
        },

        /**
         * Validate the position being proposed for the given model element.  This
         * evaluates whether the proposed position would cause the model element
         * to move through another solid element, or the side of the beaker, or
         * something that would look weird to the user and, if so, prevent the odd
         * behavior from happening by returning a location that works better.
         *
         * @param element         Element whose position is being validated.
         * @param proposedPosition Proposed new position for element
         * @return The original proposed position if valid, or alternative position
         *         if not.
         */
        validatePosition: function(element, proposedPosition) {
            // Compensate for the element's center X position
            var translation = this._translation
                .set(proposedPosition)
                .sub(element.get('position'));

            // Figure out how far the block's right edge appears to protrude to
            //   the side due to perspective.
            var blockPerspectiveExtension = Block.SURFACE_WIDTH * Constants.BlockView.PERSPECTIVE_EDGE_PROPORTION * Math.cos(Constants.BlockView.PERSPECTIVE_ANGLE) / 2;

            // Validate against burner boundaries.  Treat the burners as one big
            //   blocking rectangle so that the user can't drag things between
            //   them.  Also, compensate for perspective so that we can avoid
            //   difficult z-order issues.
            var standPerspectiveExtension = this.leftBurner.getOutlineRect().h * Burner.EDGE_TO_HEIGHT_RATIO * Math.cos(Constants.BurnerStandView.PERSPECTIVE_ANGLE) / 2;
            var burnerRectX = this.leftBurner.getOutlineRect().x - standPerspectiveExtension - (element !== this.beaker ? blockPerspectiveExtension : 0);
            var burnerBlockingRect = this._burnerBlockingRect.set( 
                burnerRectX,
                this.leftBurner.getOutlineRect().y,
                this.rightBurner.getOutlineRect().right() - burnerRectX,
                this.leftBurner.getOutlineRect().h
            );
            translation = this.determineAllowedTranslation(element.getRect(), burnerBlockingRect, translation, false);

            // Validate against the sides of the beaker.
            if (element !== this.beaker) {
                // Create three rectangles to represent the two sides and the top
                //   of the beaker.
                var testRectThickness = 1E-3; // 1 mm thick walls.
                var beakerRect = this.beaker.getRect();
                var beakerLeftSide = this._beakerLeftSide.set(
                    beakerRect.left() - blockPerspectiveExtension,
                    this.beaker.getRect().bottom(),
                    testRectThickness + blockPerspectiveExtension * 2,
                    this.beaker.getRect().h + blockPerspectiveExtension
                );
                var beakerRightSide = this._beakerRightSide.set(
                    this.beaker.getRect().right() - testRectThickness - blockPerspectiveExtension,
                    this.beaker.getRect().bottom(),
                    testRectThickness + blockPerspectiveExtension * 2,
                    this.beaker.getRect().h + blockPerspectiveExtension
                );
                var beakerBottom = this._beakerBottom.set(
                    this.beaker.getRect().left(), 
                    this.beaker.getRect().bottom(), 
                    this.beaker.getRect().w, 
                    testRectThickness
                );

                // Do not restrict the model element's motion in positive Y
                //   direction if the beaker is sitting on top of the model 
                //   element - the beaker will simply be lifted up.
                var restrictPositiveY = !this.beaker.isStackedUpon(element);

                // Clamp the translation based on the beaker position.
                translation = this.determineAllowedTranslation(element.getRect(), beakerLeftSide,  translation, restrictPositiveY);
                translation = this.determineAllowedTranslation(element.getRect(), beakerRightSide, translation, restrictPositiveY);
                translation = this.determineAllowedTranslation(element.getRect(), beakerBottom,    translation, restrictPositiveY);
            }

            // Now check the model element's motion against each of the blocks.
            for (var i = 0; i < this.blocks.length; i++) {
                var block = this.blocks[i];

                if (element === block)
                    continue;

                // Do not restrict the model element's motion in positive Y
                //   direction if the tested block is sitting on top of the model
                //   element - the block will simply be lifted up.
                var restrictPositiveY = !block.isStackedUpon(element);

                var testRect = this._testRect.set(element.getRect());
                if (element === this.beaker) {
                    // Special handling for the beaker - block it at the outer
                    // edge of the block instead of the center in order to
                    // simplify z-order handling.
                    testRect.set( 
                        testRect.x - blockPerspectiveExtension,
                        testRect.y,
                        testRect.w + blockPerspectiveExtension * 2,
                        testRect.h
                    );
                }

                // Clamp the translation based on the test block's position, but
                //   handle the case where the block is immersed in the beaker.
                if (element !== this.beaker || !this.beaker.getRect().contains(block.getRect())) {
                    translation = this.determineAllowedTranslation(testRect, block.getRect(), translation, restrictPositiveY);
                }
            }

            // Determine the new position based on the resultant translation and return it.
            return translation.add(element.get('position'));
        },

        /*
         * Determine the portion of a proposed translation that may occur given
         * a moving rectangle and a stationary rectangle that can block the moving
         * one.
         *
         * @param movingRect
         * @param stationaryRect
         * @param proposedTranslation
         * @param restrictPosY        Boolean that controls whether the positive Y
         *                            direction is restricted.  This is often set
         *                            false if there is another model element on
         *                            top of the one being tested.
         * @return
         */
        determineAllowedTranslation: function(movingRect, stationaryRect, proposedTranslation, restrictPosY) {
            var translation;

            translation = this.checkOverlapOnProposedTranslation(movingRect, stationaryRect, proposedTranslation, restrictPosY);
            
            if (translation)
                return translation;

            translation = this.checkCollisionsOnProposedTranslation(movingRect, stationaryRect, proposedTranslation, restrictPosY);

            return translation;
        },

        checkOverlapOnProposedTranslation: function(movingRect, stationaryRect, proposedTranslation, restrictPosY) {
            var translation = this._allowedTranslation;

            // Test for case where rectangles already overlap.
            if (movingRect.overlaps(stationaryRect)) {
                // The rectangles already overlap.  Are they right on top of one another?
                if (movingRect.center().x === stationaryRect.center().x && movingRect.center().x === stationaryRect.center().x) {
                    console.error('IntroSimulation - Warning: Rectangle centers in same location--returning zero vector.');
                    return translation.set(0, 0);
                }

                // Determine the motion in the X & Y directions that will "cure"
                //   the overlap.
                var xOverlapCure = 0;
                if (movingRect.right() > stationaryRect.left() && movingRect.left() < stationaryRect.left()) {
                    xOverlapCure = stationaryRect.left() - movingRect.right();
                }
                else if (stationaryRect.right() > movingRect.left() && stationaryRect.left() < movingRect.left()) {
                    xOverlapCure = stationaryRect.right() - movingRect.left();
                }
                var yOverlapCure = 0;
                if (movingRect.top() > stationaryRect.bottom() && movingRect.bottom() < stationaryRect.bottom()) {
                    yOverlapCure = stationaryRect.bottom() - movingRect.top();
                }
                else if ( stationaryRect.top() > movingRect.bottom() && stationaryRect.bottom() < movingRect.bottom()) {
                    yOverlapCure = stationaryRect.top() - movingRect.bottom();
                }

                // Something is wrong with algorithm if both values are zero,
                //   since overlap was detected by the "intersects" method.
                if (xOverlapCure === 0 && yOverlapCure === 0)
                    return;

                // Return a vector with the smallest valid "cure" value, leaving
                //   the other translation value unchanged.
                if (xOverlapCure !== 0 && Math.abs(xOverlapCure) < Math.abs(yOverlapCure)) {
                    return translation.set(xOverlapCure, proposedTranslation.y);
                }
                else {
                    return translation.set(proposedTranslation.x, yOverlapCure);
                }
            }
        },

        checkCollisionsOnProposedTranslation: function(movingRect, stationaryRect, proposedTranslation, restrictPosY) {
            var translation = this._allowedTranslation;

            var xTranslation = proposedTranslation.x;
            var yTranslation = proposedTranslation.y;

            // X direction.
            if (proposedTranslation.x > 0) {
                // Check for collisions moving right.
                var rightEdgeSmear = this.projectShapeFromLine(movingRect.right(), movingRect.bottom(), movingRect.right(), movingRect.top(), proposedTranslation);

                if (movingRect.right() <= stationaryRect.left() && rightEdgeSmear.intersects(stationaryRect)) {
                    // Collision detected, limit motion.
                    xTranslation = stationaryRect.left() - movingRect.right() - IntroSimulation.MIN_INTER_ELEMENT_DISTANCE;
                }
            }
            else if (proposedTranslation.x < 0) {
                // Check for collisions moving left.
                var leftEdgeSmear = this.projectShapeFromLine(movingRect.left(), movingRect.bottom(), movingRect.left(), movingRect.top(), proposedTranslation);

                if (movingRect.left() >= stationaryRect.right() && leftEdgeSmear.intersects(stationaryRect)) {
                    // Collision detected, limit motion.
                    xTranslation = stationaryRect.right() - movingRect.left() + IntroSimulation.MIN_INTER_ELEMENT_DISTANCE;
                }
            }

            // Y direction.
            if (proposedTranslation.y > 0 && restrictPosY) {
                // Check for collisions moving up.
                var topEdgeSmear = this.projectShapeFromLine(movingRect.left(), movingRect.top(), movingRect.right(), movingRect.top(), proposedTranslation);

                if (movingRect.top() <= stationaryRect.bottom() && topEdgeSmear.intersects(stationaryRect)) {
                    // Collision detected, limit motion.
                    yTranslation = stationaryRect.bottom() - movingRect.top() - IntroSimulation.MIN_INTER_ELEMENT_DISTANCE;
                }
            }
            if (proposedTranslation.y < 0) {
                // Check for collisions moving down.
                var bottomEdgeSmear = this.projectShapeFromLine(movingRect.left(), movingRect.bottom(), movingRect.right(), movingRect.bottom(), proposedTranslation);

                if (movingRect.bottom() >= stationaryRect.top() && bottomEdgeSmear.intersects(stationaryRect)) {
                    // Collision detected, limit motion.
                    yTranslation = stationaryRect.top() - movingRect.bottom() + IntroSimulation.MIN_INTER_ELEMENT_DISTANCE;
                }
            }

            return translation.set(xTranslation, yTranslation);
        },

        projectShapeFromLine: function(x1, y1, x2, y2, projection) {
            var curve = new PiecewiseCurve();
            curve.moveTo(x1, y1);
            curve.lineTo(x1 + projection.x, y1 + projection.y);
            curve.lineTo(x2 + projection.x, y2 + projection.y);
            curve.lineTo(x2, y2);
            curve.close();
            return curve;
        },

        /**
         * Finds the most appropriate supporting surface for the element.
         */
        findBestSupportSurface: function(element) {
            var bestOverlappingSurface = null;

            // Check each of the possible supporting elements in the model to see
            //   if this element can go on top of it.
            for (var i = 0; i < this.supportingSurfaces.length; i++) {
                var potentialSupportingElement = this.supportingSurfaces[i];

                if (potentialSupportingElement === element || potentialSupportingElement.isStackedUpon(element)) {
                    // The potential supporting element is either the same as the
                    //   test element or is sitting on top of the test element.  In
                    //   either case, it can't be used to support the test element,
                    //   so skip it.
                    continue;
                }

                if (element.getBottomSurface().overlapsWith( potentialSupportingElement.getTopSurface())) {
                    // There is at least some overlap.  Determine if this surface
                    //   is the best one so far.
                    var surfaceOverlap = this.getHorizontalOverlap(potentialSupportingElement.getTopSurface(), element.getBottomSurface());
                    
                    // The following nasty 'if' clause determines if the potential
                    //   supporting surface is a better one than we currently have
                    //   based on whether we have one at all, or has more overlap
                    //   than the previous best choice, or is directly above the
                    //   current one.
                    if (bestOverlappingSurface === null || (
                            surfaceOverlap > this.getHorizontalOverlap(bestOverlappingSurface, element.getBottomSurface()) && 
                            !this.isDirectlyAbove(bestOverlappingSurface, potentialSupportingElement.getTopSurface())
                        ) || (
                            this.isDirectlyAbove(potentialSupportingElement.getTopSurface(), bestOverlappingSurface)
                        )) {
                        bestOverlappingSurface = potentialSupportingElement.getTopSurface();
                    }
                }
            }

            // Make sure that the best supporting surface isn't at the bottom of
            //   a stack, which can happen in cases where the model element being
            //   tested isn't directly above the best surface's center.
            if (bestOverlappingSurface) {
                while (bestOverlappingSurface.elementOnSurface !== null ) {
                    bestOverlappingSurface = bestOverlappingSurface.elementOnSurface.getTopSurface();
                }
            }

            return bestOverlappingSurface;
        },

        /**
         * Get the amount of overlap in the x direction between two horizontal surfaces.
         */
        getHorizontalOverlap: function(s1, s2) {
            var lowestMax  = Math.min(s1.xMax, s2.xMax);
            var highestMin = Math.max(s1.xMin, s2.xMin);
            return Math.max(lowestMax - highestMin, 0);
        },
        
        /**
         * Returns true if surface s1's center is above surface s2.
         */
        isDirectlyAbove: function(s1, s2) {
            return s2.containsX(s1.getCenterX()) && s1.yPos > s2.yPos;
        },

        /**
         * This replaces EFACIntroModel.getTemperatureAndColorAtLocation because
         *   I believe it should be the job of the element model to internally
         *   decide what its temperature should be, and it should be up to the
         *   element view to determine the color.  Therefore, the simulation
         *   model will only return the element, and objects that use this will
         *   be responsible for requesting the temperature and color at location
         *   from the returned element.
         */
        getElementAtLocation: function(x, y) {
            var location = this._location;
            if (x instanceof Vector2)
                location.set(x);
            else
                location.set(x, y);

            // Test blocks first.  This is a little complicated since the z-order
            //   must be taken into account.
            this.blocks.sort(function(b1, b2) {
                if (b1.get('position').equals(b2.get('position')))
                    return 0;
                if (b2.get('position').x > b1.get('position').x || b2.get('position').y > b1.get('position').y)
                    return 1;
                return -1;
            });

            for (var i = 0; i < this.blocks.length; i++) {
                if (this.blocks[i].getProjectedShape().contains(location))
                    return this.blocks[i];
            }
            
            // Test if this point is in the water or steam associated with the beaker.
            if (this.beaker.getThermalContactArea().getBounds().contains(location) ||
                (this.beaker.getSteamArea().contains(location) && this.beaker.get('steamingProportion') > 0)) {
                return this.beaker;
            }
            
            // Test if the point is a burner.
            for (var j = 0; j < this.burners.length; j++) {
                if (this.burners[j].getFlameIceRect().contains(location))
                    return this.burners[j];
            }

            // Point is in nothing else, so return the air.
            return this.air;
        },

        getBlockList: function() {
            return this.blocks;
        },

        getBeaker: function() {
            return this.beaker;
        },

        thermometerSensedElementChanged: function(thermometer, element) {
            var blockWidthIncludingPerspective = this.ironBlock.getProjectedShape().getBounds().w;
            var beakerLeft  = this.beaker.getRect().center().x - blockWidthIncludingPerspective / 2;
            var beakerRight = this.beaker.getRect().center().x + blockWidthIncludingPerspective / 2;
            var thermometerX = thermometer.get('position').x;
            if (thermometer.previous('sensedElement') === this.beaker && 
                !thermometer.get('userControlled') && 
                thermometerX >= beakerLeft && 
                thermometerX <= beakerRight
            ) {
                thermometer.set('userControlled', true);
                thermometer.setPosition(this.beaker.getRect().right() - 0.01, this.beaker.getRect().bottom() + this.beaker.getRect().h * 0.33);
                thermometer.set('userControlled', false);
            }
        }

    }, Constants.IntroSimulation);

    return IntroSimulation;
});
Пример #4
0
define(function (require, exports, module) {

    'use strict';

    // Libraries
    var _ = require('underscore');

    // Common dependencies
    var FixedIntervalSimulation = require('common/simulation/fixed-interval-simulation');
    var Vector2 = require('common/math/vector2');
    
    // Project dependiencies
    var Air = require('models/air');

    var Faucet = require('models/energy-source/faucet');
    var Sun    = require('models/energy-source/sun');
    var Teapot = require('models/energy-source/teapot');
    var Biker  = require('models/energy-source/biker');

    var ElectricalGenerator = require('models/energy-converter/electrical-generator');
    var SolarPanel          = require('models/energy-converter/solar-panel');

    var IncandescentLightBulb = require('models/energy-user/incandescent-light-bulb');
    var FluorescentLightBulb  = require('models/energy-user/fluorescent-light-bulb');
    var BeakerHeater          = require('models/energy-user/beaker-heater');

    var CarouselAnimator = require('models/carousel-animator');
    var Belt             = require('models/belt');
    
    // Constants
    var Constants = require('constants');

    /**
     * 
     */
    var EnergySystemsSimulation = FixedIntervalSimulation.extend({

        defaults: _.extend(FixedIntervalSimulation.prototype.defaults, {
            source: null,
            converter: null,
            user: null
        }),
        
        initialize: function(attributes, options) {
            options = options || {};
            options.framesPerSecond = Constants.FRAMES_PER_SECOND;

            FixedIntervalSimulation.prototype.initialize.apply(this, arguments);

            this.on('change:source',    this.sourceChanged);
            this.on('change:converter', this.converterChanged);
            this.on('change:user',      this.userChanged);
        },

        /**
         * Initializes all the model components necessary for the 
         *   simulation to function.
         */
        initComponents: function() {
            // Air
            this.air = new Air();

            // Sources
            this.faucet = new Faucet();
            this.sun    = new Sun();
            this.teapot = new Teapot();
            this.biker  = new Biker();

            // Converters
            this.electricalGenerator = new ElectricalGenerator();
            this.solarPanel          = new SolarPanel();

            // Users
            this.incandescentLightBulb = new IncandescentLightBulb();
            this.fluorescentLightBulb  = new FluorescentLightBulb();
            this.beakerHeater          = new BeakerHeater();

            // Belt
            // Create the belt that interconnects the biker and the generator.
            //   Some position tweaking was needed in order to get this to
            //   show up in the right place.  Not entirely sure why.
            this.belt = new Belt({
                wheel1Radius: Biker.REAR_WHEEL_RADIUS,
                wheel1Center: new Vector2(EnergySystemsSimulation.ENERGY_SOURCE_POSITION).add(Biker.CENTER_OF_BACK_WHEEL_OFFSET).add(0.005, 0),
                wheel2Radius: ElectricalGenerator.WHEEL_RADIUS,
                wheel2Center: new Vector2(EnergySystemsSimulation.ENERGY_CONVERTER_POSITION).add(ElectricalGenerator.WHEEL_CENTER_OFFSET)
            });

            // Add meaningful cids for debugging
            this.faucet.cid = 'faucet';
            this.sun.cid    = 'sun';
            this.teapot.cid = 'teapot';
            this.biker.cid  = 'biker';

            this.electricalGenerator.cid = 'electrical-generator';
            this.solarPanel.cid          = 'solar-panel';

            this.incandescentLightBulb.cid = 'incandescent-light-bulb';
            this.fluorescentLightBulb.cid  = 'fluorescent-light-bulb';
            this.beakerHeater.cid          = 'beaker-heater';

            // Group lists
            this.sources = [
                this.faucet,
                this.sun,
                this.teapot,
                this.biker
            ];

            this.converters = [
                this.electricalGenerator,
                this.solarPanel
            ];

            this.users = [
                this.beakerHeater,
                this.incandescentLightBulb,
                this.fluorescentLightBulb
            ];

            // List of all models
            this.models = _.flatten([
                this.air,
                this.sources,
                this.converters,
                this.users
            ]);

            // Events
            this.listenTo(this.electricalGenerator, 'change:active', function(faucet, active) {
                this.faucet.set('waterPowerableElementInPlace', active);
                this.teapot.set('steamPowerableElementInPlace', active);
                this.biker.set('mechanicalPoweredSystemIsNext', active);

                this.updateBeltVisibility();
            });

            this.listenTo(this.biker, 'change:active', function(faucet, active) {
                this.electricalGenerator.set('directCouplingMode', active);

                this.updateBeltVisibility();
            });

            // The sun needs a reference to the solar panel
            this.sun.set('solarPanel', this.solarPanel);

            this.selectDefaultElements();
            this.get('source').activate();
            this.get('converter').activate();
            this.get('user').activate();
            this.get('source').set('opacity', 1);
            this.get('converter').set('opacity', 1);
            this.get('user').set('opacity', 1);

            // Animators
            this.sourceAnimator = new CarouselAnimator({
                elements: this.sources,
                activeElement: this.get('source'),
                activeElementPosition: EnergySystemsSimulation.ENERGY_SOURCE_POSITION
            });
            this.converterAnimator = new CarouselAnimator({
                elements: this.converters,
                activeElement: this.get('converter'),
                activeElementPosition: EnergySystemsSimulation.ENERGY_CONVERTER_POSITION
            });
            this.userAnimator = new CarouselAnimator({
                elements: this.users,
                activeElement: this.get('user'),
                activeElementPosition: EnergySystemsSimulation.ENERGY_USER_POSITION
            });

            var activateElement = function(activeElement) { 
                activeElement.activate();
            };

            this.listenTo(this.sourceAnimator,    'destination-reached', activateElement);
            this.listenTo(this.converterAnimator, 'destination-reached', activateElement);
            this.listenTo(this.userAnimator,      'destination-reached', activateElement);
        },

        selectDefaultElements: function() {
            this.set('source',    this.faucet);
            this.set('converter', this.electricalGenerator);
            this.set('user',      this.beakerHeater);
        },

        /**
         * This is called on a reset to set the simulation
         *   components back to defaults.  The inherited 
         *   behavior is to just call initComponents, but
         *   since we want to manually reset each component 
         *   in this simulation instead of clearing them 
         *   out and starting over, we override this
         *   function.
         */
        resetComponents: function() {
            /* We could have a case where one of the
             *   currently selected items is one of
             *   defaults, so just selecting it again
             *   wouldn't properly deactivate it first.
             */
            _.each(_.flatten([
                this.sources,
                this.converters,
                this.users
            ]), function(element) {
                if (element.active())
                    element.deactivate();
            });

            this.faucet.set('waterPowerableElementInPlace', true);
            this.teapot.set('steamPowerableElementInPlace', true);
            this.biker.set('mechanicalPoweredSystemIsNext', true);
            this.electricalGenerator.set('directCouplingMode', false);
            this.updateBeltVisibility();

            this.selectDefaultElements();

            this.get('source').activate();
            this.get('converter').activate();
            this.get('user').activate();

            this.sourceAnimator.reset();
            this.converterAnimator.reset();
            this.userAnimator.reset();
        },

        preloadEnergyChunks: function() {
            this.get('source').preloadEnergyChunks();
            this.get('converter').preloadEnergyChunks();
            this.get('user').preloadEnergyChunks();
        },

        /**
         * Internal update that is called on each fixed-time step
         *   because it's a fixed-interval simulation model.
         */
        _update: function(time, deltaTime) {
            // For the time slider and anything else relying on time
            // this.set('time', time);

            this.sourceAnimator.update(time, deltaTime);
            this.converterAnimator.update(time, deltaTime);
            this.userAnimator.update(time, deltaTime);

            // Update the active elements to produce, convert, and use energy.
            var energyFromSource    = this.get('source').update(time, deltaTime);
            var energyFromConverter = this.get('converter').update(time, deltaTime, energyFromSource);
                                      this.get('user').update(time, deltaTime, energyFromConverter);

            // Transfer energy chunks between elements
            var sourceOutput = this.get('source').extractOutgoingEnergyChunks();
            this.get('converter').injectEnergyChunks(sourceOutput); 
            var converterOutput = this.get('converter').extractOutgoingEnergyChunks();
            this.get('user').injectEnergyChunks(converterOutput);

            //console.log('source output: ' + sourceOutput.length + ', converter output: ' + converterOutput.length +', bulb output: ' + this.get('user').radiatedEnergyChunkMovers.length);
        },

        sourceChanged: function(simulation, source) {
            this.activeElementChanged(source, this.previous('source'));
            this.sourceAnimator.set('activeElement', source);
        },

        converterChanged: function(simulation, converter) {
            this.activeElementChanged(converter, this.previous('converter'));
            this.converterAnimator.set('activeElement', converter);
        },

        userChanged: function(simulation, user) {
            this.activeElementChanged(user, this.previous('user'));
            this.userAnimator.set('activeElement', user);
        },

        activeElementChanged: function(activeElement, previousElement) {
            if (previousElement)
                previousElement.deactivate();
        },

        updateBeltVisibility: function() {
            this.belt.set('visible', this.electricalGenerator.active() && this.biker.active());
        }

    }, Constants.EnergySystemsSimulation);

    return EnergySystemsSimulation;
});