Parse

File Parse point.js

This runs the server-side parser and regenerates the documentation tree for this source file.

Source

            /*
files:
    relative-xy.js
    pointcast.js
categories: primary
    point
doc_readme: point/readme.md
doc_content: point/*.md
doc_imports: point
doc_loader: loadDocInfo

*/

const isPoint = function(value) {
    return value.constructor == Point
}


const isFunction = function(value) {
    return (typeof(value) == 'function')
}


const point = function(p, b) {
    if(p.constructor == Point) {
        return p
    }
    if(Array.isArray(p)) {
        return new Point({x: p[0], y:p[1]})
    }

    if(b !== undefined) {
        return new Point({x: p, y: b})
    }

    return p
}



function getVector(radians, multiplier=1) {
    /* Return the 2D XY vector of this point using `this.radians`.

        point.getVector()
        { x: 1.2, y: 0.3 }

    Provide a multuplier, for example to create an incident angle:

        point.getVector(-1)
        { x: -1.2, y: -0.3 }

     */
    return {
        // Step 1: Convert the incident angle (from radians) to a vector
        x: multiplier * Math.cos(radians)
        , y: multiplier * Math.sin(radians)
    }
}


window.loadDocInfo = function() {
    /* Return the think to resolve */
    console.log('loadDocInfo called')
    return Point;
}


class Positionable extends Relative {
    /* The base `Positionable` class provides functionality for
    plotting the X and Y of an entity. This includes any fundamental
    methods such as `multiply()`.
    */

    /* ABOVE X method

    Multiline - touching.
    */
    set x(value) {
        /* Set the _X_ (horizontal | latitude | across) value of the positionable.
        From cooridinate top left `(0,0)``

            point.x = 100

        If the given value is a function, the function is called immediately,
        with _this_ positionable as the first argument.

            stage.center.x = (p)=>200

        This property is known as a _special_ prop, and will actuate the
        `setSpecial` method and dirty flag.
         */
        // this._opts.x = isFunction(v)? v(this, 'x'): v
        return this.setSpecial('x', value)
    }

    set y(value) {
        /* Set the _Y_ (vertical | height | longtitude) value of the positionable.
        From cooridinate top left `(0,0)``

            point.x = 100

        If the given value is a function, the function is called immediately,
        with _this_ positionable as the first argument.

            stage.center.x = (p)=>200

        This property is known as a _special_ prop, and will actuate the
        `setSpecial` method and dirty flag.
         */
        // this._opts.y = isFunction(v)? v(this, 'y'): v
        return this.setSpecial('y', value)
    }

    get x() {

        // const _x = this._opts.x;
        // let r = _x == undefined? 0: _x
        // let v = this._relativeData[0]
        // r = isFunction(r)? r(this, 'x'): r
        // return r + v
        return this.getSpecial('x', 0)
    }

    get y() {

        // const _y = this._opts.y;
        // let r = _y == undefined? 0: _y
        // r = isFunction(r)? r(this, 'y'): r
        // return r + this._relativeData[1]
        return this.getSpecial('y', 1)
    }

    set radius(v) {
        // this._opts.radius = isFunction(v)? v(this, 'radius'): v
        return this.setSpecial('radius', v)
    }

    get radius() {
        /*
        Return the _radius_ of this point in base units (pixels)

            const point = new Point(100,300, 20)
            point.radius == 20
        */
        // const _radius = this._opts.radius
        // let r = _radius == undefined? 0: _radius;
        // r = isFunction(r)? r(this, 'radius'): r;
        // return r + this._relativeData[2]
        return this.getSpecial('radius', 2, 5)
    }

    setSpecial(key,  value) {
        /* Set the given `key`, `value`, assuming the given key is a "special"
        string, such as "radius".
        This method is called by the special getters and setters for this point:

            const point = new Point(100,300, 20)
            point.setSpecial('radius', 55)

        Synonymous to:

            point.radius = 55

        */
        let rV = isFunction(value)? value(this, key): value
        const v = this.onSpecialSet(key, rV)
        this._opts[key] = v === undefined? rV: v
        return true
    }

    onSpecialSet(key, value) {
        /* A callback executed by setSpecial when a "special" property is called.

        If a method on this point exists matching the _set method_ pattern,
        the method is called with the given `value`:

            class MyPoint extends Point {
                radiusSet(value) {
                    console.log('New Radius value is', value)
                }
            }

        */
        this._dirty = true
        let name = `${key}Set`
        let r = this[name] && this[name](value)
        this._onDirty?.forEach(f=>f(name, r))
        // console.log(name)
        return r
    }

