496 lines
14 KiB
JavaScript
496 lines
14 KiB
JavaScript
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
|
|
|
"use strict";
|
|
|
|
(function() {
|
|
|
|
if (self.base) return;
|
|
|
|
const BASE = self.base = {},
|
|
round_decimal_precision = 8;
|
|
|
|
/** ******************************************************************
|
|
* Utility Functions
|
|
******************************************************************* */
|
|
|
|
function time() { return Date.now() }
|
|
|
|
function lerp(from, to, maxInc) {
|
|
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;
|
|
}
|
|
let out = [];
|
|
while (steps-- > 0) {
|
|
from += per * dir;
|
|
out.push(from);
|
|
}
|
|
return out;
|
|
};
|
|
|
|
function numOrDefault(num, def) {
|
|
return num !== undefined ? num : def;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
let i, j;
|
|
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);
|
|
}
|
|
|
|
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) {
|
|
return Math.abs(v1-v2) <= (dist || BASE.config.precision_merge);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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) {
|
|
return Math.abs(offset) - precision;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param value
|
|
* @param min
|
|
* @param max
|
|
* @returns {boolean}
|
|
*/
|
|
function inRange(value, min, max) {
|
|
let val = parseFloat(value);
|
|
return val >= min && val <= max;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param v
|
|
* @param zeros
|
|
* @returns {number}
|
|
*/
|
|
function round(v, zeros) {
|
|
let pow = Math.pow(10,zeros || round_decimal_precision);
|
|
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) {
|
|
let keys = BASE.key,
|
|
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
|
|
|
|
//if (Math.abs(d) < 0.0000000001) {
|
|
if (Math.abs(d) < 0.0001) {
|
|
// lines are parallel or collinear
|
|
return test && !parallelok ? null : keys.PARALLEL;
|
|
}
|
|
|
|
let a = p1y - p3y, // origin dy
|
|
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
|
|
|
|
let ia = a >= -0.0001 && a <= 1.0001,
|
|
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;
|
|
|
|
let ip = BASE.newPoint(
|
|
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) {
|
|
let keys = BASE.key,
|
|
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);
|
|
|
|
let a = p1y - p3y,
|
|
b = p1x - p3x,
|
|
n1 = (s2x * a) - (s2y * b),
|
|
n2 = (s1x * a) - (s1y * b);
|
|
|
|
if (Math.abs(d) < 0.000000000001) {
|
|
// lines are parallel or collinear
|
|
return null;
|
|
}
|
|
|
|
a = n1 / d;
|
|
b = n2 / d;
|
|
|
|
if (infinite || (inCloseRange(b,0,1) && a >= 0)) {
|
|
let ip = BASE.newPoint(
|
|
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) {
|
|
let d1x = (p2.x - p1.x),
|
|
d1y = (p2.y - p1.y),
|
|
d2x = (p4.x - p3.x),
|
|
d2y = (p4.y - p3.y);
|
|
|
|
return (d2y * d1x) - (d2x * d1y);
|
|
}
|
|
|
|
/**
|
|
* return circle center given three points
|
|
* from https://math.stackexchange.com/questions/1076177/3d-coordinates-of-circle-center-given-three-point-on-the-circle
|
|
*/
|
|
function center3d(a,b,c) {
|
|
let ax = a.x,
|
|
ay = a.y,
|
|
az = a.z || 0,
|
|
bx = b.x,
|
|
by = b.y,
|
|
bz = b.z || 0,
|
|
cx = c.x,
|
|
cy = c.y,
|
|
cz = c.z || 0,
|
|
Cx = bx-ax,
|
|
Cy = by-ay,
|
|
Cz = bz-az,
|
|
Bx = cx-ax,
|
|
By = cy-ay,
|
|
Bz = cz-az,
|
|
B2 = (ax*ax)-(cx*cx)+(ay*ay)-(cy*cy)+(az*az)-(cz*cz),
|
|
C2 = (ax*ax)-(bx*bx)+(ay*ay)-(by*by)+(az*az)-(bz*bz),
|
|
CByz = Cy*Bz-Cz*By,
|
|
CBxz = Cx*Bz-Cz*Bx,
|
|
CBxy = Cx*By-Cy*Bx,
|
|
ZZ1 = -(Bz-Cz*Bx/Cx)/(By-Cy*Bx/Cx),
|
|
Z01 = -(B2-Bx/Cx*C2)/(2*(By-Cy*Bx/Cx)),
|
|
ZZ2 = -(ZZ1*Cy+Cz)/Cx,
|
|
Z02 = -(2*Z01*Cy+C2)/(2*Cx),
|
|
dz = -((Z02-ax)*CByz-(Z01-ay)*CBxz-az*CBxy)/(ZZ2*CByz-ZZ1*CBxz+CBxy),
|
|
dx = ZZ2*dz + Z02,
|
|
dy = ZZ1*dz + Z01;
|
|
|
|
return {x:dx, y:dy, z:dz};
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function center2d(A,B,C,rad) {
|
|
// let yDelta_a = B.y - A.y,
|
|
// xDelta_a = B.x - A.x,
|
|
// yDelta_b = C.y - B.y,
|
|
// xDelta_b = C.x - B.x,
|
|
// aSlope = yDelta_a/xDelta_a,
|
|
// bSlope = yDelta_b/xDelta_b,
|
|
// center = {x:0, y:0, z:0};
|
|
// center.x = (aSlope*bSlope*(A.y - C.y) + bSlope*(A.x + B.x) - aSlope*(B.x+C.x) )/(2* (bSlope-aSlope) );
|
|
// center.y = -1*(center.x - (A.x+B.x)/2)/aSlope + (A.y+B.y)/2;
|
|
let center = center3d(A,B,C);
|
|
if (rad) {
|
|
let dx = center.x - A.x;
|
|
let dy = center.y - A.y;
|
|
center.r = Math.sqrt(dx*dx + dy*dy)
|
|
// if (isNaN(center.r)) console.log('NaN',{dx,dy,A,B,C,center,xDelta_a,xDelta_b,yDelta_a,yDelta_b,aSlope,bSlope});
|
|
}
|
|
return center;
|
|
}
|
|
|
|
function thetaDiff(n1, n2, sign) {
|
|
let diff = n2 - n1;
|
|
while (diff < -Math.PI) diff += Math.PI * 2;
|
|
while (diff > Math.PI) diff -= Math.PI * 2;
|
|
return sign ? diff : Math.abs(diff);
|
|
}
|
|
|
|
/** ******************************************************************
|
|
* 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,
|
|
// heal disjoint polygons in slicing (experimental)
|
|
bridgeLineGapDistance : 0.0001,
|
|
// 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
|
|
precision_merge : 0.005,
|
|
precision_slice_z : 0.0001,
|
|
// Point.isInPolygon nearPolygon value
|
|
// Point.isInPolygonNotNear nearPolygon value
|
|
// Point.isMergable2D distToSq2D value
|
|
// Point.isMergable3D distToSq2D value
|
|
// Polygon.isInside nearPolygon value
|
|
// Polygon.isOutside nearPolygon value
|
|
precision_merge_sq : sqr(0.005),
|
|
// 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,
|
|
// 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
|
|
};
|
|
|
|
BASE.util = {
|
|
sqr,
|
|
lerp,
|
|
time,
|
|
round,
|
|
area2,
|
|
center2d,
|
|
center3d,
|
|
thetaDiff,
|
|
distSq : dist2,
|
|
distSqv2 : dist2v2,
|
|
inRange,
|
|
isCloseTo,
|
|
inCloseRange,
|
|
isCollinear,
|
|
isClockwise,
|
|
isCounterClockwise,
|
|
doCombinations,
|
|
offsetPrecision,
|
|
intersectRayLine,
|
|
intersect,
|
|
determinant,
|
|
numOrDefault,
|
|
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;
|
|
}
|
|
};
|
|
|
|
})();
|