533 lines
21 KiB
JavaScript
533 lines
21 KiB
JavaScript
"use strict";
|
|
/* eslint-disable no-constructor-return */
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Polyline = void 0;
|
|
const line_1 = require("./line");
|
|
const point_1 = require("./point");
|
|
const rectangle_1 = require("./rectangle");
|
|
const geometry_1 = require("./geometry");
|
|
class Polyline extends geometry_1.Geometry {
|
|
get start() {
|
|
return this.points[0] || null;
|
|
}
|
|
get end() {
|
|
return this.points[this.points.length - 1] || null;
|
|
}
|
|
constructor(points) {
|
|
super();
|
|
if (points != null) {
|
|
if (typeof points === 'string') {
|
|
return Polyline.parse(points);
|
|
}
|
|
this.points = points.map((p) => point_1.Point.create(p));
|
|
}
|
|
else {
|
|
this.points = [];
|
|
}
|
|
}
|
|
scale(sx, sy, origin = new point_1.Point()) {
|
|
this.points.forEach((p) => p.scale(sx, sy, origin));
|
|
return this;
|
|
}
|
|
rotate(angle, origin) {
|
|
this.points.forEach((p) => p.rotate(angle, origin));
|
|
return this;
|
|
}
|
|
translate(dx, dy) {
|
|
const t = point_1.Point.create(dx, dy);
|
|
this.points.forEach((p) => p.translate(t.x, t.y));
|
|
return this;
|
|
}
|
|
round(precision = 0) {
|
|
this.points.forEach((p) => p.round(precision));
|
|
return this;
|
|
}
|
|
bbox() {
|
|
if (this.points.length === 0) {
|
|
return new rectangle_1.Rectangle();
|
|
}
|
|
let x1 = Infinity;
|
|
let x2 = -Infinity;
|
|
let y1 = Infinity;
|
|
let y2 = -Infinity;
|
|
const points = this.points;
|
|
for (let i = 0, ii = points.length; i < ii; i += 1) {
|
|
const point = points[i];
|
|
const x = point.x;
|
|
const y = point.y;
|
|
if (x < x1)
|
|
x1 = x;
|
|
if (x > x2)
|
|
x2 = x;
|
|
if (y < y1)
|
|
y1 = y;
|
|
if (y > y2)
|
|
y2 = y;
|
|
}
|
|
return new rectangle_1.Rectangle(x1, y1, x2 - x1, y2 - y1);
|
|
}
|
|
closestPoint(p) {
|
|
const cpLength = this.closestPointLength(p);
|
|
return this.pointAtLength(cpLength);
|
|
}
|
|
closestPointLength(p) {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0 || count === 1) {
|
|
return 0;
|
|
}
|
|
let length = 0;
|
|
let cpLength = 0;
|
|
let minSqrDistance = Infinity;
|
|
for (let i = 0, ii = count - 1; i < ii; i += 1) {
|
|
const line = new line_1.Line(points[i], points[i + 1]);
|
|
const lineLength = line.length();
|
|
const cpNormalizedLength = line.closestPointNormalizedLength(p);
|
|
const cp = line.pointAt(cpNormalizedLength);
|
|
const sqrDistance = cp.squaredDistance(p);
|
|
if (sqrDistance < minSqrDistance) {
|
|
minSqrDistance = sqrDistance;
|
|
cpLength = length + cpNormalizedLength * lineLength;
|
|
}
|
|
length += lineLength;
|
|
}
|
|
return cpLength;
|
|
}
|
|
closestPointNormalizedLength(p) {
|
|
const length = this.length();
|
|
if (length === 0) {
|
|
return 0;
|
|
}
|
|
const cpLength = this.closestPointLength(p);
|
|
return cpLength / length;
|
|
}
|
|
closestPointTangent(p) {
|
|
const cpLength = this.closestPointLength(p);
|
|
return this.tangentAtLength(cpLength);
|
|
}
|
|
containsPoint(p) {
|
|
if (this.points.length === 0) {
|
|
return false;
|
|
}
|
|
const ref = point_1.Point.clone(p);
|
|
const x = ref.x;
|
|
const y = ref.y;
|
|
const points = this.points;
|
|
const count = points.length;
|
|
let startIndex = count - 1;
|
|
let intersectionCount = 0;
|
|
for (let endIndex = 0; endIndex < count; endIndex += 1) {
|
|
const start = points[startIndex];
|
|
const end = points[endIndex];
|
|
if (ref.equals(start)) {
|
|
return true;
|
|
}
|
|
const segment = new line_1.Line(start, end);
|
|
if (segment.containsPoint(p)) {
|
|
return true;
|
|
}
|
|
// do we have an intersection?
|
|
if ((y <= start.y && y > end.y) || (y > start.y && y <= end.y)) {
|
|
// this conditional branch IS NOT entered when `segment` is collinear/coincident with `ray`
|
|
// (when `y === start.y === end.y`)
|
|
// this conditional branch IS entered when `segment` touches `ray` at only one point
|
|
// (e.g. when `y === start.y !== end.y`)
|
|
// since this branch is entered again for the following segment, the two touches cancel out
|
|
const xDifference = start.x - x > end.x - x ? start.x - x : end.x - x;
|
|
if (xDifference >= 0) {
|
|
// segment lies at least partially to the right of `p`
|
|
const rayEnd = new point_1.Point(x + xDifference, y); // right
|
|
const ray = new line_1.Line(p, rayEnd);
|
|
if (segment.intersectsWithLine(ray)) {
|
|
// an intersection was detected to the right of `p`
|
|
intersectionCount += 1;
|
|
}
|
|
} // else: `segment` lies completely to the left of `p` (i.e. no intersection to the right)
|
|
}
|
|
// move to check the next polyline segment
|
|
startIndex = endIndex;
|
|
}
|
|
// returns `true` for odd numbers of intersections (even-odd algorithm)
|
|
return intersectionCount % 2 === 1;
|
|
}
|
|
intersectsWithLine(line) {
|
|
const intersections = [];
|
|
for (let i = 0, n = this.points.length - 1; i < n; i += 1) {
|
|
const a = this.points[i];
|
|
const b = this.points[i + 1];
|
|
const int = line.intersectsWithLine(new line_1.Line(a, b));
|
|
if (int) {
|
|
intersections.push(int);
|
|
}
|
|
}
|
|
return intersections.length > 0 ? intersections : null;
|
|
}
|
|
isDifferentiable() {
|
|
for (let i = 0, ii = this.points.length - 1; i < ii; i += 1) {
|
|
const a = this.points[i];
|
|
const b = this.points[i + 1];
|
|
const line = new line_1.Line(a, b);
|
|
if (line.isDifferentiable()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
length() {
|
|
let len = 0;
|
|
for (let i = 0, ii = this.points.length - 1; i < ii; i += 1) {
|
|
const a = this.points[i];
|
|
const b = this.points[i + 1];
|
|
len += a.distance(b);
|
|
}
|
|
return len;
|
|
}
|
|
pointAt(ratio) {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0) {
|
|
return null;
|
|
}
|
|
if (count === 1) {
|
|
return points[0].clone();
|
|
}
|
|
if (ratio <= 0) {
|
|
return points[0].clone();
|
|
}
|
|
if (ratio >= 1) {
|
|
return points[count - 1].clone();
|
|
}
|
|
const total = this.length();
|
|
const length = total * ratio;
|
|
return this.pointAtLength(length);
|
|
}
|
|
pointAtLength(length) {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0) {
|
|
return null;
|
|
}
|
|
if (count === 1) {
|
|
return points[0].clone();
|
|
}
|
|
let fromStart = true;
|
|
if (length < 0) {
|
|
fromStart = false;
|
|
length = -length; // eslint-disable-line
|
|
}
|
|
let tmp = 0;
|
|
for (let i = 0, ii = count - 1; i < ii; i += 1) {
|
|
const index = fromStart ? i : ii - 1 - i;
|
|
const a = points[index];
|
|
const b = points[index + 1];
|
|
const l = new line_1.Line(a, b);
|
|
const d = a.distance(b);
|
|
if (length <= tmp + d) {
|
|
return l.pointAtLength((fromStart ? 1 : -1) * (length - tmp));
|
|
}
|
|
tmp += d;
|
|
}
|
|
const lastPoint = fromStart ? points[count - 1] : points[0];
|
|
return lastPoint.clone();
|
|
}
|
|
tangentAt(ratio) {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0 || count === 1) {
|
|
return null;
|
|
}
|
|
if (ratio < 0) {
|
|
ratio = 0; // eslint-disable-line
|
|
}
|
|
if (ratio > 1) {
|
|
ratio = 1; // eslint-disable-line
|
|
}
|
|
const total = this.length();
|
|
const length = total * ratio;
|
|
return this.tangentAtLength(length);
|
|
}
|
|
tangentAtLength(length) {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0 || count === 1) {
|
|
return null;
|
|
}
|
|
let fromStart = true;
|
|
if (length < 0) {
|
|
fromStart = false;
|
|
length = -length; // eslint-disable-line
|
|
}
|
|
let lastValidLine;
|
|
let tmp = 0;
|
|
for (let i = 0, ii = count - 1; i < ii; i += 1) {
|
|
const index = fromStart ? i : ii - 1 - i;
|
|
const a = points[index];
|
|
const b = points[index + 1];
|
|
const l = new line_1.Line(a, b);
|
|
const d = a.distance(b);
|
|
if (l.isDifferentiable()) {
|
|
// has a tangent line (line length is not 0)
|
|
if (length <= tmp + d) {
|
|
return l.tangentAtLength((fromStart ? 1 : -1) * (length - tmp));
|
|
}
|
|
lastValidLine = l;
|
|
}
|
|
tmp += d;
|
|
}
|
|
if (lastValidLine) {
|
|
const ratio = fromStart ? 1 : 0;
|
|
return lastValidLine.tangentAt(ratio);
|
|
}
|
|
return null;
|
|
}
|
|
simplify(
|
|
// TODO: Accept startIndex and endIndex to specify where to start and end simplification
|
|
options = {}) {
|
|
const points = this.points;
|
|
// we need at least 3 points
|
|
if (points.length < 3) {
|
|
return this;
|
|
}
|
|
const threshold = options.threshold || 0;
|
|
// start at the beginning of the polyline and go forward
|
|
let currentIndex = 0;
|
|
// we need at least one intermediate point (3 points) in every iteration
|
|
// as soon as that stops being true, we know we reached the end of the polyline
|
|
while (points[currentIndex + 2]) {
|
|
const firstIndex = currentIndex;
|
|
const middleIndex = currentIndex + 1;
|
|
const lastIndex = currentIndex + 2;
|
|
const firstPoint = points[firstIndex];
|
|
const middlePoint = points[middleIndex];
|
|
const lastPoint = points[lastIndex];
|
|
const chord = new line_1.Line(firstPoint, lastPoint); // = connection between first and last point
|
|
const closestPoint = chord.closestPoint(middlePoint); // = closest point on chord from middle point
|
|
const closestPointDistance = closestPoint.distance(middlePoint);
|
|
if (closestPointDistance <= threshold) {
|
|
// middle point is close enough to the chord = simplify
|
|
// 1) remove middle point:
|
|
points.splice(middleIndex, 1);
|
|
// 2) in next iteration, investigate the newly-created triplet of points
|
|
// - do not change `currentIndex`
|
|
// = (first point stays, point after removed point becomes middle point)
|
|
}
|
|
else {
|
|
// middle point is far from the chord
|
|
// 1) preserve middle point
|
|
// 2) in next iteration, move `currentIndex` by one step:
|
|
currentIndex += 1;
|
|
// = (point after first point becomes first point)
|
|
}
|
|
}
|
|
// `points` array was modified in-place
|
|
return this;
|
|
}
|
|
toHull() {
|
|
const points = this.points;
|
|
const count = points.length;
|
|
if (count === 0) {
|
|
return new Polyline();
|
|
}
|
|
// Step 1: find the starting point -- point with
|
|
// the lowest y (if equality, highest x).
|
|
let startPoint = points[0];
|
|
for (let i = 1; i < count; i += 1) {
|
|
if (points[i].y < startPoint.y) {
|
|
startPoint = points[i];
|
|
}
|
|
else if (points[i].y === startPoint.y && points[i].x > startPoint.x) {
|
|
startPoint = points[i];
|
|
}
|
|
}
|
|
// Step 2: sort the list of points by angle between line
|
|
// from start point to current point and the x-axis (theta).
|
|
// Step 2a: create the point records = [point, originalIndex, angle]
|
|
const sortedRecords = [];
|
|
for (let i = 0; i < count; i += 1) {
|
|
let angle = startPoint.theta(points[i]);
|
|
if (angle === 0) {
|
|
// Give highest angle to start point.
|
|
// The start point will end up at end of sorted list.
|
|
// The start point will end up at beginning of hull points list.
|
|
angle = 360;
|
|
}
|
|
sortedRecords.push([points[i], i, angle]);
|
|
}
|
|
// Step 2b: sort the list in place
|
|
sortedRecords.sort((record1, record2) => {
|
|
let ret = record1[2] - record2[2];
|
|
if (ret === 0) {
|
|
ret = record2[1] - record1[1];
|
|
}
|
|
return ret;
|
|
});
|
|
// Step 2c: duplicate start record from the top of
|
|
// the stack to the bottom of the stack.
|
|
if (sortedRecords.length > 2) {
|
|
const startPoint = sortedRecords[sortedRecords.length - 1];
|
|
sortedRecords.unshift(startPoint);
|
|
}
|
|
// Step 3
|
|
// ------
|
|
// Step 3a: go through sorted points in order and find those with
|
|
// right turns, and we want to get our results in clockwise order.
|
|
// Dictionary of points with left turns - cannot be on the hull.
|
|
const insidePoints = {};
|
|
// Stack of records with right turns - hull point candidates.
|
|
const hullRecords = [];
|
|
const getKey = (record) => `${record[0].toString()}@${record[1]}`;
|
|
while (sortedRecords.length !== 0) {
|
|
const currentRecord = sortedRecords.pop();
|
|
const currentPoint = currentRecord[0];
|
|
// Check if point has already been discarded.
|
|
if (insidePoints[getKey(currentRecord)]) {
|
|
continue;
|
|
}
|
|
let correctTurnFound = false;
|
|
while (!correctTurnFound) {
|
|
if (hullRecords.length < 2) {
|
|
// Not enough points for comparison, just add current point.
|
|
hullRecords.push(currentRecord);
|
|
correctTurnFound = true;
|
|
}
|
|
else {
|
|
const lastHullRecord = hullRecords.pop();
|
|
const lastHullPoint = lastHullRecord[0];
|
|
const secondLastHullRecord = hullRecords.pop();
|
|
const secondLastHullPoint = secondLastHullRecord[0];
|
|
const crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint);
|
|
if (crossProduct < 0) {
|
|
// Found a right turn.
|
|
hullRecords.push(secondLastHullRecord);
|
|
hullRecords.push(lastHullRecord);
|
|
hullRecords.push(currentRecord);
|
|
correctTurnFound = true;
|
|
}
|
|
else if (crossProduct === 0) {
|
|
// the three points are collinear
|
|
// three options:
|
|
// there may be a 180 or 0 degree angle at lastHullPoint
|
|
// or two of the three points are coincident
|
|
// we have to take rounding errors into account
|
|
const THRESHOLD = 1e-10;
|
|
const angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint);
|
|
if (Math.abs(angleBetween - 180) < THRESHOLD) {
|
|
// rouding around 180 to 180
|
|
// if the cross product is 0 because the angle is 180 degrees
|
|
// discard last hull point (add to insidePoints)
|
|
// insidePoints.unshift(lastHullPoint);
|
|
insidePoints[getKey(lastHullRecord)] = lastHullPoint;
|
|
// reenter second-to-last hull point (will be last at next iter)
|
|
hullRecords.push(secondLastHullRecord);
|
|
// do not do anything with current point
|
|
// correct turn not found
|
|
}
|
|
else if (lastHullPoint.equals(currentPoint) ||
|
|
secondLastHullPoint.equals(lastHullPoint)) {
|
|
// if the cross product is 0 because two points are the same
|
|
// discard last hull point (add to insidePoints)
|
|
// insidePoints.unshift(lastHullPoint);
|
|
insidePoints[getKey(lastHullRecord)] = lastHullPoint;
|
|
// reenter second-to-last hull point (will be last at next iter)
|
|
hullRecords.push(secondLastHullRecord);
|
|
// do not do anything with current point
|
|
// correct turn not found
|
|
}
|
|
else if (Math.abs(((angleBetween + 1) % 360) - 1) < THRESHOLD) {
|
|
// rounding around 0 and 360 to 0
|
|
// if the cross product is 0 because the angle is 0 degrees
|
|
// remove last hull point from hull BUT do not discard it
|
|
// reenter second-to-last hull point (will be last at next iter)
|
|
hullRecords.push(secondLastHullRecord);
|
|
// put last hull point back into the sorted point records list
|
|
sortedRecords.push(lastHullRecord);
|
|
// we are switching the order of the 0deg and 180deg points
|
|
// correct turn not found
|
|
}
|
|
}
|
|
else {
|
|
// found a left turn
|
|
// discard last hull point (add to insidePoints)
|
|
// insidePoints.unshift(lastHullPoint);
|
|
insidePoints[getKey(lastHullRecord)] = lastHullPoint;
|
|
// reenter second-to-last hull point (will be last at next iter of loop)
|
|
hullRecords.push(secondLastHullRecord);
|
|
// do not do anything with current point
|
|
// correct turn not found
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// At this point, hullPointRecords contains the output points in clockwise order
|
|
// the points start with lowest-y,highest-x startPoint, and end at the same point
|
|
// Step 3b: remove duplicated startPointRecord from the end of the array
|
|
if (hullRecords.length > 2) {
|
|
hullRecords.pop();
|
|
}
|
|
// Step 4: find the lowest originalIndex record and put it at the beginning of hull
|
|
let lowestHullIndex; // the lowest originalIndex on the hull
|
|
let indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex
|
|
for (let i = 0, n = hullRecords.length; i < n; i += 1) {
|
|
const currentHullIndex = hullRecords[i][1];
|
|
if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) {
|
|
lowestHullIndex = currentHullIndex;
|
|
indexOfLowestHullIndexRecord = i;
|
|
}
|
|
}
|
|
let hullPointRecordsReordered = [];
|
|
if (indexOfLowestHullIndexRecord > 0) {
|
|
const newFirstChunk = hullRecords.slice(indexOfLowestHullIndexRecord);
|
|
const newSecondChunk = hullRecords.slice(0, indexOfLowestHullIndexRecord);
|
|
hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk);
|
|
}
|
|
else {
|
|
hullPointRecordsReordered = hullRecords;
|
|
}
|
|
const hullPoints = [];
|
|
for (let i = 0, n = hullPointRecordsReordered.length; i < n; i += 1) {
|
|
hullPoints.push(hullPointRecordsReordered[i][0]);
|
|
}
|
|
return new Polyline(hullPoints);
|
|
}
|
|
equals(p) {
|
|
if (p == null) {
|
|
return false;
|
|
}
|
|
if (p.points.length !== this.points.length) {
|
|
return false;
|
|
}
|
|
return p.points.every((a, i) => a.equals(this.points[i]));
|
|
}
|
|
clone() {
|
|
return new Polyline(this.points.map((p) => p.clone()));
|
|
}
|
|
toJSON() {
|
|
return this.points.map((p) => p.toJSON());
|
|
}
|
|
serialize() {
|
|
return this.points.map((p) => `${p.serialize()}`).join(' ');
|
|
}
|
|
}
|
|
exports.Polyline = Polyline;
|
|
(function (Polyline) {
|
|
function isPolyline(instance) {
|
|
return instance != null && instance instanceof Polyline;
|
|
}
|
|
Polyline.isPolyline = isPolyline;
|
|
})(Polyline = exports.Polyline || (exports.Polyline = {}));
|
|
(function (Polyline) {
|
|
function parse(svgString) {
|
|
const str = svgString.trim();
|
|
if (str === '') {
|
|
return new Polyline();
|
|
}
|
|
const points = [];
|
|
const coords = str.split(/\s*,\s*|\s+/);
|
|
for (let i = 0, ii = coords.length; i < ii; i += 2) {
|
|
points.push({ x: +coords[i], y: +coords[i + 1] });
|
|
}
|
|
return new Polyline(points);
|
|
}
|
|
Polyline.parse = parse;
|
|
})(Polyline = exports.Polyline || (exports.Polyline = {}));
|
|
//# sourceMappingURL=polyline.js.map
|