@joint/core
Version:
JavaScript diagramming library
248 lines (206 loc) • 11.2 kB
JavaScript
export function parsePoints(svgString) {
// Step 1: Discard surrounding spaces
const trimmedString = svgString.trim();
if (trimmedString === '') return [];
const points = [];
// Step 2: Split at commas (+ their surrounding spaces) or at multiple spaces
// ReDoS mitigation: Have an anchor at the beginning of each alternation
// Note: This doesn't simplify double (or more) commas - causes empty coords
// This regex is used by `split()`, so it doesn't need to use /g
const coords = trimmedString.split(/\b\s*,\s*|,\s*|\s+/);
const numCoords = coords.length;
for (let i = 0; i < numCoords; i += 2) {
// Step 3: Convert each coord to number
// Note: If the coord cannot be converted to a number, it will be `NaN`
// Note: If the coord is empty ("", e.g. from ",," input), it will be `0`
// Note: If we end up with an odd number of coords, the last point's second coord will be `NaN`
points.push({ x: +coords[i], y: +coords[i + 1] });
}
return points;
}
export function clonePoints(points) {
const numPoints = points.length;
if (numPoints === 0) return [];
const newPoints = [];
for (let i = 0; i < numPoints; i++) {
const point = points[i].clone();
newPoints.push(point);
}
return newPoints;
}
// Returns a convex-hull polyline from this polyline.
// Implements the Graham scan (https://en.wikipedia.org/wiki/Graham_scan).
// Output polyline starts at the first element of the original polyline that is on the hull, then continues clockwise.
// Minimal polyline is found (only vertices of the hull are reported, no collinear points).
export function convexHull(points) {
const { abs } = Math;
var i;
var n;
var numPoints = points.length;
if (numPoints === 0) return []; // if points array is empty
// step 1: find the starting point - point with the lowest y (if equality, highest x)
var startPoint;
for (i = 0; i < numPoints; i++) {
if (startPoint === undefined) {
// if this is the first point we see, set it as start point
startPoint = points[i];
} else if (points[i].y < startPoint.y) {
// start point should have lowest y from all points
startPoint = points[i];
} else if ((points[i].y === startPoint.y) && (points[i].x > startPoint.x)) {
// if two points have the lowest y, choose the one that has highest x
// there are no points to the right of startPoint - no ambiguity about theta 0
// if there are several coincident start point candidates, first one is reported
startPoint = points[i];
}
}
// step 2: sort the list of points
// sorting by angle between line from startPoint to point and the x-axis (theta)
// step 2a: create the point records = [point, originalIndex, angle]
var sortedPointRecords = [];
for (i = 0; i < numPoints; i++) {
var angle = startPoint.theta(points[i]);
if (angle === 0) {
angle = 360; // 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
}
var entry = [points[i], i, angle];
sortedPointRecords.push(entry);
}
// step 2b: sort the list in place
sortedPointRecords.sort(function(record1, record2) {
// returning a negative number here sorts record1 before record2
// if first angle is smaller than second, first angle should come before second
var sortOutput = record1[2] - record2[2]; // negative if first angle smaller
if (sortOutput === 0) {
// if the two angles are equal, sort by originalIndex
sortOutput = record2[1] - record1[1]; // negative if first index larger
// coincident points will be sorted in reverse-numerical order
// so the coincident points with lower original index will be considered first
}
return sortOutput;
});
// step 2c: duplicate start record from the top of the stack to the bottom of the stack
if (sortedPointRecords.length > 2) {
var startPointRecord = sortedPointRecords[sortedPointRecords.length - 1];
sortedPointRecords.unshift(startPointRecord);
}
// step 3a: go through sorted points in order and find those with right turns
// we want to get our results in clockwise order
var insidePoints = {}; // dictionary of points with left turns - cannot be on the hull
var hullPointRecords = []; // stack of records with right turns - hull point candidates
var currentPointRecord;
var currentPoint;
var lastHullPointRecord;
var lastHullPoint;
var secondLastHullPointRecord;
var secondLastHullPoint;
while (sortedPointRecords.length !== 0) {
currentPointRecord = sortedPointRecords.pop();
currentPoint = currentPointRecord[0];
// check if point has already been discarded
// keys for insidePoints are stored in the form 'point.x@point.y@@originalIndex'
if (insidePoints.hasOwnProperty(currentPointRecord[0] + '@@' + currentPointRecord[1])) {
// this point had an incorrect turn at some previous iteration of this loop
// this disqualifies it from possibly being on the hull
continue;
}
var correctTurnFound = false;
while (!correctTurnFound) {
if (hullPointRecords.length < 2) {
// not enough points for comparison, just add current point
hullPointRecords.push(currentPointRecord);
correctTurnFound = true;
} else {
lastHullPointRecord = hullPointRecords.pop();
lastHullPoint = lastHullPointRecord[0];
secondLastHullPointRecord = hullPointRecords.pop();
secondLastHullPoint = secondLastHullPointRecord[0];
var crossProduct = secondLastHullPoint.cross(lastHullPoint, currentPoint);
if (crossProduct < 0) {
// found a right turn
hullPointRecords.push(secondLastHullPointRecord);
hullPointRecords.push(lastHullPointRecord);
hullPointRecords.push(currentPointRecord);
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
var THRESHOLD = 1e-10; // we have to take rounding errors into account
var angleBetween = lastHullPoint.angleBetween(secondLastHullPoint, currentPoint);
if (abs(angleBetween - 180) < THRESHOLD) { // rounding 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[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
// reenter second-to-last hull point (will be last at next iter)
hullPointRecords.push(secondLastHullPointRecord);
// 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[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
// reenter second-to-last hull point (will be last at next iter)
hullPointRecords.push(secondLastHullPointRecord);
// do not do anything with current point
// correct turn not found
} else if (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)
hullPointRecords.push(secondLastHullPointRecord);
// put last hull point back into the sorted point records list
sortedPointRecords.push(lastHullPointRecord);
// 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[lastHullPointRecord[0] + '@@' + lastHullPointRecord[1]] = lastHullPoint;
// reenter second-to-last hull point (will be last at next iter of loop)
hullPointRecords.push(secondLastHullPointRecord);
// 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 (hullPointRecords.length > 2) {
hullPointRecords.pop();
}
// step 4: find the lowest originalIndex record and put it at the beginning of hull
var lowestHullIndex; // the lowest originalIndex on the hull
var indexOfLowestHullIndexRecord = -1; // the index of the record with lowestHullIndex
n = hullPointRecords.length;
for (i = 0; i < n; i++) {
var currentHullIndex = hullPointRecords[i][1];
if (lowestHullIndex === undefined || currentHullIndex < lowestHullIndex) {
lowestHullIndex = currentHullIndex;
indexOfLowestHullIndexRecord = i;
}
}
var hullPointRecordsReordered = [];
if (indexOfLowestHullIndexRecord > 0) {
var newFirstChunk = hullPointRecords.slice(indexOfLowestHullIndexRecord);
var newSecondChunk = hullPointRecords.slice(0, indexOfLowestHullIndexRecord);
hullPointRecordsReordered = newFirstChunk.concat(newSecondChunk);
} else {
hullPointRecordsReordered = hullPointRecords;
}
var hullPoints = [];
n = hullPointRecordsReordered.length;
for (i = 0; i < n; i++) {
hullPoints.push(hullPointRecordsReordered[i][0]);
}
return hullPoints;
}