grid-apps-cmms/src/geo/base.js

637 lines
19 KiB
JavaScript
Raw Normal View History

2020-06-03 22:39:30 -04:00
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
2017-04-18 22:22:29 -04:00
2017-04-18 17:09:33 -04:00
"use strict";
(function() {
2020-06-03 22:39:30 -04:00
if (self.base) return;
2017-04-18 17:09:33 -04:00
2020-06-03 22:39:30 -04:00
const BASE = self.base = {},
2017-04-18 17:09:33 -04:00
round_decimal_precision = 8;
/** ******************************************************************
* Utility Functions
******************************************************************* */
2020-06-03 22:39:30 -04:00
function time() { return Date.now() }
2017-04-18 17:09:33 -04:00
2021-02-20 22:50:03 -05:00
function lerp(from, to, maxInc, incFrom) {
let dir = Math.sign(to - from);
let delta = Math.abs(to - from);
let steps = Math.floor(delta / maxInc);
let rem = delta % maxInc;
let per = delta / steps;
if (rem) {
steps++;
per = delta / steps;
}
2021-02-20 22:50:03 -05:00
let out = incFrom ? [from] : [];
while (steps-- > 0) {
from += per * dir;
out.push(from);
}
return out;
}
2021-06-22 15:06:07 -04:00
/** track an array of promises as they all complete */
async function pwait(promises, tracker) {
let count = 0;
if (tracker)
for (let p of promises) {
p.then(data => {
tracker(count++, promises.length, data);
});
}
await Promise.all(promises);
}
2021-06-22 15:06:07 -04:00
/** return a promise that resolves after a given time */
function ptimer(time) {
return new Promise((resolve, reject) => {
setTimeout(resolve, time);
});
}
function numOrDefault(num, def) {
return num !== undefined ? num : def;
}
2017-04-18 17:09:33 -04:00
/**
* call function with all combinations of a1, a2
* and passing in the supplied arg object.
*
* @param {Array} a1
* @param {Array} a2
* @param {Object} arg
* @param {Function} fn
* @returns {Object}
*/
function doCombinations(a1, a2, arg, fn) {
2020-06-03 22:39:30 -04:00
let i, j;
2017-04-18 17:09:33 -04:00
for (i = 0; i < a1.length; i++) {
for (j = (a1 === a2 ? i + 1 : 0); j < a2.length; j++) {
fn(a1[i], a2[j], arg);
}
}
return arg;
}
/**
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @returns {boolean}
*/
function isClockwise(p1, p2, p3) {
return area2(p1, p2, p3) > 0;
}
/**
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @returns {boolean}
*/
function isCounterClockwise(p1, p2, p3) {
return area2(p1, p2, p3) < 0;
}
/**
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @returns {boolean}
*/
// function isCollinear(p1, p2, p3) {
// return inCloseRange(area2(p1, p2, p3), -0.00001, 0.00001);
// }
2017-04-18 17:09:33 -04:00
function pac(p1, p2) {
return (p2.x - p1.x) * (p2.y + p1.y);
}
/**
* returns 2x area for a triangle with sign indicating handedness
*
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @returns {number} negative for CCW progression, positive for CW progression
*/
function area2(p1, p2, p3) {
return pac(p1,p2) + pac(p2,p3) + pac(p3,p1);
}
/**
*
* @param v1
* @param v2
* @param [dist]
* @returns {boolean}
*/
function isCloseTo(v1,v2,dist) {
2017-10-28 13:30:08 -04:00
return Math.abs(v1-v2) <= (dist || BASE.config.precision_merge);
2017-04-18 17:09:33 -04:00
}
/**
*
* @param val
* @param min
* @param max
* @returns {boolean}
*/
function inCloseRange(val, min, max) {
return (isCloseTo(val,min) || val >= min) && (isCloseTo(val,max) || val <= max);
}
/**
* return square of value
* @param v
* @returns {number}
*/
function sqr(v) { return v * v }
/**
* return distance squared between two points
* @param p1
* @param p2
* @returns {number}
*/
function dist2(p1,p2) { return sqr(p2.x - p1.x) + sqr(p2.y - p1.y) }
/**
* return distance squared between two points
* enables faster Point.nearPolygon()
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @returns {number}
*/
function dist2v2(x1,y1,x2,y2) { return sqr(x2 - x1) + sqr(y2 - y1) }
/**
*
* @param offset
* @param precision
* @returns {number}
*/
function offsetPrecision(offset, precision) {
2017-10-28 13:30:08 -04:00
return Math.abs(offset) - precision;
2017-04-18 17:09:33 -04:00
}
/**
*
* @param value
* @param min
* @param max
* @returns {boolean}
*/
function inRange(value, min, max) {
2020-06-03 22:39:30 -04:00
let val = parseFloat(value);
2017-04-18 17:09:33 -04:00
return val >= min && val <= max;
}
/**
*
* @param v
* @param zeros
* @returns {number}
*/
function round(v, zeros) {
2020-06-03 22:39:30 -04:00
let pow = Math.pow(10,zeros || round_decimal_precision);
2017-04-18 17:09:33 -04:00
return Math.round(v * pow) / pow;
}
/**
* used by {@link Polygon.trace} and {@link Polygon.intersect}
*
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @param {Point} p4
* @param {String} [test]
* @param {boolean} [parallelok]
* @returns {?Point | String}
*/
function intersect(p1, p2, p3, p4, test, parallelok) {
2020-06-03 22:39:30 -04:00
let keys = BASE.key,
2017-04-18 17:09:33 -04:00
p1x = p1.x,
p1y = p1.y,
p2x = p2.x,
p2y = p2.y,
p3x = p3.x,
p3y = p3.y,
p4x = p4.x,
p4y = p4.y,
d1x = (p2x - p1x), // ad.x
d1y = (p2y - p1y), // ad.y
d2x = (p4x - p3x), // bd.x
d2y = (p4y - p3y), // bd.y
d = (d2y * d1x) - (d2x * d1y); // det
2017-10-28 13:30:08 -04:00
//if (Math.abs(d) < 0.0000000001) {
if (Math.abs(d) < 0.0001) {
2017-04-18 17:09:33 -04:00
// lines are parallel or collinear
return test && !parallelok ? null : keys.PARALLEL;
}
2020-06-03 22:39:30 -04:00
let a = p1y - p3y, // origin dy
2017-04-18 17:09:33 -04:00
b = p1x - p3x, // origin dx
n1 = (d2x * a) - (d2y * b),
n2 = (d1x * a) - (d1y * b);
a = n1 / d; // roughly distance from l1 origin to l2 intersection
b = n2 / d; // roughly distance from l2 origin to l1 intersection
2020-06-03 22:39:30 -04:00
let ia = a >= -0.0001 && a <= 1.0001,
2017-04-18 17:09:33 -04:00
ib = b >= -0.0001 && b <= 1.0001,
segint = (ia && ib),
rayint = (a >= 0 && b >= 0);
if (test === keys.SEGINT && !segint) return null;
if (test === keys.RAYINT && !rayint) return null;
2020-06-03 22:39:30 -04:00
let ip = BASE.newPoint(
2017-04-18 17:09:33 -04:00
p1x + (a * d1x), // x
p1y + (a * d1y), // y
p3.z || p4.z, // z
segint ? keys.SEGINT : rayint ? keys.RAYINT : keys.PROJECT
);
ip.dist = a;
ip.p1 = p3;
ip.p2 = p4;
return ip;
}
/**
* used by {@link rayIntersect} and {@link Polygon.trace}
*
* @param {Point} ro
* @param {Slope} s1
* @param {Point} p1
* @param {Point} p2
* @param {boolean} [infinite]
* @returns {?Point}
*/
function intersectRayLine(ro, s1, p1, p2, infinite) {
2020-06-03 22:39:30 -04:00
let keys = BASE.key,
2017-04-18 17:09:33 -04:00
p1x = ro.x,
p1y = ro.y,
s1x = s1.dx,
s1y = s1.dy,
p3x = p1.x,
p3y = p1.y,
p4x = p2.x,
p4y = p2.y,
s2x = p4x - p3x,
s2y = p4y - p3y,
d = (s2y * s1x) - (s2x * s1y);
2020-06-03 22:39:30 -04:00
let a = p1y - p3y,
2017-04-18 17:09:33 -04:00
b = p1x - p3x,
n1 = (s2x * a) - (s2y * b),
n2 = (s1x * a) - (s1y * b);
2017-10-28 13:30:08 -04:00
if (Math.abs(d) < 0.000000000001) {
2017-04-18 17:09:33 -04:00
// lines are parallel or collinear
return null;
}
a = n1 / d;
b = n2 / d;
if (infinite || (inCloseRange(b,0,1) && a >= 0)) {
2020-06-03 22:39:30 -04:00
let ip = BASE.newPoint(
2017-04-18 17:09:33 -04:00
p1x + (a * s1x),
p1y + (a * s1y),
p2.z || ro.z,
keys.NONE
);
ip.dist = a;
ip.p1 = p1;
ip.p2 = p2;
return ip;
}
return null;
}
/**
* @param {Point} p1
* @param {Point} p2
* @param {Point} p3
* @param {Point} p4
* @returns {number}
*/
function determinant(p1, p2, p3, p4) {
2020-06-03 22:39:30 -04:00
let d1x = (p2.x - p1.x),
2017-04-18 17:09:33 -04:00
d1y = (p2.y - p1.y),
d2x = (p4.x - p3.x),
d2y = (p4.y - p3.y);
return (d2y * d1x) - (d2x * d1y);
}
2021-06-07 16:05:28 -04:00
/**
* Find Z of XY pair given plane defined by 3 points
*/
2021-06-08 14:02:02 -04:00
function zInPlane(p1, p2, p3, x, y) {
2021-06-07 16:05:28 -04:00
let vec1 = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
let vec2 = new THREE.Vector3(p3.x - p1.x, p3.y - p1.y, p3.z - p1.z);
vec1.cross(vec2);
2021-06-08 14:02:02 -04:00
2021-06-07 16:05:28 -04:00
if (vec1.z !== 0) {
return ((vec1.x * (x - p1.x) + vec1.y * (y - p1.y)) / -vec1.z) + p1.z;
}
}
2021-06-01 15:10:29 -04:00
/**
* find circle center given 3 points in XY plane
*/
function circleCenter(A,B,C)
{
let denominator = 2*determinant33([[A.x, A.y, 1], [B.x, B.y, 1], [C.x, C.y, 1]]);
2021-06-01 15:10:29 -04:00
let xmat = [[A.x*A.x + A.y*A.y, A.y, 1],[B.x*B.x + B.y*B.y, B.y, 1],[C.x*C.x + C.y*C.y, C.y, 1]];
2021-06-01 16:29:56 -04:00
let ymat = [[A.x, A.x*A.x + A.y*A.y, 1],[B.x, B.x*B.x + B.y*B.y, 1],[C.x, C.x*C.x + C.y*C.y, 1]];
2021-06-01 15:10:29 -04:00
let center = {x:determinant33(xmat)/denominator, y:determinant33(ymat)/denominator, z:A.z};
2021-06-01 15:10:29 -04:00
if (denominator !== 0) {
return center;
}
}
2021-06-01 15:10:29 -04:00
/**
* find the determinant of a 3x3 matrix array organized [row, col]
*/
function determinant33(mat33)
{
let cofactor00 = mat33[0][0] * (mat33[1][1] * mat33[2][2] - mat33[1][2] * mat33[2][1]);
let cofactor01 = -mat33[0][1] * (mat33[1][0] * mat33[2][2] - mat33[1][2] * mat33[2][0]);
let cofactor02 = mat33[0][2] * (mat33[1][0] * mat33[2][1] - mat33[1][1] * mat33[2][0]);
return cofactor00 + cofactor01 + cofactor02;
}
/**
* return circle center given three points
* from https://stackoverflow.com/questions/4103405/what-is-the-algorithm-for-finding-the-center-of-a-circle-from-three-points
*/
2021-02-04 21:35:42 -05:00
function center2d(A,B,C,rad) {
let center = circleCenter(A,B,C);
2021-05-31 22:44:13 -04:00
if (center && rad) {
2021-02-04 21:35:42 -05:00
let dx = center.x - A.x;
let dy = center.y - A.y;
center.r = Math.sqrt(dx*dx + dy*dy)
}
return center;
}
2021-02-06 16:07:56 -05:00
/**
* return one of two possible circle centers given two points, a radius and clock direction
2021-02-06 16:07:56 -05:00
* https://stackoverflow.com/questions/36211171/finding-center-of-a-circle-given-two-points-and-radius
*/
function center2pr(p1, p2, r, clockwise) {
2021-02-06 16:07:56 -05:00
let x1 = p1.x,
x2 = p2.x,
y1 = p1.y,
y2 = p2.y,
q = Math.sqrt(Math.pow((x2-x1),2) + Math.pow((y2-y1),2)),
y3 = (y1+y2)/2,
x3 = (x1+x2)/2,
basex = Math.sqrt(Math.pow(r,2)-Math.pow((q/2),2))*(y1-y2)/q, //calculate once
basey = Math.sqrt(Math.pow(r,2)-Math.pow((q/2),2))*(x2-x1)/q, //calculate once
centerx1 = x3 + basex, //center x of circle 1
centery1 = y3 + basey, //center y of circle 1
centerx2 = x3 - basex, //center x of circle 2
centery2 = y3 - basey, //center y of circle 2
dir = new THREE.Vector2(x2 - x1, y2 - y1),
vec1 = new THREE.Vector2(centerx1 - x1, centery1 - y1),
vec2 = new THREE.Vector2(centerx2 - x1, centery2 - x1);
if (clockwise) {
return dir.cross(vec1) > 0 ? {x:centerx1, y:centery1} : {x:centerx2, y:centery2};
} else {
return dir.cross(vec1) < 0 ? {x:centerx1, y:centery1} : {x:centerx2, y:centery2};
}
2021-02-06 16:07:56 -05:00
}
// find angle difference between 0 and 2pi from n1 to n2 (signed depending on clock direction)
function thetaDiff(n1, n2, clockwise) {
2021-02-06 01:29:33 -05:00
let diff = n2 - n1;
while (diff < -Math.PI) diff += Math.PI * 2;
while (diff > Math.PI) diff -= Math.PI * 2;
if (clockwise && diff > 0) diff -= Math.PI * 2;
if (!clockwise && diff < 0) diff += Math.PI * 2;
return diff;
2021-02-06 01:29:33 -05:00
}
// order array of elements using next closest element comparator
// used for things like next-closest point walks (fdm thin fill)
// in future, replace poly2poly and similar with this
function orderClosest(array, fn, from) {
if (!array.length) {
return array;
}
let out = new Array(array.length);
let outi = 0;
let root = 0;
if (!from) {
from = array[root++];
out[outi++] = from;
}
for (;;) {
let best, best_i, best_dist = Infinity;
for (let i=root; i<array.length; i++) {
let el = array[i];
if (!el) continue;
let dist = fn(from, el);
if (dist < best_dist) {
best_dist = dist;
best_i = i;
best = el;
}
}
if (!best) break;
array[best_i] = undefined;
from = out[outi++] = best;
}
return out;
}
// wrapper for earcut that handles higher order dimensions and finds the
// two with the greatest delta to pass to the earcut algorith, then returns
// an unwrapped array in the original dimensions
function triangulate(array, holes, dims) {
2022-01-29 23:38:52 -05:00
let narray;
if (dims === 2) {
narray = array;
} else {
let min = new Array(dims).fill(Infinity);
let max = new Array(dims).fill(-Infinity);
for (let i=0, l=array.length; i<l; ) {
for (let j=0, av; j<dims; j++) {
av = array[i++];
min[j] = Math.min(min[j], av);
max[j] = Math.max(max[j], av);
}
}
let delta = new Array(dims);
for (let i=0; i<dims; i++) {
delta[i] = max[i] - min[i];
}
2022-01-29 23:38:52 -05:00
let dmax = 0, d1, d2;
for (let i=0; i<dims; i++) {
if (delta[i] > dmax) {
dmax = delta[i];
d1 = i;
}
}
delta[d1] = dmax = 0;
for (let i=0; i<dims; i++) {
if (delta[i] > dmax) {
dmax = delta[i];
d2 = i;
}
}
narray = new Array((array.length / dims) * 2);
for (let i=0, j=0; i<array.length; i += dims) {
narray[j++] = array[i + d1];
narray[j++] = array[i + d2];
}
}
let ec = earcut(narray, holes, 2);
let oa = new Array(ec.length * dims);
for (let i=0, e=0, l=ec.length, ai; i<l; i++) {
ai = ec[i] * dims;
for (let j=0; j<dims; j++) {
oa[e++] = array[ ai + j ];
}
}
return oa;
}
2022-01-30 17:17:16 -05:00
function flatten(arr) {
return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), [])
}
2017-04-18 17:09:33 -04:00
/** ******************************************************************
* Connect to base
******************************************************************* */
BASE.key = {
NONE : "",
PROJECT : "project",
SEGINT : "segint",
RAYINT : "rayint",
PARALLEL : "parallel"
};
BASE.config = {
// size of gcode debug arrow head
debug_arrow : 0.25,
// default # of decimal places in generated gcode
gcode_decimals : 4,
2017-04-18 17:09:33 -04:00
// heal disjoint polygons in slicing (experimental)
2022-01-25 21:54:07 -05:00
bridgeLineGapDistance : 0.05,
bridgeLineGapDistanceMax : 25,
2017-04-18 17:09:33 -04:00
// Bounds default margin nearTo
// Polygon.offset mindist2 offset precision
precision_offset : 0.05,
// Polygon.isEquivalent area() isCloseTo
precision_poly_area : 0.05,
// Polygon.isEquivalent bounds() equals value
precision_poly_bounds: 0.01,
// Polygon.isEquivalent point distance to other poly line
precision_poly_merge: 0.05,
// Polygon.traceIntersects mindist2
// Polygon.overlaps (bounds overlaps test precision)
// Polygon.isEquivalent circularity (is circle if 1-this < merge)
// Slope.isSame (vert/horiz w/in this value)
// isCloseTo() default for dist
// sliceIntersects point merge dist for non-fill
2020-10-28 12:16:02 -04:00
precision_merge : 0.005,
precision_slice_z : 0.0001,
2017-04-18 17:09:33 -04:00
// Point.isInPolygon nearPolygon value
// Point.isInPolygonNotNear nearPolygon value
// Point.isMergable2D distToSq2D value
// Point.isMergable3D distToSq2D value
// Polygon.isInside nearPolygon value
// Polygon.isOutside nearPolygon value
2020-10-28 12:16:02 -04:00
precision_merge_sq : sqr(0.005),
2017-04-18 17:09:33 -04:00
// Bound.isNested inflation value for potential parent
precision_bounds : 0.0001,
// Slope.isSame default precision
precision_slope : 0.02,
// Slope.isSame use to calculate precision
precision_slope_merge : 0.25,
// sliceIntersect point merge distance for fill
precision_fill_merge : 0.001,
// convertPoints point merge distance
// other values break cube-s9 (wtf)
precision_decimate : 0.05,
// decimate test over this many points
decimate_threshold : 500000,
2017-04-18 17:09:33 -04:00
// Point.onLine precision distance (endpoints in Polygon.intersect)
precision_point_on_line : 0.01,
// Polygon.isEquivalent value for determining similar enough to test
precision_circularity : 0.001,
// polygon fill hinting (settings override)
hint_len_min : sqr(3),
hint_len_max : sqr(20),
hint_min_circ : 0.15,
// tolerances to determine if a point is near a masking polygon
precision_mask_tolerance : 0.001,
// Polygon isInside,isOutside tolerance (accounts for midpoint skew)
precision_close_to_poly_sq : sqr(0.001),
// how long a segment has to be to trigger a midpoint check (inner/outer)
precision_midpoint_check_dist : 1,
precision_nested_sq : sqr(0.01),
// clipper multiplier
clipper : 100000,
// clipper poly clean
clipperClean : 250
2017-04-18 17:09:33 -04:00
};
BASE.util = {
sqr,
lerp,
time,
round,
area2,
pwait,
2021-06-22 15:06:07 -04:00
ptimer,
2022-01-30 17:17:16 -05:00
flatten,
center2d,
2021-02-06 16:07:56 -05:00
center2pr,
2021-02-06 01:29:33 -05:00
thetaDiff,
2017-04-18 17:09:33 -04:00
distSq : dist2,
distSqv2 : dist2v2,
inRange,
isCloseTo,
inCloseRange,
// isCollinear,
isClockwise,
isCounterClockwise,
doCombinations,
offsetPrecision,
intersectRayLine,
intersect,
determinant,
numOrDefault,
orderClosest,
2021-06-07 16:05:28 -04:00
zInPlane,
triangulate,
comma: (v) => {
if (!v) return v;
let [lt,rt] = v.toString().split('.');
lt = lt.toString().split('').reverse().map((v,i,a) => {
return (i < a.length - 1 && i % 3 === 2) ? `,${v}` : v
}).reverse().join('');
return rt ? `${lt}.${rt}` : lt;
}
2017-04-18 17:09:33 -04:00
};
})();