/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; // use: add.array // use: add.class gapp.register("geo.base", [], (root, exports) => { const base = root.base = {}; const round_decimal_precision = 5; function time() { return Date.now() } 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; } let out = incFrom ? [from] : []; while (steps-- > 0) { from += per * dir; out.push(from); } return out; } /** 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); } /** 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; } /** * call function with all combinations of a1, a2 * and passing in the supplied arg object. * used by polygons.trimTo() for trimming slice projected fills * @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; } function isClockwise(p1, p2, p3) { return area2(p1, p2, p3) > 0; } function isCounterClockwise(p1, p2, p3) { return area2(p1, p2, p3) < 0; } function pac(p1, p2) { return (p2.x - p1.x) * (p2.y + p1.y); } /** * returns 2x area for a triangle with sign indicating handedness * @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); } function isCloseTo(v1, v2, dist) { return Math.abs(v1 - v2) <= (dist || base.config.precision_merge); } function inCloseRange(val, min, max) { return (isCloseTo(val, min) || val >= min) && (isCloseTo(val, max) || val <= max); } /** * return square of value */ function sqr(v) { return v * v } /** * return distance squared between two points */ function distSq(p1, p2) { return sqr(p2.x - p1.x) + sqr(p2.y - p1.y) } /** * return distance squared between two points * enables faster Point.nearPolygon() */ function distSqv2(x1, y1, x2, y2) { return sqr(x2 - x1) + sqr(y2 - y1) } function offsetPrecision(offset, precision) { return Math.abs(offset) - precision; } function inRange(value, min, max) { let val = parseFloat(value); return val >= min && val <= max; } function round(v, zeros) { const prec = zeros !== undefined ? zeros : round_decimal_precision; if (prec === 0) return v | 0; let pow = Math.pow(10, prec); return Math.round(v * pow) / pow; } /** * used by {@link Polygon.trace} and {@link Polygon.intersect} */ 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} */ 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); } /** * Find Z of XY pair given plane defined by 3 points */ function zInPlane(p1, p2, p3, x, y) { 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); if (vec1.z !== 0) { return ((vec1.x * (x - p1.x) + vec1.y * (y - p1.y)) / -vec1.z) + p1.z; } } /** * 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] ]); 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] ]; 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] ]; let center = { x: determinant33(xmat) / denominator, y: determinant33(ymat) / denominator, z: A.z }; if (denominator !== 0) { return center; } } /** * 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 */ function center2d(A, B, C, rad) { let center = circleCenter(A, B, C); if (center && rad) { let dx = center.x - A.x; let dy = center.y - A.y; center.r = Math.sqrt(dx * dx + dy * dy) } return center; } /** * return one of two possible circle centers given two points, a radius and clock direction * https://stackoverflow.com/questions/36211171/finding-center-of-a-circle-given-two-points-and-radius */ function center2pr(p1, p2, r, clockwise) { 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 }; } } // find angle difference between 0 and 2pi from n1 to n2 (signed depending on clock direction) function thetaDiff(n1, n2, clockwise) { 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; } // 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, pos) { let narray; let info; 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]; } let dmax = 0, d1, d2; for (let i = 0; i < dims; i++) { if (delta[i] > dmax) { dmax = delta[i]; d1 = i; } } info = { d1, d2, dmax, delta: delta.slice() }; 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); if (pos) { return ec; } 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]; } } if (oa.length === 0) { console.log('debug_triangulate', {array, oa, info}); } return oa; } function flatten(arr) { return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), []) } function 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; } /** ****************************************************************** * 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.05, bridgeLineGapDistanceMax: 25, // 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, comma, round, area2, pwait, ptimer, flatten, distSq, distSqv2, center2d, center2pr, determinant, orderClosest, doCombinations, offsetPrecision, numOrDefault, thetaDiff, intersect, inRange, isCloseTo, inCloseRange, isClockwise, isCounterClockwise, intersectRayLine, triangulate, zInPlane }; });