flatten-js
Version:
Javascript library for 2d geometry
331 lines (283 loc) • 11.4 kB
JavaScript
/**
* Created by Alex Bol on 2/20/2017.
*/
"use strict";
module.exports = function(Flatten) {
/**
* Class representing a line
* @type {Line}
*/
Flatten.Line = class Line {
/**
* Line may be constructed by point and normal vector or by two points that a line passes through
* @param {Point} pt - point that a line passes through
* @param {Vector|Point} norm - normal vector to a line or second point a line passes through
*/
constructor(...args) {
/**
* Point a line passes through
* @type {Point}
*/
this.pt = new Flatten.Point();
/**
* Normal vector to a line <br/>
* Vector is normalized (length == 1)
* @type {Vector}
*/
this.norm = new Flatten.Vector(0,1);
if (args.length == 0) {
return;
}
if (args.length == 1 && args[0] instanceof Object && args[0].name === "line") {
let {pt, norm} = args[0];
this.pt = new Flatten.Point(pt);
this.norm = new Flatten.Vector(norm);
return;
}
if (args.length == 2) {
let a1 = args[0];
let a2 = args[1];
if (a1 instanceof Flatten.Point && a2 instanceof Flatten.Point) {
this.pt = a1;
this.norm = Line.points2norm(a1, a2);
return;
}
if (a1 instanceof Flatten.Point && a2 instanceof Flatten.Vector) {
if (Flatten.Utils.EQ_0(a2.x) && Flatten.Utils.EQ_0(a2.y)) {
throw Flatten.Errors.ILLEGAL_PARAMETERS;
}
this.pt = a1.clone();
this.norm = a2.clone();
this.norm = this.norm.normalize();
return;
}
if (a1 instanceof Flatten.Vector && a2 instanceof Flatten.Point) {
if (Flatten.Utils.EQ_0(a1.x) && Flatten.Utils.EQ_0(a1.y)) {
throw Flatten.Errors.ILLEGAL_PARAMETERS;
}
this.pt = a2.clone();
this.norm = a1.clone();
this.norm = this.norm.normalize();
return;
}
}
throw Flatten.Errors.ILLEGAL_PARAMETERS;
}
/**
* Returns cloned new instance of a line
* @returns {Line}
*/
clone() {
return new Flatten.Line(this.pt, this.norm);
}
/**
* Slope of the line - angle in radians between line and axe x from 0 to 2PI
* @returns {number} - slope of the line
*/
get slope() {
let vec = new Flatten.Vector(this.norm.y, -this.norm.x);
return vec.slope;
}
/**
* Get coefficients [A,B,C] of a standard line equation in the form Ax + By = C
* @code [A, B, C] = line.standard
* @returns {number[]} - array of coefficients
*/
get standard() {
let A = this.norm.x;
let B = this.norm.y;
let C = this.norm.dot(this.pt);
return [A,B,C];
}
/**
* Return true if parallel or incident to other line
* @param {Line} other_line - line to check
* @returns {boolean}
*/
parallelTo(other_line) {
return Flatten.Utils.EQ_0(this.norm.cross(other_line.norm));
}
/**
* Returns true if incident to other line
* @param {Line} other_line - line to check
* @returns {boolean}
*/
incidentTo(other_line) {
return this.parallelTo(other_line) && this.pt.on(other_line);
}
/**
* Returns true if point belongs to line
* @param {Point} pt Query point
* @returns {boolean}
*/
contains(pt) {
if (this.pt.equalTo(pt)) {
return true;
}
/* Line contains point if vector to point is orthogonal to the line normal vector */
let vec = new Flatten.Vector(this.pt, pt);
return Flatten.Utils.EQ_0(this.norm.dot(vec));
}
/**
* Returns array of intersection points
* @param {Shape} shape - shape to intersect with
* @returns {Point[]}
*/
intersect(shape) {
if (shape instanceof Flatten.Point) {
return this.contains(shape) ? [shape] : [];
}
if (shape instanceof Flatten.Line) {
return Line.intersectLine2Line(this, shape);
}
if (shape instanceof Flatten.Circle) {
return Line.intersectLine2Circle(this, shape);
}
if (shape instanceof Flatten.Segment) {
return shape.intersect(this);
}
if (shape instanceof Flatten.Arc) {
return Line.intersectLine2Arc(this, shape);
}
if (shape instanceof Flatten.Polygon) {
return Flatten.Polygon.intersectLine2Polygon(this, shape);
}
}
/**
* Calculate distance and shortest segment from line to shape and returns array [distance, shortest_segment]
* @param {Shape} shape Shape of the one of the types Point, Circle, Segment, Arc, Polygon
* @returns {Number}
* @returns {Segment}
*/
distanceTo(shape) {
let {Distance} = Flatten;
if (shape instanceof Flatten.Point) {
let [distance, shortest_segment] = Distance.point2line(shape, this);
shortest_segment = shortest_segment.reverse();
return [distance, shortest_segment];
}
if (shape instanceof Flatten.Circle) {
let [distance, shortest_segment] = Distance.circle2line(shape, this);
shortest_segment = shortest_segment.reverse();
return [distance, shortest_segment];
}
if (shape instanceof Flatten.Segment) {
let [distance, shortest_segment] = Distance.segment2line(shape, this);
return [distance, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Arc) {
let [distance, shortest_segment] = Distance.arc2line(shape, this);
return [distance, shortest_segment.reverse()];
}
if (shape instanceof Flatten.Polygon) {
let [distance, shortest_segment] = Distance.shape2polygon(this, shape);
return [distance, shortest_segment];
}
}
/**
* Return string to draw svg segment representing line inside given box
* @param {Box} box Box representing drawing area
* @param {Object} attrs - an object with attributes of svg circle element
*/
svg(box, attrs = {}) {
let ip = Line.intersectLine2Box(this, box);
if (ip.length === 0)
return "";
let ps = ip[0];
let pe = ip.length == 2 ? ip[1] : ip.find( pt => !pt.equalTo(ps) );
if (pe === undefined) pe = ps;
let segment = new Flatten.Segment(ps, pe);
return segment.svg(attrs);
}
/**
* 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:"line"});
}
static points2norm(pt1, pt2) {
if (pt1.equalTo(pt2)) {
throw Flatten.Errors.ILLEGAL_PARAMETERS;
}
let vec = new Flatten.Vector(pt1, pt2);
let unit = vec.normalize();
return unit.rotate90CCW();
}
static intersectLine2Line(line1, line2) {
let ip = [];
let [A1, B1, C1] = line1.standard;
let [A2, B2, C2] = line2.standard;
/* Cramer's rule */
let det = A1*B2 - B1*A2;
let detX = C1*B2 - B1*C2;
let detY = A1*C2 - C1*A2;
if (!Flatten.Utils.EQ_0(det)) {
let new_ip = new Flatten.Point( detX/det, detY/det );
ip.push(new_ip);
}
return ip;
}
static intersectLine2Circle(line, circle) {
let ip = [];
let prj = circle.pc.projectionOn(line); // projection of circle center on line
let dist = circle.pc.distanceTo(prj)[0]; // distance from circle center to projection
if (Flatten.Utils.EQ(dist, circle.r)) { // line tangent to circle - return single intersection point
ip.push(prj);
}
else if (Flatten.Utils.LT(dist, circle.r)) { // return two intersection points
let delta = Math.sqrt(circle.r*circle.r - dist*dist);
let v_trans, pt;
v_trans = line.norm.rotate90CCW().multiply(delta);
pt = prj.translate(v_trans);
ip.push(pt);
v_trans = line.norm.rotate90CW().multiply(delta);
pt = prj.translate(v_trans);
ip.push(pt);
}
return ip;
}
static intersectLine2Box(line, box) {
let pts = [
new Flatten.Point(box.xmin, box.ymin),
new Flatten.Point(box.xmax, box.ymin),
new Flatten.Point(box.xmax, box.ymax),
new Flatten.Point(box.xmin, box.ymax)
];
let segs = [
new Flatten.Segment(pts[0], pts[1]),
new Flatten.Segment(pts[1], pts[2]),
new Flatten.Segment(pts[2], pts[3]),
new Flatten.Segment(pts[3], pts[0])
];
let ips = [];
for(let seg of segs) {
let ips_tmp = seg.intersect(line);
for (let ip of ips_tmp) {
ips.push(ip);
}
};
return ips;
}
static intersectLine2Arc(line, arc) {
let ip = [];
if (Line.intersectLine2Box(line, arc.box).length == 0) {
return ip;
}
let circle = new Flatten.Circle(arc.pc, arc.r);
let ip_tmp = line.intersect(circle);
for (let pt of ip_tmp) {
if (pt.on(arc)) {
ip.push(pt);
}
}
return ip;
}
};
/**
* Function to create line equivalent to "new" constructor
* @param args
*/
Flatten.line = (...args) => new Flatten.Line(...args);
};