Source: rigid_shapes/rigid_shape.js


/* 
 * File: rigid_shape.js
 *      base class for objects to participate in physics system
 */
"use strict";

import * as debugDraw from "../core/debug_draw.js";

let kShapeColor = [0, 1, 0, 1];
let kBoundColor = [1, 1, 1, 1];

import * as physics from "../components/physics.js";
import * as input from "../components/input.js";
import * as loop from "../core/loop.js";

let kRigidShapeUIDelta = 0.01;   // for UI interactive debugging
let kPrintPrecision = 2;         // for printing float precision

class RigidShape {
    /**
     * @classdesc Base class for objects to participate in physics system, extended by RigidCircles and RigidRectangles. 
     * Used to control the Transform of another object
     * <p>Found in Chapter 9, page 534 of the textbook</p>
     * Examples:
     * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.1.rigid_shapes_and_bounds/index.html 9.1 Rigid Shape and Bounds},
     * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.6.collision_position_correction/index.html 9.6 Collision Position Correction},
     * {@link https://mylesacd.github.io/build-your-own-2d-game-engine-2e-doc/BookSourceCode/chapter9/9.9.physics_presets/index.html 9.9 Physics Presets}
     * 
     * @constructor
     * @param {Transform} xf - the Transform for this RigidShape
     * @returns {RigidShape} a new RigidShape instance
     */
    constructor(xf) {
        this.mXform = xf;
        this.mAcceleration = physics.getSystemAcceleration();
        this.mVelocity = vec2.fromValues(0, 0);
        this.mType = "";

        this.mInvMass = 1;
        this.mInertia = 0;

        this.mFriction = 0.8;
        this.mRestitution = 0.2;

        this.mAngularVelocity = 0;

        this.mBoundRadius = 0;

        this.mDrawBounds = false;
    }

    // #region getters and setters
    /**
     * Returns what subtype this RigidShape belongs to
     * @method
     * @returns {string} mType - whether this is a RigidCircle or RigidRectangle
     */
    getType() { return this.mType; }

    /**
     * Returns the inverse mass of this RigidShape
     * @method
     * @returns {float} mInvMass - the inverse mass
     */
    getInvMass() { return this.mInvMass; }

    /**
     * Sets the mass of this RigidShape
     * @method
     * @param {float} m - the new mass
     */
    setMass(m) {
        if (m > 0) {
            this.mInvMass = 1 / m;
            this.mAcceleration = physics.getSystemAcceleration();
        } else {
            this.mInvMass = 0;
            this.mAcceleration = [0, 0];  // to ensure object does not move
        }
        this.updateInertia();
    }
    /**
     * Returns the inertia of this RigidShape
     * @method
     * @returns {float} mInertia - the inertia
     */
    getInertia() { return this.mInertia; }
    /**
     * Sets the inertia of this RigidShape
     * @method
     * @param {float} i - the new inertia
     */
    setInertia(i) { this.mInertia = i; }

    /**
     * Returns the friction for this RigidShape
     * @method
     * @returns {float} mFriction - the friction
     */
    getFriction() { return this.mFriction; }
    /**
     * Sets the friction for this RigidShape
     * @method
     * @param {float} f - the new friction
     */
    setFriction(f) { this.mFriction = f; }

    /**
     * Returns the restitution for this RigidShape
     * @method
     * @returns {float} mRestitution - the restitution
     */
    getRestitution() { return this.mRestitution; }
    /**
     * Sets the restitution for this RigidShape
     * @param {float} r - the new restitution 
     */
    setRestitution(r) { this.mRestitution = r; }

    /**
     * Returns the angular velocity of this RigidShape
     * @method
     * @returns {float} mAngularVelocity - the angular velocity
     */
    getAngularVelocity() { return this.mAngularVelocity; }
    /**
     * Sets the angular velocity for this RigidShape
     * @method
     * @param {float} w - the new angular velocity 
     */
    setAngularVelocity(w) { this.mAngularVelocity = w; }
    /**
     * Adds a value to the current angular velocity for this RigidShape
     * @method
     * @param {float} dw - value to add
     */
    setAngularVelocityDelta(dw) { this.mAngularVelocity += dw; }

    /**
     * Returns the world coordinates of the center of this RigidShape
     * @method
     * @returns {vec2} [X,Y] center point
     */
    getCenter() { return this.mXform.getPosition(); }

    /**
     * Returns the radius of the bounds for this RigidShape
     * @method
     * @returns {float} mBoundRadius - the bounding radius
     */
    getBoundRadius() { return this.mBoundRadius; }
    
    /**
     * Toggle whether the bounds of this RigidShape are drawn
     * @method
     */
    toggleDrawBound() { this.mDrawBounds = !this.mDrawBounds; }
    /**
     * Sets the bound radius of this RigidShape
     * @method
     * @param {float} r - the new bound radius 
     */
    setBoundRadius(r) { this.mBoundRadius = r; }

    /**
     * Returns the velocity vector of this RigidShape
     * @returns {vec2} mVelocity - [X,Y] velocity vector
     */
    getVelocity() { return this.mVelocity; }

    /**
     * Set the velocity vector of this RigidShape
     * @param {float} x - horizontal world coordinate velocity
     * @param {float} y - vertical world coordinate velocity
     */
    setVelocity(x, y) {
        this.mVelocity[0] = x;
        this.mVelocity[1] = y;
    }