    onDirty(func) {
        /* Assign a function to call when a special value on this node is
        changed.

            let p = new Point(10, 10)
            p.onDirty((key) => {
                console.log('set', key)
            })
            p.y = 100
            // set y

        This provides a more _reactive_ method of dirty test, as this is called
        immediately after the special key is changed.
        Returns nothing.
        */
        if(!this._onDirty) { this._onDirty = [] }
        this._onDirty.push(func)
    }

    get dirty() {
        /* Return the _is_ `dirty` value. */
        return this._dirty
    }

    set dirty(v) {
        /* Typically we should allow dirty to be auto managed,
       however it can be overridden with custom functionality.
      At the moment we simply accept and store the given value.

      This may affect the running code, as dirty is reflagged by the
      internal point. In the future turning off auto dirty may be an option..*/
        this._dirty = v;
    }

    get wasDirty() {
        /* Return the _is_ `dirty` value, but also remove the dirty flag.
        This is useful for dirty tests within the active render loop:

            if(myPoint.wasDirty) {
                // will call.
                console.log('Dirty')
            }

            myPoint.dirty
            // false

        */
        let r = this._dirty
        this._dirty = false;
        return r
    }


    getSpecial(key, relIndex=undefined, defaultValue=0) {
        /* Return a stored _special_ value given a `key`.

        If the `relIndex`  property is not None, the relative value found
        at the index (within this points relative data Array),
        add the stashed relative value to the result.

            const point = new Point(100,300, 20)
            point.getSpecial('radius', 2)
            // 20

        if a default value is given, and the internal `key` value does not exist,
        return the default value:

            point.getSpecial('banana', 'yellow')
            'yellow'

        */
        const internalValue = this._opts[key];
        let r = internalValue == undefined? defaultValue: internalValue
        r = isFunction(r)? r(this, key): r
        let relVal = relIndex != undefined? this.getRelativeData()[relIndex]: 0
        return r + relVal
    }

    set(x, y, radius, rotation) {
        /*
            set(x, y, radius, rotation)
            set(100)    => x,y
            set([])     => [x,y,radius, rotation]
            set({})     => *
        */
        const isUndefined = function(v) {
            return v === undefined
        }

        const ifUndefined = (v, d) => v === undefined? d: v


        if(isUndefined(y)) {

            if(Array.isArray(x)) {
                this.fromArray
                let lmap = {
                    1: () => {
                        /* An array of one
                            set([200])
                        */
                    }
                    , 2: ()=> {
                        [x,y] = x
                    }
                    , 3: ()=> {
                        [x,y, radius] = x
                    }
                    , 4: ()=> {
                        [x,y, radius, rotation] = x
                    }
                }

                lmap[x.length]()

            } else if(typeof(x)=='number') {
                y = x
                x = x
            }else{
                // object
                for(let k in x) {
                    this[k] = x[k]
                }
                y = x?.y
                x = x?.x
            }
        }

        /* XY Are absolutely mandatory. TODO: pickup from _defaults_ */
        this.x = ifUndefined(x, 0)
        this.y = ifUndefined(y, 0)

        if(!isUndefined(radius)) {
            /* We don't have to worry about object.radius here
            as that will already be set. */
            this.radius = radius
        }

        if(!isUndefined(rotation)) {
            /* same as above. */
            this.rotation = rotation
        }
    }

    _cast(other, _2=other) {
        if(typeof(other) == 'number') { other = point(other, _2) }
        if(Array.isArray(other)){ return point(other, _2) }
        return other
    }

    subtract(other, _2=other){
        /* "subtract" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, _2)
        }

        return new Point(this.x - other.x, this.y - other.y)
    }

    add(other, _b,) {
        /* "Add" this point to the _other_ point, returning a new point. */
        other = this._cast(other, _b)

        return new Point(
            this.x + other.x,
            this.y + other.y
        )
    }

    divide(other) {
        /* "Divide" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, other)
        }

        let nNaN = v => isNaN(v)? 0: v;

        return new Point(
              nNaN(this.x / other.x)
            , nNaN(this.y / other.y)
        )
    }

    multiply(other) {
        /* "Multiply" this point to the _other_ point, returning a new point. */
        if(typeof(other) == 'number') {
            other = point(other, other)
        }

        return new Point(
            this.x * other.x,
            this.y * other.y
        )
    }

