core_World.js

const RectangularWorldBoundary = require("../walls/RectangularWorldBoundary");
const Collision = require("../behaviors/Collision");
const SpatialHashGrid = require("../core/SpatialHashGrid");
const Solver = require("../core/Solver");
const Renderer = require("../renderers/Renderer");
const Gravity = require("../behaviors/Gravity");
const PositionLock = require("../behaviors/PositionLock");
const Particle = require("./Particle");
const Vector2D = require("../utils/Vector2D");

/**
 * `World` the global-state instance of the physics engine that keeps track of all the objects. This provides
 * a higher level of abstraction from the user but may be limiting in some ways. It is a good idea to extend and 
 * override this class for any specific properties. With the current SpatialHashing algorithm, the world should have
 * finite bounds.
 */
class World {
    /**
     * Instantiates new `World` instance
     * @param {HTMLCanvasElement} canvas HTML canvas where the elements are displayed
     * @param {Number} width width of world
     * @param {Number} height height of world
     * @param {Number} xGrids integer number of grid separations in the x direction
     * @param {Number} yGrids integer number of grid separations in the y direction
     * @param {Number} timeStep change in time per solve iteration
     * @param {Number} iterationPerFrame number of solve iterations per frame
     * @param {Number} constraintIteration number of times constraints are solved per iteration
     * @constructor
     */
    constructor(canvas, width, height, xGrids, yGrids = null, timeStep = 1, iterationPerFrame = 1, constraintIteration = 1) {
        this.timeStep = timeStep;
        this.iterationPerFrame = iterationPerFrame;
        this.constraintIteration = constraintIteration;
        this.canvas = canvas;
        this.width = width;
        this.height = height;
        this.xGrids = xGrids;
        this.yGrids = yGrids;
        this.gravity = null;
        this.collision = null;
        this.dragBehavior = null;
        this.chargeBehavior = null;
        
        this.particles = new SpatialHashGrid(width, height, xGrids, yGrids);
        this.particlesList = [];
        this.constraints = [];
        this.walls = [];
        
        this.solver = new Solver(this.timeStep, this.iterationPerFrame, this.constraintIteration, this.particles, this.constraints, this.walls);
        this.renderer = new Renderer(this.solver, this.canvas);

        this.isRender = true;
    }

    /**
     * Adds a particle to the world
     * @param {Particle} p 
     * @public
     */
    addParticle(p) {
        if (this.gravity)
            p.addSelfBehavior(this.gravity);
        if (this.collision)
            p.addNearBehavior(this.collision);
        if (this.dragBehavior)
            p.addSelfBehavior(this.dragBehavior);
        if (this.chargeBehavior)
            p.addSelfBehavior(this.chargeBehavior);
        this.particles.add(p);
        this.updateParticleList();
    }

    /**
     * Removes a particle from the world
     * @param {Particle} p
     * @public
     */
    removeParticle(p) {
        this.particles.deleteItem(p);
        let removeCons = [];
        for (let c of this.constraints) {
            if (c.particles().includes(p)) {
                removeCons.push(c) 
            }
        }

        for (let c of removeCons) {
            this.removeConstraint(c);
        }
        
        this.updateParticleList();
    }

    /**
     * Adds a constraint to the world
     * @param {Constraint} c 
     * @public
     */
    addConstraint(c) {
        this.constraints.push(c);
    }

    /**
     * Removes a constraint from the world
     * @param {Constraint} c 
     * @returns {Boolean} true if the constraint is removed
     * @public
     */
    removeConstraint(c) {
        const index = this.constraints.indexOf(c);
		if (index > -1) {
			this.constraints.splice(index, 1);
			return true;
		}
		return false;
    }

    /**
     * Adds a wall to the world
     * @param {Wall} w 
     * @public
     */
    addWall(w) {
        this.walls.push(w);
    }

    /**
     * Removes a wall from the world
     * @param {Wall} w 
     * @returns {boolean} true if the wall is removed 
     * @public
     */
    removeWall(w) {
        const index = this.walls.indexOf(w);
		if (index > -1) {
			this.walls.splice(index, 1);
			return true;
		}
		return false;
    }

    /**
     * Clears all of the particles and any associated constraints
     * @public
     */
    clearParticles() {
        this.particles = new SpatialHashGrid(this.width, this.height, this.xGrids, this.yGrids);
        this.solver.particles = this.particles;
        this.clearConstraints();
        this.updateParticleList();
    }

    /**
     * Clears all of the constraints
     * @public
     */
    clearConstraints() {
        this.constraints = [];
        this.solver.constraints = this.constraints;
        this.updateParticleList();
    }

    /**
     * Clears all of the walls
     * @public 
     */
    clearWalls() {
        this.walls = [];
        this.solver.walls = [];
    }

    /**
     * Update the list of particles. Must be called every time the number of particles change.
     * @public
     */
    updateParticleList() {
        this.particlesList = this.particles.values();
        this.solver.updateSolverParticles();
    }

    /**
     * Adds a SelfBehavior to **all** the particles in the world
     * @param {SelfBehavior} b 
     * @public
     */
    addGlobalSelfBehavior(b) {
        for (let p of this.particlesList) {
            p.addSelfBehavior(b);
        }
    }

    /**
     * Removes a SelfBehavior from **all** the particles in the world
     * @param {SelfBehavior} b 
     * @public
     */
    removeGlobalSelfBehavior(b) {
        for (let p of this.particlesList) {
            p.removeSelfBehavior(b);
        }
    }

    /**
     * Adds a NearBehavior to **all** the particles in the world
     * @param {NearBehavior} b 
     * @public
     */
    addGlobalNearBehavior(b) {
        for (let p of this.particlesList) {
            p.addNearBehavior(b);
        }
    }

