classes/circle.js

/**
 * Created by Alex Bol on 3/6/2017.
 */

"use strict";

module.exports = function(Flatten) {
    let {Arc,vector} = Flatten;
    /**
     * Class representing a circle
     * @type {Circle}
     */
    Flatten.Circle = class Circle {
        /**
         *
         * @param {Point} pc - circle center point
         * @param {number} r - circle radius
         */
        constructor(...args) {
            /**
             * Circle center
             * @type {Point}
             */
            this.pc = new Flatten.Point();
            /**
             * Circle radius
             * @type {number}
             */
            this.r = 1;

            if (args.length == 1 && args[0] instanceof Object && args[0].name === "circle") {
                let {pc, r} = args[0];
                this.pc = new Flatten.Point(pc);
                this.r = r;
                return;
            }
            else {
                let [pc, r] = [...args];
                if (pc && pc instanceof Flatten.Point) this.pc = pc.clone();
                if (r !== undefined) this.r = r;
                return;
            }

            throw Flatten.Errors.ILLEGAL_PARAMETERS;
        }

        /**
         * Method clone returns new instance of a Circle
         * @returns {Circle}
         */
        clone() {
            return new Flatten.Circle(this.pc.clone(), this.r);
        }

        /**
         * Circle center
         * @returns {Point}
         */
        get center() {
            return this.pc;
        }

        /**
         * Circle bounding box
         * @returns {Box}
         */
        get box() {
            return new Flatten.Box(
                this.pc.x - this.r,
                this.pc.y - this.r,
                this.pc.x + this.r,
                this.pc.y + this.r
            );
        }

        /**
         * Return true if circle contains point
         * @param {Point} pt - test point
         * @returns {boolean}
         */
        contains(pt) {
            return Flatten.Utils.LE(pt.distanceTo(this.center)[0], this.r);
        }

        /**
         * Transform circle to closed arc
         * @param {boolean} counterclockwise
         * @returns {Arc}
         */
        toArc(counterclockwise=true) {
            return new Flatten.Arc(this.center, this.r, Math.PI, -Math.PI, counterclockwise);
        }

        /**
         * Returns array of intersection points between circle and other shape
         * @param {Shape} shape Shape of the one of supported types
         * @returns {Point[]}
         */
        intersect(shape) {
            if (shape instanceof Flatten.Point) {
                return this.contains(shape) ? [shape] : [];
            }
            if (shape instanceof Flatten.Line) {
                return shape.intersect(this);
            }

            if (shape instanceof Flatten.Segment) {
                return shape.intersect(this);
            }

            if (shape instanceof Flatten.Circle) {
                return Circle.intersectCirle2Circle(this, shape);
            }

            if (shape instanceof Flatten.Arc) {
                return shape.intersect(this);
            }
            if (shape instanceof Flatten.Polygon) {
                return Flatten.Polygon.intersectShape2Polygon(this, shape);
            }
        }

        /**
         * Calculate distance and shortest segment from circle to shape and return array [distance, shortest segment]
         * @param {Shape} shape Shape of the one of supported types Point, Line, Circle, Segment, Arc, Polygon or Planar Set
         * @returns {number} distance from circle to shape
         * @returns {Segment} shortest segment between circle and shape (started at circle, ended at shape)

         */
        distanceTo(shape) {
            let {Distance} = Flatten;
            let {point2circle, circle2circle, circle2line, segment2circle, arc2circle} = Distance;

            if (shape instanceof Flatten.Point) {
                let [distance, shortest_segment] = point2circle(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.Circle) {
                let [distance, shortest_segment] = circle2circle(this, shape);
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.Line) {
                let [distance, shortest_segment] = circle2line(this, shape);
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.Segment) {
                let [distance, shortest_segment] = segment2circle(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.Arc) {
                let [distance, shortest_segment] = arc2circle(shape, this);
                shortest_segment = shortest_segment.reverse();
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.Polygon) {
                let [distance, shortest_segment] = Distance.shape2polygon(this, shape);
                return [distance, shortest_segment];
            }

            if (shape instanceof Flatten.PlanarSet) {
                let [dist, shortest_segment] = Distance.shape2planarSet(this, shape);
                return [dist, shortest_segment];
            }
        }

        static intersectCirle2Circle(circle1, circle2) {
            let ip = [];

            if (circle1.box.not_intersect(circle2.box)) {
                return ip;
            }

            let vec = new Flatten.Vector(circle1.pc, circle2.pc);

            let r1 = circle1.r;
            let r2 = circle2.r;

            // Degenerated circle
            if (Flatten.Utils.EQ_0(r1) || Flatten.Utils.EQ_0(r2))
                return ip;

            // In case of equal circles return one leftmost point
            if (Flatten.Utils.EQ_0(vec.x) && Flatten.Utils.EQ_0(vec.y) && Flatten.Utils.EQ(r1, r2)) {
                ip.push(circle1.pc.translate(-r1, 0));
                return ip;
            }

            let dist = circle1.pc.distanceTo(circle2.pc)[0];

            if (Flatten.Utils.GT(dist, r1 + r2))               // circles too far, no intersections
                return ip;

            if (Flatten.Utils.LT(dist, Math.abs(r1 - r2)))     // one circle is contained within another, no intersections
                return ip;

            // Normalize vector.
            vec.x /= dist;
            vec.y /= dist;

            let pt;

            // Case of touching from outside or from inside - single intersection point
            // TODO: check this specifically not sure if correct
            if (Flatten.Utils.EQ(dist, r1 + r2) || Flatten.Utils.EQ(dist, Math.abs(r1 - r2))) {
                pt = circle1.pc.translate(r1*vec.x, r1*vec.y);
                ip.push(pt);
                return ip;
            }

            // Case of two intersection points

            // Distance from first center to center of common chord:
            //   a = (r1^2 - r2^2 + d^2) / 2d
            // Separate for better accuracy
            let a = (r1*r1)/(2*dist) - (r2*r2)/(2*dist) + dist/2;

            let mid_pt = circle1.pc.translate(a*vec.x, a*vec.y);
            let h = Math.sqrt(r1*r1 - a*a);
            // let norm;

            // norm = vec.rotate90CCW().multiply(h);
            pt = mid_pt.translate(vec.rotate90CCW().multiply(h));
            ip.push(pt);

            // norm = vec.rotate90CW();
            pt = mid_pt.translate(vec.rotate90CW().multiply(h));
            ip.push(pt);

            return ip;
        }

        /**
         * Return string to draw circle in svg
         * @param {Object} attrs - an object with attributes of svg circle element,
         * like "stroke", "strokeWidth", "fill" <br/>
         * Defaults are stroke:"black", strokeWidth:"1", fill:"none"
         * @returns {string}
         */
        svg(attrs = {}) {
            let {stroke, strokeWidth, fill, fillOpacity, id, className} = attrs;
            // let rest_str = Object.keys(rest).reduce( (acc, key) => acc += ` ${key}="${rest[key]}"`, "");
            let id_str = (id && id.length > 0) ? `id="${id}"` : "";
            let class_str = (className && className.length > 0) ? `class="${className}"` : "";

            return `\n<circle cx="${this.pc.x}" cy="${this.pc.y}" r="${this.r}" stroke="${stroke || "black"}" stroke-width="${strokeWidth || 1}" fill="${fill || "none"}" fill-opacity="${fillOpacity || 1.0}" ${id_str} ${class_str} />`;
        }

        /**
         * This method returns an object that defines how data will be
         * serialized when called JSON.stringify() method
         * @returns {Object}
         */
        toJSON() {
            return Object.assign({},this,{name:"circle"});
        }
    };

    /**
     * Shortcut to create new circle
     * @param args
     */
    Flatten.circle = (...args) => new Flatten.Circle(...args);
};