    /**
     * Reverse the direction of the velocity vector of this RigidShape
     * @method
     */
    flipVelocity() {
        this.mVelocity[0] = -this.mVelocity[0];
        this.mVelocity[1] = -this.mVelocity[1];
    }
    /**
     * Returns the world coordinate acceleration vector of this RigidShape
     * @method
     * @returns {vec2} mAcceleration - the acceleration vector
     */
    getAcceleration() { return this.mAcceleration; }

    /**
     * Sets the world coordinate acceleration vector of this RigidShape
     * @method
     * @param {float} x - horizontal acceleration
     * @param {float} y - vertical acceleration
     */
    setAcceleration(x, y) {
        this.mAcceleration[0] = x;
        this.mAcceleration[1] = y;
    }
    
    /**
     * Set the Transform for this RigidShape
     * @method
     * @param {Transform} xf - the Transform
     */
    setTransform(xf) { this.mXform = xf; }
    // #endregion

    travel() {
        let dt = loop.getUpdateIntervalInSeconds();

        // update velocity by acceleration
        vec2.scaleAndAdd(this.mVelocity, this.mVelocity, this.mAcceleration, dt);

        // p  = p + v*dt  with new velocity
        let p = this.mXform.getPosition();
        vec2.scaleAndAdd(p, p, this.mVelocity, dt);

        this.mXform.incRotationByRad(this.mAngularVelocity * dt);
    }

    /**
     * Set the world coordinate position of this RigidShape
     * @method
     * @param {float} x - horizontal position
     * @param {float} y - vertical position
     */
    setPosition(x, y) {
        this.mXform.setPosition(x, y);
    }

    /**
     * Adjust the position of this RigidShape by moving the position a distance along a specified vector
     * @method
     * @param {vec2} v - the [X,Y] direction vector
     * @param {float} delta - the distance to move
     */
    adjustPositionBy(v, delta) {
        let p = this.mXform.getPosition();
        vec2.scaleAndAdd(p, p, v, delta);
    }

    /**
     * Update this RigidShape changing the position, velocity, and rotation if 
     * physics motion is enabled and the RigidShape has mass
     * @method
     */
    update() {
        if (this.mInvMass === 0)
            return;

        if (physics.getHasMotion())
            this.travel();
    }

    /**
     * Test if this RigidShape is overlapping with another RigidShape
     * @param {RigidShape} otherShape - the other RigidShape
     * @returns {boolean} true if the two shapes overlap
     */
    boundTest(otherShape) {
        let vFrom1to2 = [0, 0];
        vec2.subtract(vFrom1to2, otherShape.mXform.getPosition(), this.mXform.getPosition());
        let rSum = this.mBoundRadius + otherShape.mBoundRadius;
        let dist = vec2.length(vFrom1to2);
        if (dist > rSum) {
            // not overlapping
            return false;
        }
        return true;
    }

    // #region drawing as line and circle
    /**
     * Draws this RigidShape to aCamera if bound drawing is enabled
     * @param {Camera} aCamera - the Camera to draw to
     */
    draw(aCamera) {
        if (!this.mDrawBounds)
            return;
        debugDraw.drawCircle(aCamera, this.mXform.getPosition(), this.mBoundRadius, this._boundColor());
        debugDraw.drawCrossMarker(aCamera, this.mXform.getPosition(), 
                                  this.mBoundRadius * 0.2, this._boundColor());
    }

    _shapeColor() { return kShapeColor; }
    _boundColor() { return kBoundColor; }
    // #endregion 

    // #region support interactive debugging and state querying
    /**
     * Returns a string containing the mass, inertia, friction, and restitution of this RigidShape
     * @returns {string} - the information string
     */
    getCurrentState() {
        let m = this.mInvMass;
        if (m !== 0)
            m = 1 / m;

        return "M=" + m.toFixed(kPrintPrecision) +
            "(I=" + this.mInertia.toFixed(kPrintPrecision) + ")" +
            " F=" + this.mFriction.toFixed(kPrintPrecision) +
            " R=" + this.mRestitution.toFixed(kPrintPrecision);
    }
    
    /**
     * Based on keyboard input raise or lower the mass, friction, or restitution of this RigidShape
     * @method
     */
    userSetsState() {
        // keyboard control
        let delta = 0;

        if (input.isKeyPressed(input.keys.Up)) {
            delta = kRigidShapeUIDelta;
        }
        if (input.isKeyPressed(input.keys.Down)) {
            delta = -kRigidShapeUIDelta;
        }
        if (delta !== 0) {
            if (input.isKeyPressed(input.keys.M)) {
                let m = 0;
                if (this.mInvMass > 0)
                    m = 1 / this.mInvMass;
                this.setMass(m + delta * 10);
            }
            if (input.isKeyPressed(input.keys.F)) {
                this.mFriction += delta;
                if (this.mFriction < 0)
                    this.mFriction = 0;
                if (this.mFriction > 1)
                    this.mFriction = 1;
            }
            if (input.isKeyPressed(input.keys.N)) {
                this.mRestitution += delta;
                if (this.mRestitution < 0)
                    this.mRestitution = 0;
                if (this.mRestitution > 1)
                    this.mRestitution = 1;
            }
        }
    }
    // #endregion
}

export default RigidShape;