    /**
     * Removes a NearBehavior to **all** the particles in the world
     * @param {NearBehavior} b 
     * @public
     */
    removeGlobalNearBehavior(b) {
        for (let p of this.particlesList) {
            p.removeNearBehavior(b);
        }
    }

    /**
     * Removes existing gravity and adds a new global gravity behavior to all the particles, while updating 
     * `this.gravity`
     * @param {Number} num 
     * @public
     */
    enableGravity(num) {
        if (this.gravity) {
            this.disableGravity();
        }
        this.gravity = new Gravity(new Vector2D(0, num));
        this.addGlobalSelfBehavior(this.gravity);
    }

    /**
     * Removes the global gravity behavior and sets `this.gravity` to `null`
     * @returns {Boolean} true if gravity is successfully disabled
     * @public
     */
    disableGravity() {
        if (this.gravity) {
            this.removeGlobalSelfBehavior(this.gravity);
            this.gravity = null
            return true;
        }
        return false;
    }

    /**
     * Progresses world to next state
     * @public
     */
    nextFrame() {
        this.solver.nextFrame();
        if (this.isRender) {
            this.renderer.renderFrame();
        }
    }

    /**
     * Removes existing collision behavior and adds a new global collision behavior to all the particles, while updating 
     * `this.collision`
     * @public
     */
    enableCollisions() {
        if (this.collision) {
            this.disableCollisions();
        }
        this.collision = new Collision();
        this.addGlobalNearBehavior(this.collision);
    }

    /**
     * Removes the global gravity behavior and sets `this.collision` to `null`
     * @returns {Boolean} true if collision is successfully disabled
     * @public
     */
    disableCollisions() {
        if (this.collision) {
            this.removeGlobalNearBehavior(this.collision);
            this.collision = null;
            return true;
        }
        return false;
    }

    /**
     * Creates a `RectangularWorldBoundary` that confines the boundary of the world with rigid collisions. Also sets `this.worldConstraint`.
     * @param {Number} x1 x value of top-left coordinate (smaller)
     * @param {Number} x2 larger x value of bottom-right coordinate (larger)
     * @param {Number} y1 y value of top-left coordinate (smaller)
     * @param {Number} y2 y value of bottom-right coordinate (larger)
     * @public
     */
    constrainBoundary(x1= -Infinity, x2= Infinity, y1= -Infinity, y2= Infinity) {
        this.worldConstraint = new RectangularWorldBoundary(x1, x2, y1, y2);
        this.walls.push(this.worldConstraint);
    }

    /**
     * Removes existing drag behavior and adds a new global drag behavior  with a given viscosity to all the particles, while updating 
     * `this.drag`
     * @param {Number} viscosity 
     * @public
     */
    enableDrag(viscosity) {
        if (this.dragBehavior) {
            this.disableDrag();
        }
        this.dragBehavior = new Drag(viscosity);
        this.addGlobalSelfBehavior(this.dragBehavior);
    }

    /**
     * Removes existing drag behavior and sets `this.drag` to `null`
     * @returns {Boolean} true if drag is successfully disabled 
     * @public
     */
    disableDrag() {
        if (this.dragBehavior) {
            this.removeGlobalSelfBehavior(this.dragBehavior);
            return true;
        }
        return false;
    }

    /**
     * Removes existing ChargeInteraction behavior and adds a new global ChargeInteraction behavior to all the particles, while updating 
     * `this.chargeBehavior`
     * @public
     */
    enableChargeInteractions() {
        if(this.chargeBehavior) {
           this.disableChargeInteractions();
        }
        this.chargeBehavior = new ChargeInteraction();
        this.addGlobalNearBehavior(this.chargeBehavior);
    }

    /**
     * Removes the global ChargeInteraction behavior and sets `this.chargeBehavior` to `null`
     * @returns {Boolean} true if charge behavior is successfully disabled
     * @public
     */
    disableChargeInteractions() {
        if (this.chargeBehavior) {
            this.removeGlobalSelfBehavior(this.chargeBehavior);
            return true;
        }
        return false;
    }

    /**
     * Make a particle a "mass-pivot" by adding a `PositionLock` behavior and setting the adds `Number.MAX_SAFE_INTEGER / 10` to the particle mass. This is a **work around**
     * and may cause some other physics to break. This particle pivot will not work with position constraints.
     * @param {Particle} p 
     * @param {Vector2D} pos 
     * @returns {Boolean} true if particle is successfully converted to a "mass-pivot".
     * @public
     */
    makePivot(p, pos=null) {
        // this is incredibly scuffed, but this is what i could think of without introducing high cohesion
        for (let b of p.selfBehavior) {
            console.log(b);
            if(b instanceof PositionLock) {
                return false;
            }
        }
        p.mass = p.mass + Number.MAX_SAFE_INTEGER / 10;
        if(pos === null) {
            p.addSelfBehavior(new PositionLock(p.pos));
        } else {
            p.addSelfBehavior(new PositionLock(pos));
        }
        return true;
    }

    /**
     * Frees a particle from being a "mass-pivot". This attempts to correct the mass increase from the `makePivot()` method. 
     * Once again, it may not work properly :skull:.
     * @param {Particle} p 
     * @returns true if particle is successfully freed from a "mass-pivot".
     * @public
     */
    freePivot(p) {
        for (let b of p.selfBehavior) {
            if(b instanceof PositionLock) {
                p.mass = p.mass - Number.MAX_SAFE_INTEGER / 10;
                p.removeSelfBehavior(b);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets the optional `update()` function for the solver. 
     * @param {Function} update 
     * @public
     */
    setSolverUpdate(update) {
        this.solver.update = update;
    }

}

module.exports = World;