romgrk-2d-geometry
Version:
Javascript library for 2d geometry
583 lines (503 loc) • 21.7 kB
text/typescript
import * as Utils from '../utils/utils'
import * as Intersection from '../algorithms/intersection';
import * as geom from '../classes'
import { PlanarSet } from '../data_structures/PlanarSet';
import { IntervalTree } from '../data_structures/interval-tree';
/**
* Calculate distance and shortest segment between points
* @param pt1
* @param pt2
* @returns {Number | Segment} - distance and shortest segment
*/
export function point2point(pt1, pt2): [number, geom.Segment] {
return pt1.distanceTo(pt2);
}
/**
* Calculate distance and shortest segment between point and line
*/
export function point2line(pt: geom.Point, line: geom.Line): [number, geom.Segment] {
let closest_point = pt.projectionOn(line);
let vec = new geom.Vector(pt, closest_point);
return [vec.length, new geom.Segment(pt, closest_point)];
}
/**
* Calculate distance and shortest segment between point and circle
*/
export function point2circle(pt: geom.Point, circle: geom.Circle): [number, geom.Segment] {
let [dist2center, shortest_dist] = pt.distanceTo(circle.center);
if (Utils.EQ_0(dist2center)) {
return [circle.r, new geom.Segment(pt, circle.toArc().start)];
} else {
let dist = Math.abs(dist2center - circle.r);
let v = new geom.Vector(circle.pc, pt).normalize().multiply(circle.r);
let closest_point = circle.pc.translate(v);
return [dist, new geom.Segment(pt, closest_point)];
}
}
/**
* Calculate distance and shortest segment between point and segment
*/
export function point2segment(pt: geom.Point, segment: geom.Segment): [number, geom.Segment] {
/* Degenerated case of zero-length segment */
if (segment.start.equalTo(segment.end)) {
return point2point(pt, segment.start);
}
let v_seg = new geom.Vector(segment.start, segment.end);
let v_ps2pt = new geom.Vector(segment.start, pt);
let v_pe2pt = new geom.Vector(segment.end, pt);
let start_sp = v_seg.dot(v_ps2pt);
/* dot product v_seg * v_ps2pt */
let end_sp = -v_seg.dot(v_pe2pt);
/* minus dot product v_seg * v_pe2pt */
let dist;
let closest_point;
if (Utils.GE(start_sp, 0) && Utils.GE(end_sp, 0)) { /* point inside segment scope */
let v_unit = segment.tangentInStart(); // new geom.Vector(v_seg.x / this.length, v_seg.y / this.length);
/* unit vector ||v_unit|| = 1 */
dist = Math.abs(v_unit.cross(v_ps2pt));
/* dist = abs(v_unit x v_ps2pt) */
closest_point = segment.start.translate(v_unit.multiply(v_unit.dot(v_ps2pt)));
return [dist, new geom.Segment(pt, closest_point)];
} else if (start_sp < 0) { /* point is out of scope closer to ps */
return pt.distanceTo(segment.start);
} else { /* point is out of scope closer to pe */
return pt.distanceTo(segment.end);
}
};
/**
* Calculate distance and shortest segment between point and arc
*/
export function point2arc(pt: geom.Point, arc: geom.Arc): [number, geom.Segment] {
let circle = new geom.Circle(arc.pc, arc.r);
let dist_and_segment = [];
let dist, shortest_segment;
[dist, shortest_segment] = point2circle(pt, circle);
if (shortest_segment.end.on(arc)) {
dist_and_segment.push(point2circle(pt, circle));
}
dist_and_segment.push(point2point(pt, arc.start));
dist_and_segment.push(point2point(pt, arc.end));
sort(dist_and_segment);
return dist_and_segment[0];
}
/**
* Calculate distance and shortest segment between segment and line
*/
export function segment2line(seg: geom.Segment, line: geom.Line): [number, geom.Segment] {
let ip = seg.intersect(line);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])]; // distance = 0, closest point is the first point
}
let dist_and_segment = [];
dist_and_segment.push(point2line(seg.start, line));
dist_and_segment.push(point2line(seg.end, line));
sort(dist_and_segment);
return dist_and_segment[0];
}
/**
* Calculate distance and shortest segment between two segments
* @param seg1
* @param seg2
* @returns {Number | Segment} - distance and shortest segment
*/
export function segment2segment(seg1, seg2): [number, geom.Segment] {
let ip = Intersection.intersectSegment2Segment(seg1, seg2);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])]; // distance = 0, closest point is the first point
}
// Seg1 and seg2 not intersected
let dist_and_segment = [];
let dist_tmp, shortest_segment_tmp
[dist_tmp, shortest_segment_tmp] = point2segment(seg2.start, seg1);
dist_and_segment.push([dist_tmp, shortest_segment_tmp.reverse()]);
[dist_tmp, shortest_segment_tmp] = point2segment(seg2.end, seg1);
dist_and_segment.push([dist_tmp, shortest_segment_tmp.reverse()]);
dist_and_segment.push(point2segment(seg1.start, seg2));
dist_and_segment.push(point2segment(seg1.end, seg2));
sort(dist_and_segment);
return dist_and_segment[0];
}
/**
* Calculate distance and shortest segment between segment and circle
* @param seg
* @param circle
* @returns {Number | Segment} - distance and shortest segment
*/
export function segment2circle(seg, circle): [number, geom.Segment] {
/* Case 1 Segment and circle intersected. Return the first point and zero distance */
let ip = seg.intersect(circle);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
// No intersection between segment and circle
/* Case 2. Distance to projection of center point to line bigger than radius
* And projection point belong to segment
* Then measure again distance from projection to circle and return it */
let line = new geom.Line(seg.ps, seg.pe);
let [dist, shortest_segment] = point2line(circle.center, line);
if (Utils.GE(dist, circle.r) && shortest_segment.end.on(seg)) {
return point2circle(shortest_segment.end, circle);
}
/* Case 3. Otherwise closest point is one of the end points of the segment */
else {
let [dist_from_start, shortest_segment_from_start] = point2circle(seg.start, circle);
let [dist_from_end, shortest_segment_from_end] = point2circle(seg.end, circle);
return Utils.LT(dist_from_start, dist_from_end) ?
[dist_from_start, shortest_segment_from_start] :
[dist_from_end, shortest_segment_from_end];
}
}
/**
* Calculate distance and shortest segment between segment and arc
* @param seg
* @param arc
* @returns {Number | Segment} - distance and shortest segment
*/
export function segment2arc(seg, arc): [number, geom.Segment] {
/* Case 1 Segment and arc intersected. Return the first point and zero distance */
let ip = seg.intersect(arc);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
// No intersection between segment and arc
let line = new geom.Line(seg.ps, seg.pe);
let circle = new geom.Circle(arc.pc, arc.r);
/* Case 2. Distance to projection of center point to line bigger than radius AND
* projection point belongs to segment AND
* distance from projection point to circle belongs to arc =>
* return this distance from projection to circle */
let [dist_from_center, shortest_segment_from_center] = point2line(circle.center, line);
if (Utils.GE(dist_from_center, circle.r) && shortest_segment_from_center.end.on(seg)) {
let [dist_from_projection, shortest_segment_from_projection] =
point2circle(shortest_segment_from_center.end, circle);
if (shortest_segment_from_projection.end.on(arc)) {
return [dist_from_projection, shortest_segment_from_projection];
}
}
/* Case 3. Otherwise closest point is one of the end points of the segment */
let dist_and_segment = [];
dist_and_segment.push(point2arc(seg.start, arc));
dist_and_segment.push(point2arc(seg.end, arc));
let dist_tmp, segment_tmp;
[dist_tmp, segment_tmp] = point2segment(arc.start, seg);
dist_and_segment.push([dist_tmp, segment_tmp.reverse()]);
[dist_tmp, segment_tmp] = point2segment(arc.end, seg);
dist_and_segment.push([dist_tmp, segment_tmp.reverse()]);
sort(dist_and_segment);
return dist_and_segment[0];
}
/**
* Calculate distance and shortest segment between two circles
* @param circle1
* @param circle2
* @returns {Number | Segment} - distance and shortest segment
*/
export function circle2circle(circle1, circle2): [number, geom.Segment] {
let ip = circle1.intersect(circle2);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
// Case 1. Concentric circles. Convert to arcs and take distance between two arc starts
if (circle1.center.equalTo(circle2.center)) {
let arc1 = circle1.toArc();
let arc2 = circle2.toArc();
return point2point(arc1.start, arc2.start);
} else {
// Case 2. Not concentric circles
let line = new geom.Line(circle1.center, circle2.center);
let ip1 = line.intersect(circle1);
let ip2 = line.intersect(circle2);
let dist_and_segment = [];
dist_and_segment.push(point2point(ip1[0], ip2[0]));
dist_and_segment.push(point2point(ip1[0], ip2[1]));
dist_and_segment.push(point2point(ip1[1], ip2[0]));
dist_and_segment.push(point2point(ip1[1], ip2[1]));
sort(dist_and_segment);
return dist_and_segment[0];
}
}
/**
* Calculate distance and shortest segment between two circles
* @param circle
* @param line
* @returns {Number | Segment} - distance and shortest segment
*/
export function circle2line(circle, line): [number, geom.Segment] {
let ip = circle.intersect(line);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
let [dist_from_center, shortest_segment_from_center] = point2line(circle.center, line);
let [dist, shortest_segment] = point2circle(shortest_segment_from_center.end, circle);
shortest_segment = shortest_segment.reverse();
return [dist, shortest_segment];
}
/**
* Calculate distance and shortest segment between arc and line
* @param arc
* @param line
* @returns {Number | Segment} - distance and shortest segment
*/
export function arc2line(arc, line): [number, geom.Segment] {
/* Case 1 Line and arc intersected. Return the first point and zero distance */
let ip = line.intersect(arc);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
let circle = new geom.Circle(arc.center, arc.r);
/* Case 2. Distance to projection of center point to line bigger than radius AND
* projection point belongs to segment AND
* distance from projection point to circle belongs to arc =>
* return this distance from projection to circle */
let [dist_from_center, shortest_segment_from_center] = point2line(circle.center, line);
if (Utils.GE(dist_from_center, circle.r)) {
let [dist_from_projection, shortest_segment_from_projection] =
point2circle(shortest_segment_from_center.end, circle);
if (shortest_segment_from_projection.end.on(arc)) {
return [dist_from_projection, shortest_segment_from_projection];
}
} else {
let dist_and_segment = [];
dist_and_segment.push(point2line(arc.start, line));
dist_and_segment.push(point2line(arc.end, line));
sort(dist_and_segment);
return dist_and_segment[0];
}
}
/**
* Calculate distance and shortest segment between arc and circle
* @param arc
* @param circle2
* @returns {Number | Segment} - distance and shortest segment
*/
export function arc2circle(arc, circle2): [number, geom.Segment] {
let ip = arc.intersect(circle2);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
let circle1 = new geom.Circle(arc.center, arc.r);
let [dist, shortest_segment] = circle2circle(circle1, circle2);
if (shortest_segment.start.on(arc)) {
return [dist, shortest_segment];
} else {
let dist_and_segment = [];
dist_and_segment.push(point2circle(arc.start, circle2));
dist_and_segment.push(point2circle(arc.end, circle2));
sort(dist_and_segment);
return dist_and_segment[0];
}
}
/**
* Calculate distance and shortest segment between two arcs
* @param arc1
* @param arc2
* @returns {Number | Segment} - distance and shortest segment
*/
export function arc2arc(arc1, arc2): [number, geom.Segment] {
let ip = arc1.intersect(arc2);
if (ip.length > 0) {
return [0, new geom.Segment(ip[0], ip[0])];
}
let circle1 = new geom.Circle(arc1.center, arc1.r);
let circle2 = new geom.Circle(arc2.center, arc2.r);
let [dist, shortest_segment] = circle2circle(circle1, circle2);
if (shortest_segment.start.on(arc1) && shortest_segment.end.on(arc2)) {
return [dist, shortest_segment];
} else {
let dist_and_segment = [];
let dist_tmp, segment_tmp;
[dist_tmp, segment_tmp] = point2arc(arc1.start, arc2);
if (segment_tmp.end.on(arc2)) {
dist_and_segment.push([dist_tmp, segment_tmp]);
}
[dist_tmp, segment_tmp] = point2arc(arc1.end, arc2);
if (segment_tmp.end.on(arc2)) {
dist_and_segment.push([dist_tmp, segment_tmp]);
}
[dist_tmp, segment_tmp] = point2arc(arc2.start, arc1);
if (segment_tmp.end.on(arc1)) {
dist_and_segment.push([dist_tmp, segment_tmp.reverse()]);
}
[dist_tmp, segment_tmp] = point2arc(arc2.end, arc1);
if (segment_tmp.end.on(arc1)) {
dist_and_segment.push([dist_tmp, segment_tmp.reverse()]);
}
[dist_tmp, segment_tmp] = point2point(arc1.start, arc2.start);
dist_and_segment.push([dist_tmp, segment_tmp]);
[dist_tmp, segment_tmp] = point2point(arc1.start, arc2.end);
dist_and_segment.push([dist_tmp, segment_tmp]);
[dist_tmp, segment_tmp] = point2point(arc1.end, arc2.start);
dist_and_segment.push([dist_tmp, segment_tmp]);
[dist_tmp, segment_tmp] = point2point(arc1.end, arc2.end);
dist_and_segment.push([dist_tmp, segment_tmp]);
sort(dist_and_segment);
return dist_and_segment[0];
}
}
/**
* Calculate distance and shortest segment between point and polygon
* @param point
* @param polygon
* @returns {Number | Segment} - distance and shortest segment
*/
export function point2polygon(point, polygon): [number, geom.Segment] {
let min_dist_and_segment = [Number.POSITIVE_INFINITY, new geom.Segment()] as [number, geom.Segment];
for (let edge of polygon.edges) {
let [dist, shortest_segment] = (edge.shape instanceof geom.Segment) ?
point2segment(point, edge.shape) : point2arc(point, edge.shape);
if (Utils.LT(dist, min_dist_and_segment[0])) {
min_dist_and_segment = [dist, shortest_segment] as [number, geom.Segment];
}
}
return min_dist_and_segment;
}
export function shape2polygon(shape, polygon): [number, geom.Segment] {
let min_dist_and_segment = [Number.POSITIVE_INFINITY, new geom.Segment()] as [number, geom.Segment];
for (let edge of polygon.edges) {
let [dist, shortest_segment] = shape.distanceTo(edge.shape);
if (Utils.LT(dist, min_dist_and_segment[0])) {
min_dist_and_segment = [dist, shortest_segment] as [number, geom.Segment];
}
}
return min_dist_and_segment;
}
/**
* Calculate distance and shortest segment between two polygons
* @param polygon1
* @param polygon2
* @returns {Number | Segment} - distance and shortest segment
*/
export function polygon2polygon(polygon1, polygon2): [number, geom.Segment] {
let min_dist_and_segment = [Number.POSITIVE_INFINITY, new geom.Segment()] as [number, geom.Segment];
for (let edge1 of polygon1.edges) {
for (let edge2 of polygon2.edges) {
let [dist, shortest_segment] = edge1.shape.distanceTo(edge2.shape);
if (Utils.LT(dist, min_dist_and_segment[0])) {
min_dist_and_segment = [dist, shortest_segment] as [number, geom.Segment];
}
}
}
return min_dist_and_segment;
}
/**
* Returns [mindist, maxdist] array of squared minimal and maximal distance between boxes
* Minimal distance by x is
* (box2.xmin - box1.xmax), if box1 is left to box2
* (box1.xmin - box2.xmax), if box2 is left to box1
* 0, if box1 and box2 are intersected by x
* Minimal distance by y is defined in the same way
*
* Maximal distance is estimated as a sum of squared dimensions of the merged box
*
* @param box1
* @param box2
*/
export function box2box_minmax(box1, box2) {
let mindist_x = Math.max(Math.max(box1.xmin - box2.xmax, 0), Math.max(box2.xmin - box1.xmax, 0));
let mindist_y = Math.max(Math.max(box1.ymin - box2.ymax, 0), Math.max(box2.ymin - box1.ymax, 0));
let mindist = mindist_x * mindist_x + mindist_y * mindist_y;
let box = box1.merge(box2);
let dx = box.xmax - box.xmin;
let dy = box.ymax - box.ymin;
let maxdist = dx * dx + dy * dy;
return [mindist, maxdist] as const;
}
export function minmax_tree_process_level(shape, level, min_stop, tree) {
// Calculate minmax distance to each shape in current level
// Insert result into the interval tree for further processing
// update min_stop with maxdist, it will be the new stop distance
let mindist, maxdist;
for (let node of level) {
// [mindist, maxdist] = box2box_minmax(shape.box, node.max);
// if (Utils.GT(mindist, min_stop))
// continue;
// Estimate min-max dist to the shape stored in the node.item, using node.item.key which is shape's box
[mindist, maxdist] = box2box_minmax(shape.box, node.item.key);
if (node.item.value instanceof geom.Edge) {
tree.insert([mindist, maxdist], node.item.value.shape);
} else {
tree.insert([mindist, maxdist], node.item.value);
}
if (Utils.LT(maxdist, min_stop)) {
min_stop = maxdist; // this will be the new distance estimation
}
}
if (level.length === 0)
return min_stop;
// Calculate new level from left and right children of the current
let new_level_left = level.map(node => node.left.isNil() ? undefined : node.left).filter(node => node !== undefined);
let new_level_right = level.map(node => node.right.isNil() ? undefined : node.right).filter(node => node !== undefined);
// Merge left and right subtrees and leave only relevant subtrees
let new_level = [...new_level_left, ...new_level_right].filter(node => {
// Node subtree quick reject, node.max is a subtree box
let [mindist, maxdist] = box2box_minmax(shape.box, node.max);
return (Utils.LE(mindist, min_stop));
});
min_stop = minmax_tree_process_level(shape, new_level, min_stop, tree);
return min_stop;
}
/**
* Calculates sorted tree of [mindist, maxdist] intervals between query shape
* and shapes of the planar set.
* @param shape
* @param set
*/
export function minmax_tree(shape, set, min_stop) {
let tree = new IntervalTree();
let level = [set.index.root];
let squared_min_stop = min_stop < Number.POSITIVE_INFINITY ? min_stop * min_stop : Number.POSITIVE_INFINITY;
squared_min_stop = minmax_tree_process_level(shape, level, squared_min_stop, tree);
return tree;
}
export function minmax_tree_calc_distance(shape, node, min_dist_and_segment) {
let min_dist_and_segment_new, stop;
if (node != null && !node.isNil()) {
[min_dist_and_segment_new, stop] = minmax_tree_calc_distance(shape, node.left, min_dist_and_segment);
if (stop) {
return [min_dist_and_segment_new, stop];
}
if (Utils.LT(min_dist_and_segment_new[0], Math.sqrt(node.item.key.low))) {
return [min_dist_and_segment_new, true]; // stop condition
}
let [dist, shortest_segment] = distance(shape, node.item.value);
// console.log(dist)
if (Utils.LT(dist, min_dist_and_segment_new[0])) {
min_dist_and_segment_new = [dist, shortest_segment];
}
[min_dist_and_segment_new, stop] = minmax_tree_calc_distance(shape, node.right, min_dist_and_segment_new);
return [min_dist_and_segment_new, stop];
}
return [min_dist_and_segment, false];
}
/**
* Calculates distance between shape and Planar Set of shapes
* @param shape
* @param {PlanarSet} set
* @param {Number} min_stop
*/
export function shape2planarSet(shape, set, min_stop = Number.POSITIVE_INFINITY) {
let min_dist_and_segment = [min_stop, new geom.Segment()] as const;
let stop = false;
if (set instanceof PlanarSet) {
let tree = minmax_tree(shape, set, min_stop);
[min_dist_and_segment, stop] = minmax_tree_calc_distance(shape, tree.root, min_dist_and_segment);
}
return min_dist_and_segment;
}
export function sort(dist_and_segment) {
dist_and_segment.sort((d1, d2) => {
if (Utils.LT(d1[0], d2[0])) {
return -1;
}
if (Utils.GT(d1[0], d2[0])) {
return 1;
}
return 0;
});
}
export function distance(shape1, shape2) {
return shape1.distanceTo(shape2);
}
export function pythagore(width: number, height: number) {
return Math.sqrt(width * width + height * height)
}