    // _midpoint(other, offset=.5) {
    //     /*return a new point, with the XY set at the _mid point_ between
    //     this point and the given*/
    //     let p = this.copy()
    //     p.x = (p.x + other.x) * offset
    //     p.y = (p.y + other.y) * offset
    //     return p
    // }

    midpoint(other, offset=0.5) {
        /*
        Returns a new point, with the XY set at the point that is `offset` times
        the distance from the current point to the other point.

        this function is also `lerp` for linear interpolation
        */
        let p = this.copy();
        p.x = p.x + (other.x - p.x) * offset;
        p.y = p.y + (other.y - p.y) * offset;
        return p;
    }

    lerp = this.midpoint

}



class Rotation extends Positionable {
    set rotation(value){
        /* Set the rotation in degrees (0 to 360). If `this.modulusRotate` is
        `true` (default), the given value is fixed through modulus 360

            point.rotation = 600
            // 240
            point.rotation += 1
            // 241

        To set the rotation (degrees) whilst accounting for the _UP_ vector,
        consider the `rotate()` method.
        */

        if(this.modulusRotate == false) {
            return this.setSpecial('rotation', value)
        }

        return this.setSpecial('rotation', value % 360)

    }

    rotate(degrees) {
        this.rotation = this.UP + degrees
        return this
    }

    get rotation() {
        return this.getSpecial('rotation', 3)
    }

    get radians() {
        /*Return the _radians_ of the current rotation, where _rotation returns
        the degrees*/
        return degToRad(this.getSpecial('rotation', 3))
    }

    set radians(angle) {
        /* by pushing through the existing rotations, we account for the _up_
        vector. */
        this.rotation = radiansToDegrees(angle)
    }

    lookAt(otherPoint, add=0, rotationMultiplier=undefined) {
        /* Rotate the point such that the angle relative to the
        `otherPoint` is `0`, essentially _looking at_ the other point.

            point.lookAt(otherPoint)

        Return the angle in radians.
        */
        return this.radians = this.directionTo(otherPoint, rotationMultiplier, add)// + add
    }

    directionTo(otherPoint, rotationMultiplier=undefined, addRad=0) {
        // Calculate the differences in x and y coordinates
        let delta = 0
        try {
            delta = otherPoint.subtract(this);
        } catch(e) {
            if(!isPoint(otherPoint)) {
                otherPoint = new Point(otherPoint)
                delta = otherPoint.subtract(this);
            } else {
                throw e
            }

        }
        if(rotationMultiplier != undefined) {
            let normRad = this._normalizedRadians(otherPoint, rotationMultiplier, addRad)
            return normRad
        }

        // Calculate the angle in radians
        const angleRadians = delta.atan2()
        return angleRadians + addRad
    }

    _normalizedRadians(otherPoint, rotationMultiplier, addRad=0) {
        const delta = otherPoint.subtract(this);
        const targetRad = delta.atan2() + addRad;
        const currentRad = this.radians;

        let radDiff = targetRad - currentRad;
        // Normalize the angle difference to be within the range -PI to PI
        radDiff = Math.atan2(Math.sin(radDiff), Math.cos(radDiff));
        const newAngleRadians = currentRad + radDiff * rotationMultiplier;
        const normRad = Math.atan2(
                            Math.sin(newAngleRadians),
                            Math.cos(newAngleRadians)
                        );
        return normRad;
    }

    turnTo(otherPoint, rotationMultiplier=1){
        let normRad = this._normalizedRadians(otherPoint, rotationMultiplier)
        this.radians = normRad;
        return normRad
    }

    getTheta(other, direction=undefined) {
        /* Return the calculated theta value through atan2 and built to offload
        some of the boring.
        The _direction_ denotes the "gravity" pull. Generally this is `DOWN`.

        Synonymous to:

            let theta = Math.atan2(point.y,
                                   point.x);

            let theta = Math.atan2(point.y - other.y,
                                   point.x - other.x);

            let theta = Math.atan2(point.y - other.y,
                                   point.x - other.x) - DOWN;

        */
        let x = this.x
          , y = this.y
            ;

        if(other) {
            let _p = this.subtract(other)
            x = _p.x
            y = _p.y
        }

        // Gravity is generally DOWN by default
        // perhaps this should be -rotation.
        // direction = this.resolveStringOrFunction(direction, DOWN)
        let theta = Math.atan2(y, x) - direction
        return theta
    }
}



