You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
15 KiB
472 lines
15 KiB
4 years ago
|
import { Vector3, Vector2 } from 'three';
|
||
|
import { Math as THREEMath } from 'three';
|
||
|
import { checkIntersection } from 'line-intersect';
|
||
|
|
||
|
export class Utils {
|
||
|
/** Determines the distance of a point from a line.
|
||
|
* @param point The Point coordinates as THREE.Vector2
|
||
|
* @param start The starting coordinates of the line as THREE.Vector2
|
||
|
* @param end The ending coordinates of the line as THREE.Vector2
|
||
|
* @returns The distance value (number).
|
||
|
*/
|
||
|
static pointDistanceFromLine(point, start, end) {
|
||
|
var tPoint = Utils.closestPointOnLine(point, start, end);
|
||
|
var tDx = point.x - tPoint.x;
|
||
|
var tDy = point.y - tPoint.y;
|
||
|
return Math.sqrt(tDx * tDx + tDy * tDy);
|
||
|
}
|
||
|
|
||
|
/** Gets the projection of a point onto a line.
|
||
|
* @param point the point
|
||
|
* @param start the starting coordinates of the line as THREE.Vector2
|
||
|
* @param end the ending coordinates of the line as THREE.Vector2
|
||
|
* @returns The point as THREE.Vector2.
|
||
|
*/
|
||
|
static closestPointOnLine(point, start, end) {
|
||
|
// Inspired by: http://stackoverflow.com/a/6853926
|
||
|
var tA = point.x - start.x;
|
||
|
var tB = point.y - start.y;
|
||
|
var tC = end.x - start.x;
|
||
|
var tD = end.y - start.y;
|
||
|
|
||
|
var tDot = tA * tC + tB * tD;
|
||
|
var tLenSq = tC * tC + tD * tD;
|
||
|
var tParam = tDot / tLenSq;
|
||
|
|
||
|
var tXx, tYy;
|
||
|
|
||
|
if (tParam < 0 || (start.x === end.x && start.y === end.y)) {
|
||
|
tXx = start.x;
|
||
|
tYy = start.y;
|
||
|
} else if (tParam > 1) {
|
||
|
tXx = end.x;
|
||
|
tYy = end.y;
|
||
|
} else {
|
||
|
tXx = start.x + tParam * tC;
|
||
|
tYy = start.y + tParam * tD;
|
||
|
}
|
||
|
|
||
|
return new Vector2(tXx, tYy);
|
||
|
}
|
||
|
|
||
|
/** Gets the distance of two points.
|
||
|
* @param start the starting coordinate of the line as Vector2
|
||
|
* @param end the ending coordinate of the line as Vector2
|
||
|
* @returns The distance.
|
||
|
*/
|
||
|
static distance(start, end) {
|
||
|
return Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
|
||
|
}
|
||
|
|
||
|
/** Gets the angle between point1 -> start and 0,0 -> point2 (-pi to pi)
|
||
|
* @returns The angle.
|
||
|
*/
|
||
|
static angle(start, end) {
|
||
|
var tDot = start.x * end.x + start.y * end.y;
|
||
|
var tDet = start.x * end.y - start.y * end.x;
|
||
|
var tAngle = -Math.atan2(tDet, tDot);
|
||
|
return tAngle;
|
||
|
}
|
||
|
|
||
|
/** shifts angle to be 0 to 2pi */
|
||
|
static angle2pi(start, end) {
|
||
|
var tTheta = Utils.angle(start, end);
|
||
|
if (tTheta < 0) {
|
||
|
tTheta += 2.0 * Math.PI;
|
||
|
}
|
||
|
return tTheta;
|
||
|
}
|
||
|
|
||
|
/** shifts angle to be 0 to 2pi */
|
||
|
static getCyclicOrder(points, start = undefined) {
|
||
|
if (!start) {
|
||
|
start = new Vector2(0, 0);
|
||
|
}
|
||
|
var angles = [];
|
||
|
for (var i = 0; i < points.length; i++) {
|
||
|
var point = points[i];
|
||
|
var vect = point.clone().sub(start);
|
||
|
var radians = Math.atan2(vect.y, vect.x);
|
||
|
var degrees = THREEMath.radToDeg(radians);
|
||
|
degrees = (degrees > 0) ? degrees : (degrees + 360) % 360;
|
||
|
angles.push(degrees);
|
||
|
}
|
||
|
var indices = Utils.argsort(angles);
|
||
|
var sortedAngles = [];
|
||
|
var sortedPoints = [];
|
||
|
for (i = 0; i < indices.length; i++) {
|
||
|
sortedAngles.push(angles[indices[i]]);
|
||
|
sortedPoints.push(points[indices[i]]);
|
||
|
}
|
||
|
return { indices: indices, angles: sortedAngles, points: sortedPoints };
|
||
|
}
|
||
|
|
||
|
static argsort(numericalValues, direction = 1) {
|
||
|
var indices = Array.from(new Array(numericalValues.length), (val, index) => index);
|
||
|
return indices
|
||
|
.map((item, index) => [numericalValues[index], item]) // add the clickCount to sort by
|
||
|
.sort(([count1], [count2]) => (count1 - count2) * direction) // sort by the clickCount data
|
||
|
.map(([, item]) => item); // extract the sorted items
|
||
|
}
|
||
|
|
||
|
/** Checks if an array of points is clockwise.
|
||
|
* @param points Is array of points with x,y attributes
|
||
|
* @returns True if clockwise.
|
||
|
*/
|
||
|
static isClockwise(points) {
|
||
|
// make positive
|
||
|
let tSubX = Math.min(0, Math.min.apply(null, Utils.map(points, function(p) {
|
||
|
return p.x;
|
||
|
})));
|
||
|
let tSubY = Math.min(0, Math.min.apply(null, Utils.map(points, function(p) {
|
||
|
return p.x;
|
||
|
})));
|
||
|
|
||
|
var tNewPoints = Utils.map(points, function(p) {
|
||
|
return {
|
||
|
x: p.x - tSubX,
|
||
|
y: p.y - tSubY
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// determine CW/CCW, based on:
|
||
|
// http://stackoverflow.com/questions/1165647
|
||
|
var tSum = 0;
|
||
|
for (var tI = 0; tI < tNewPoints.length; tI++) {
|
||
|
var tC1 = tNewPoints[tI];
|
||
|
var tC2;
|
||
|
if (tI === tNewPoints.length - 1) {
|
||
|
tC2 = tNewPoints[0];
|
||
|
} else {
|
||
|
tC2 = tNewPoints[tI + 1];
|
||
|
}
|
||
|
tSum += (tC2.x - tC1.x) * (tC2.y + tC1.y);
|
||
|
}
|
||
|
return (tSum >= 0);
|
||
|
}
|
||
|
|
||
|
/** Creates a Guide.
|
||
|
* @returns A new Guide.
|
||
|
*/
|
||
|
static guide() {
|
||
|
var tS4 = function() {
|
||
|
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||
|
};
|
||
|
return tS4() + tS4() + '-' + tS4() + '-' + tS4() + '-' + tS4() + '-' + tS4() + tS4() + tS4();
|
||
|
}
|
||
|
|
||
|
/** both arguments are arrays of corners with x,y attributes */
|
||
|
static polygonPolygonIntersect(firstCorners, secondCorners) {
|
||
|
for (var tI = 0; tI < firstCorners.length; tI++) {
|
||
|
var tFirstCorner = firstCorners[tI],
|
||
|
tSecondCorner;
|
||
|
if (tI === firstCorners.length - 1) {
|
||
|
tSecondCorner = firstCorners[0];
|
||
|
} else {
|
||
|
tSecondCorner = firstCorners[tI + 1];
|
||
|
}
|
||
|
if (Utils.linePolygonIntersect(tFirstCorner.x, tFirstCorner.y, tSecondCorner.x, tSecondCorner.y, secondCorners)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** Corners is an array of points with x,y attributes */
|
||
|
static linePolygonIntersect(point, point2, corners) {
|
||
|
for (var tI = 0; tI < corners.length; tI++) {
|
||
|
var tFirstCorner = corners[tI],
|
||
|
tSecondCorner;
|
||
|
if (tI === corners.length - 1) {
|
||
|
tSecondCorner = corners[0];
|
||
|
} else {
|
||
|
tSecondCorner = corners[tI + 1];
|
||
|
}
|
||
|
if (Utils.lineLineIntersect(point, point2, { x: tFirstCorner.x, y: tFirstCorner.y }, { x: tSecondCorner.x, y: tSecondCorner.y })) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** */
|
||
|
static lineLineIntersectPoint(aStart, aEnd, bStart, bEnd) {
|
||
|
var result = checkIntersection(aStart.x, aStart.y, aEnd.x, aEnd.y, bStart.x, bStart.y, bEnd.x, bEnd.y);
|
||
|
if (result.point) {
|
||
|
return new Vector2(result.point.x, result.point.y);
|
||
|
}
|
||
|
return undefined;
|
||
|
|
||
|
}
|
||
|
|
||
|
/** */
|
||
|
static lineLineIntersect(lineAStart, lineAEnd, lineBStart, lineBEnd) {
|
||
|
function tCCW(p1, p2, p3) {
|
||
|
var tA = p1.x,
|
||
|
tB = p1.y,
|
||
|
tC = p2.x,
|
||
|
tD = p2.y,
|
||
|
tE = p3.x,
|
||
|
tF = p3.y;
|
||
|
return (tF - tB) * (tC - tA) > (tD - tB) * (tE - tA);
|
||
|
}
|
||
|
var tP1 = lineAStart,
|
||
|
tP2 = lineAEnd,
|
||
|
tP3 = lineBStart,
|
||
|
tP4 = lineBEnd;
|
||
|
return (tCCW(tP1, tP3, tP4) != tCCW(tP2, tP3, tP4)) && (tCCW(tP1, tP2, tP3) != tCCW(tP1, tP2, tP4));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
@param corners Is an array of points with x,y attributes
|
||
|
@param startX X start coord for raycast
|
||
|
@param startY Y start coord for raycast
|
||
|
*/
|
||
|
static pointInPolygon2(point, polygon) {
|
||
|
var x = point.x,
|
||
|
y = point.y;
|
||
|
var inside = false;
|
||
|
for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||
|
var intersect = ((((polygon[i].y <= y) && (y < polygon[j].y)) || ((polygon[j].y <= y) && (y < polygon[i].y))) && (x < (polygon[j].x - polygon[i].x) * (y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x));
|
||
|
if (intersect) {
|
||
|
inside = !inside;
|
||
|
}
|
||
|
}
|
||
|
return inside;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
@param corners Is an array of points with x,y attributes
|
||
|
@param startX X start coord for raycast
|
||
|
@param startY Y start coord for raycast
|
||
|
*/
|
||
|
static pointInPolygon(point, corners, start) {
|
||
|
start = start || new Vector2(0, 0);
|
||
|
var startX = start.x || 0;
|
||
|
var startY = start.y || 0;
|
||
|
|
||
|
//ensure that point(startX, startY) is outside the polygon consists of corners
|
||
|
var tMinX = 0,
|
||
|
tMinY = 0;
|
||
|
var tI = 0;
|
||
|
|
||
|
if (startX === undefined || startY === undefined) {
|
||
|
for (tI = 0; tI < corners.length; tI++) {
|
||
|
tMinX = Math.min(tMinX, corners[tI].x);
|
||
|
tMinY = Math.min(tMinX, corners[tI].y);
|
||
|
}
|
||
|
startX = tMinX - 10;
|
||
|
startY = tMinY - 10;
|
||
|
}
|
||
|
|
||
|
var tIntersects = 0;
|
||
|
for (tI = 0; tI < corners.length; tI++) {
|
||
|
var tFirstCorner = corners[tI],
|
||
|
tSecondCorner;
|
||
|
if (tI === corners.length - 1) {
|
||
|
tSecondCorner = corners[0];
|
||
|
} else {
|
||
|
tSecondCorner = corners[tI + 1];
|
||
|
}
|
||
|
|
||
|
if (Utils.lineLineIntersect(start, point, tFirstCorner.x, tFirstCorner.y, tSecondCorner.x, tSecondCorner.y)) {
|
||
|
tIntersects++;
|
||
|
}
|
||
|
}
|
||
|
// odd intersections means the point is in the polygon
|
||
|
return ((tIntersects % 2) === 1);
|
||
|
}
|
||
|
|
||
|
/** Checks if all corners of insideCorners are inside the polygon described by outsideCorners */
|
||
|
static polygonInsidePolygon(insideCorners, outsideCorners, start) {
|
||
|
start.x = start.x || 0;
|
||
|
start.y = start.y || 0;
|
||
|
|
||
|
for (var tI = 0; tI < insideCorners.length; tI++) {
|
||
|
if (!Utils.pointInPolygon(insideCorners[tI].x, insideCorners[tI].y, outsideCorners, start)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/** Checks if any corners of firstCorners is inside the polygon described by secondCorners */
|
||
|
static polygonOutsidePolygon(insideCorners, outsideCorners, start) {
|
||
|
start.x = start.x || 0;
|
||
|
start.y = start.y || 0;
|
||
|
|
||
|
for (var tI = 0; tI < insideCorners.length; tI++) {
|
||
|
if (Utils.pointInPolygon(insideCorners[tI].x, insideCorners[tI].y, outsideCorners, start)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// arrays
|
||
|
|
||
|
static forEach(array, action) {
|
||
|
for (var tI = 0; tI < array.length; tI++) {
|
||
|
action(array[tI]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static forEachIndexed(array, action) {
|
||
|
for (var tI = 0; tI < array.length; tI++) {
|
||
|
action(tI, array[tI]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static map(array, func) {
|
||
|
var tResult = [];
|
||
|
array.forEach((element) => {
|
||
|
tResult.push(func(element));
|
||
|
});
|
||
|
return tResult;
|
||
|
}
|
||
|
|
||
|
/** Remove elements in array if func(element) returns true */
|
||
|
static removeIf(array, func) {
|
||
|
var tResult = [];
|
||
|
array.forEach((element) => {
|
||
|
if (!func(element)) {
|
||
|
tResult.push(element);
|
||
|
}
|
||
|
});
|
||
|
return tResult;
|
||
|
}
|
||
|
|
||
|
/** Shift the items in an array by shift (positive integer) */
|
||
|
static cycle(arr, shift) {
|
||
|
var tReturn = arr.slice(0);
|
||
|
for (var tI = 0; tI < shift; tI++) {
|
||
|
var tmp = tReturn.shift();
|
||
|
tReturn.push(tmp);
|
||
|
}
|
||
|
return tReturn;
|
||
|
}
|
||
|
|
||
|
/** Returns in the unique elemnts in arr */
|
||
|
static unique(arr, hashFunc) {
|
||
|
var tResults = [];
|
||
|
var tMap = {};
|
||
|
for (var tI = 0; tI < arr.length; tI++) {
|
||
|
if (!tMap.hasOwnProperty(arr[tI])) {
|
||
|
tResults.push(arr[tI]);
|
||
|
tMap[hashFunc(arr[tI])] = true;
|
||
|
}
|
||
|
}
|
||
|
return tResults;
|
||
|
}
|
||
|
|
||
|
/** Remove value from array, if it is present */
|
||
|
static removeValue(array, value) {
|
||
|
for (var tI = array.length - 1; tI >= 0; tI--) {
|
||
|
if (array[tI] === value) {
|
||
|
array.splice(tI, 1);
|
||
|
return tI;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Checks if value is in array */
|
||
|
static hasValue(array, value) {
|
||
|
for (var tI = 0; tI < array.length; tI++) {
|
||
|
if (array[tI] === value) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/** Subtracts the elements in subArray from array */
|
||
|
static subtract(array, subArray) {
|
||
|
return Utils.removeIf(array, function(el) {
|
||
|
return Utils.hasValue(subArray, el);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
static point2Dto3D(point2D, elevation = 0) {
|
||
|
return new Vector3(point2D.x, elevation, point2D.y);
|
||
|
}
|
||
|
|
||
|
static point3Dto2D(point2D) {
|
||
|
return new Vector2(point2D.x, point2D.z);
|
||
|
}
|
||
|
|
||
|
static barycentricFromCartesian(triangle, point) {
|
||
|
//Vector ab
|
||
|
let ab = triangle[1].clone().sub(triangle[0]);
|
||
|
let ac = triangle[2].clone().sub(triangle[0]);
|
||
|
let ap = point.clone().sub(triangle[0]);
|
||
|
|
||
|
let dotAB = ab.dot(ab);
|
||
|
let dotAC = ac.dot(ac);
|
||
|
let dotABAC = ac.dot(ab);
|
||
|
let dotAPAB = ap.dot(ab);
|
||
|
let dotAPAC = ap.dot(ac);
|
||
|
|
||
|
//Area of triangle ABC
|
||
|
let areaABAC = (dotAB * dotAC) - (dotABAC * dotABAC);
|
||
|
let v = ((dotAC * dotAPAB) - (dotABAC * dotAPAC)) / areaABAC;
|
||
|
let w = ((dotAB * dotAPAC) - (dotABAC * dotAPAB)) / areaABAC;
|
||
|
let u = 1.0 - v - w;
|
||
|
return new Vector3(u, v, w);
|
||
|
}
|
||
|
|
||
|
static cartesianFromBarycenter(triangle, uvw) {
|
||
|
let a = triangle[0].clone();
|
||
|
let b = triangle[1].clone();
|
||
|
let c = triangle[2].clone();
|
||
|
let cartesian = a.multiplyScalar(uvw.x).add(b.multiplyScalar(uvw.y).add(c.multiplyScalar(uvw.z)));
|
||
|
return cartesian;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
export class Region {
|
||
|
constructor(points) {
|
||
|
this.points = points || [];
|
||
|
this.length = points.length;
|
||
|
}
|
||
|
|
||
|
area() {
|
||
|
var area = 0,
|
||
|
i,
|
||
|
j,
|
||
|
point1,
|
||
|
point2;
|
||
|
|
||
|
for (i = 0, j = this.length - 1; i < this.length; j = i, i += 1) {
|
||
|
point1 = this.points[i];
|
||
|
point2 = this.points[j];
|
||
|
area += point1.x * point2.y;
|
||
|
area -= point1.y * point2.x;
|
||
|
}
|
||
|
area *= 0.5;
|
||
|
|
||
|
return area;
|
||
|
};
|
||
|
|
||
|
centroid() {
|
||
|
var x = 0,
|
||
|
y = 0,
|
||
|
i,
|
||
|
j,
|
||
|
f,
|
||
|
point1,
|
||
|
point2;
|
||
|
|
||
|
for (i = 0, j = this.length - 1; i < this.length; j = i, i += 1) {
|
||
|
point1 = this.points[i];
|
||
|
point2 = this.points[j];
|
||
|
f = point1.x * point2.y - point2.x * point1.y;
|
||
|
x += (point1.x + point2.x) * f;
|
||
|
y += (point1.y + point2.y) * f;
|
||
|
}
|
||
|
|
||
|
f = this.area() * 6;
|
||
|
|
||
|
return new Vector2(x / f, y / f);
|
||
|
};
|
||
|
}
|