class Tooling extends Rotation {

    resolveStringOrFunction(direction, defaultValue) {
        let res = defaultValue;

        if(typeof(direction) == 'string') {
            // str such as this["rotation"]
            res = this[direction]
        }

        if(typeof(direction) == 'function') {
            return direction()
        }

        return res
    }

    atan2() {
        /* Return the theta value through the atan2 function for this point.*/
        let x = this.x
            , y = this.y
            ;

        let thetaRadians = Math.atan2(y, x);
        return thetaRadians
    }

    project(distance, rotation, relative=true) {
        if(rotation !== undefined && relative == true) {
            rotation = (this.UP + rotation) % 360
        }
        let np = new this.constructor(projectFrom(this, distance, rotation))
        np.rotation = this.rotation
        return np
    }

    getTip() {
        const distance = this.radius
        const rads = this.radians
        const vector = this.vector2D(distance)
        // Calculate the new x and y coordinates
        const x = this.x + vector.x //distance * Math.cos(rads);
        const y = this.y + vector.y //distance * Math.sin(rads);

        // const x = this.x + distance * Math.cos(rads);
        // const y = this.y + distance * Math.sin(rads);

        return { x, y, };
    }

    vector2D(multiplier=1) {
        /* Return the 2D XY vector of this point using `this.radians`.

            point.vector2D()
            { x: 1.2, y: 0.3 }

        Provide a multuplier, for example to create an incident angle:

            point.vector2D(-1)
            { x: -1.2, y: -0.3 }

         */
        const rads = this.radians
        return {
            // Step 1: Convert the incident angle (from radians) to a vector
            x: multiplier * Math.cos(rads)
            , y: multiplier * Math.sin(rads)
        }
    }

    copy(position, deep=false) {
        /*
        Copy this point as a new Point, or digest the given `position`.

            // new point.
            let p = stage.center.copy()

        Given another point:

            // drink options.
            p.copy(new Point(100, 100))


        Notably `copy(other)` is mutative and calls `set` on this point.
        */
        if(position) {
            this.set(position.x, position.y)
            if(position?.radians){
                this.radians = position.radians
            }

            /* Deep should inspect all given options  to a point, and apply them all. */
            if(deep==true && position.radius){
                this.radius = position.radius
            }

            return this;
        }

        return new Point(this.x, this.y, this.radius, this.rotation)
    }

    magnitude() {
        let x = this.x;
        let y = this.y;
        return Math.sqrt(x * x + y * y);
    }

    normalized(magnitude=this.magnitude()) {
        /*
        Synonymous to:

            {
                x: AB.x / magnitudeAB,
                y: AB.y / magnitudeAB
            };
        */
        return this.divide(magnitude)
        // return this.divide(this.magnitude())
    }

    interpolateTo(other, offset, pointIndex=0) {
        /* return a point relative from _this_ point towards the `other`,
        offset by the given number `offset` value.
        The `pointIndex` (default 0) identifies from which point [this, other]
        to offset.*/

        return getPointOffsetAbsolute(this, other, offset, pointIndex)
    }

    interpolateFrom(other, offset, pointIndex=0) {
        return getPointOffsetAbsolute(other, this, offset, pointIndex)
    }

    static distance(a, b){
        return Math.hypot(b.x - a.x, b.y - a.y);
    }

    quantize(amount=1) {
        let q = quantizeNumber
        return new this.constructor({
                            x: q(this.x, amount)
                            , y: q(this.y, amount)
                        })
    }

    protractorAngleTo(other, referencePoint) {
        let value = calculateAngleWithRefWithNeg(this, other, referencePoint)
        return new Angle(value)
    }

    lerpPixel(other, pixelDistance) {
        /*
        Returns a new point, offset by `pixelDistance` pixels in the direction
        from this point to the other point.
        */

        // Calculate the direction vector from this point to the other point
        let directionX = other.x - this.x;
        let directionY = other.y - this.y;
        let dirV = this.distance2D(other)
        let distance = this.distanceTo(other)
        // Normalize the direction vector (to unit length)
        let unitX = dirV.x / distance;
        let unitY = dirV.y / distance;

        // Scale the unit vector by the desired pixel distance
        let offsetX = unitX * pixelDistance;
        let offsetY = unitY * pixelDistance;

        // Create a new point at the scaled offset from the original point
        let p = this.copy();
        p.x = this.x + offsetX;
        p.y = this.y + offsetY;

        return p;
    }
}


class Point extends Tooling {
    /*The `Point` is the primary class for manipulating XY 2D points.

    Arguments:

        new Point(100, 200)

    Object | Array:

        new Point({x: 100, y: 200})
        new Point([100, 200])

    Properties:

        point = new Point
        point.x = 100
        point.y = 200

    */

    // x=0
    // y=0
    // radius = 5
    UP = UP_DEG
    _rotationDegrees = UP_DEG

    constructor(opts={}){
        /* A new point accepts arguments, an object, or an array.

        By default the `Point` accepts up to four arguments

            new Point(x, y, radius, rotation)

        The same properties may be supplied as a single `Array`:

            new Point([x, y, radius, rotation])

        If the given object is an `object`, we can assign properties
        immediately:

            point = new Point({x, y, radius, rotation, other:100})
            point.other
            // 100

        */
        super(opts)

        /* Ensure _opts_ is something. Default; a dict. */
        opts = arguments[0] || {}

        /* If given a Point instance, return the given point instance */
        if(opts && (opts.constructor == this.constructor) ){ return opts }
        // new Point(x,y, ...)  // reset the opts obj.
        if(arguments.length > 1 || typeof(arguments[0] == 'number')){ opts = {} }

        this.modulusRotate = undefined

        this._opts = Object.assign({relX: 0, relY: 0 }, opts)
        // set 0 or more object
        this.set.apply(this, arguments)
        this.created()
    }

    created() {
        // api hook for life.
    }

    update(data) {
        /*
        Perform an _update_ given a dictionary of other properties.

            point.update({ x: 200, color: 'red' })
        */
        for(let k in data) {
            this[k] = data[k]
        }

        return this
    }

    get uuid() {
        /* Get or create the random `_id` for this point.
        Return a unique string.
        */
        let r = this._id;
        if(r == undefined) {
            this._id = r = (~~(Math.random() * 10000)).toString(32)
        }
        return r
    }

    set uuid(v) {
        this._id = v
    }

    get [0]() {
        /* return the X value of the point:

            point[0]

        Note:

            point[0] == point.x
        */
        return this.x
    }

    set [0](value) {
        /*
        Sugar function to apply `this.x`:

            point[0] = 100
            point.x == 100
        */
        this.x = value
    }

    get [1]() {
        /*
        return the Y value of the point:

            point[0]

        Note:

            point[0] == point.y
        */
        return this.y
    }

    set [1](value) {
        /*
        Sugar function to apply `this.y`:

            point[0] = 300
            point.y == 300
        */
        this.y = value
    }

    get [Symbol.toStringTag]() {
        return this.toString()
    }

    [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
            return this.toString()
        }
        return null
        // return Reflect.apply(...arguments)
    }

    toString(){
        let name = 'point'
        return `${name}({x:${this.x}, y:${this.y}})`;
    }

    get _liveProps() { return true }

    asArray(fix=false) {
        /*
        Return this point as an Array of 4 values:

            const point = new Point(1,2,3,4);
            point.asArray();
            // [1,2,3,4]

        Keys:

        + x
        + y
        + radius
        + rotation

        */
        // let r = [this.x, this.y, this.radius, this.rotation]
        let r = Object.values(this.asObject())
        if(fix) {
            let int = (x)=> Number( x.toFixed(Number(fix)) )
            return r.map(v=>int(v))
        }
        return r
    }

    asObject() {
        /* Return the important information about this node,
        used for _save_ or copy methods. */
        return {
            x: this.x
            , y: this.y
            , radius: this.radius
            , rotation: this.rotation
        }
    }

}


Polypoint.head.install(Point)


Polypoint.head.mixin('Point', {
    isNaN: {
        value(any=false) {
            let r = 0;
            r += +isNaN(this.x)
            r += +isNaN(this.y)
            if(r==0) { return false }

            if(r > 0) {
                if(any) { return true }
                // two NaNs, is always isNaN == true
                if(r >= 2) { return true }
            }

            return false
        }
        , writable: true
    }
})


;Object.defineProperty(Point, 'from', {
    value: function(a,b, c, d){

        if(a.offsetX && b==undefined) {
            // is an event
            return new Point(a.offsetX, a.offsetY)
        }
        return new Point(a,b, c, d)
    }
});

copy