mesh v2 at /v2/mesh
This commit is contained in:
parent
aee9fd6ed7
commit
6566c45f9a
85 changed files with 138198 additions and 1 deletions
852
src2/geo/base.js
Normal file
852
src2/geo/base.js
Normal file
|
|
@ -0,0 +1,852 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { newPoint } from './point.js';
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// radians rotatition around origin
|
||||
function rotate(x,y,radians) {
|
||||
return [
|
||||
x * Math.cos(radians) - y * Math.sin(radians),
|
||||
y * Math.cos(radians) + x * Math.sin(radians)
|
||||
];
|
||||
}
|
||||
|
||||
const deg2rad = (Math.PI / 180);
|
||||
const rad2deg = (180 / Math.PI);
|
||||
|
||||
function toRadians(degrees) {
|
||||
return degrees * deg2rad;
|
||||
}
|
||||
|
||||
function toDegrees(radians) {
|
||||
return radians * rad2deg;
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance between two points
|
||||
*/
|
||||
function dist2D(p1, p2) {
|
||||
return Math.sqrt(distSq(p1, p2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (typeof v === 'object') {
|
||||
for (let [key,val] of Object.entries(v)) {
|
||||
if (typeof val === 'number') {
|
||||
v[key] = round(val, zeros);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function clamp(val, low, hi) {
|
||||
return Math.max(low, Math.min(hi, val));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 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 = 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
|
||||
// at present, only used by load.obj.parse()
|
||||
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;
|
||||
}
|
||||
|
||||
// Create the base namespace object
|
||||
const 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
|
||||
},
|
||||
key: {
|
||||
NONE: "",
|
||||
PROJECT: "project",
|
||||
SEGINT: "segint",
|
||||
RAYINT: "rayint",
|
||||
PARALLEL: "parallel"
|
||||
},
|
||||
util: {
|
||||
time,
|
||||
lerp,
|
||||
pwait,
|
||||
ptimer,
|
||||
numOrDefault,
|
||||
doCombinations,
|
||||
isClockwise,
|
||||
isCounterClockwise,
|
||||
pac,
|
||||
area2,
|
||||
isCloseTo,
|
||||
inCloseRange,
|
||||
sqr,
|
||||
rotate,
|
||||
toRadians,
|
||||
toDegrees,
|
||||
dist2D,
|
||||
distSq,
|
||||
distSqv2,
|
||||
offsetPrecision,
|
||||
inRange,
|
||||
round,
|
||||
clamp,
|
||||
intersect,
|
||||
intersectRayLine,
|
||||
determinant,
|
||||
zInPlane,
|
||||
circleCenter,
|
||||
determinant33,
|
||||
center2d,
|
||||
center2pr,
|
||||
thetaDiff,
|
||||
orderClosest,
|
||||
triangulate,
|
||||
flatten,
|
||||
comma
|
||||
},
|
||||
paths: {
|
||||
pointsToPath: function(points, offset, open, miter = 1.5) {
|
||||
const absoff = Math.abs(offset);
|
||||
// calculate segment normals which are used to calculate vertex normals
|
||||
// next segment info is associated with the current point
|
||||
const nupoints = [];
|
||||
const length = points.length;
|
||||
if (length === 2 && points[0].isEqual(points[1])) {
|
||||
return { };
|
||||
}
|
||||
const dedup = (open && length > 2) || (!open && length > 3);
|
||||
for (let i=0; i<length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
p1.normal = calc_normal(p1, p2);
|
||||
// drop next duplicate point if segment length is 0
|
||||
// is possible there are 3 dups in a row and this is
|
||||
// not handled. could make if a while, but that could
|
||||
// end up in a loop without additional checks. ignore
|
||||
// the case when the last and first point are the same
|
||||
// which is valid when the line is an open path
|
||||
if (dedup && p1.normal.len === 0 && i !== length - 1) {
|
||||
p1.normal = calc_normal(p1, points[(i + 2) % length]);
|
||||
i++;
|
||||
}
|
||||
nupoints.push(p1);
|
||||
}
|
||||
if (nupoints.length === 1) {
|
||||
console.log({points, nupoints});
|
||||
}
|
||||
// when points are dropped, we need the new array
|
||||
points = nupoints;
|
||||
// generate left / right paths and triangle faces
|
||||
const left = [];
|
||||
const right = [];
|
||||
const faces = [];
|
||||
const normals = [];
|
||||
const zn = -1;
|
||||
// calculate vertex normals from segments normals
|
||||
// vertex info is associated with the origin point
|
||||
let fl, fr;
|
||||
for (let i=0, l=points.length; i<l; i++) {
|
||||
let n1 = points[(i+l-1)%l].normal;
|
||||
let n2 = points[(i+l)%l].normal;
|
||||
let vn = open && (i === 0 || i === l-1) ?
|
||||
end_vertex(n1, n2, offset, i === 0) :
|
||||
calc_vertex(n1, n2, offset);
|
||||
let { p1, p2 } = n2;
|
||||
let { io, vl } = vn;
|
||||
if (offset < 0) {
|
||||
io = -io;
|
||||
}
|
||||
let split_left = false, split_right = false;
|
||||
if (io > 0) { // right
|
||||
split_left = vl > absoff * miter;
|
||||
split_right = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
} else { // left
|
||||
split_right = vl > absoff * miter;
|
||||
split_left = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
}
|
||||
let l0 = left.peek(1);
|
||||
let r0 = right.peek(1);
|
||||
if (split_left || split_right) {
|
||||
// shorten each leg and insert new point
|
||||
let delta = 0.1;
|
||||
let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 });
|
||||
let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 });
|
||||
let sn1 = np1.normal = calc_normal(np1, np2);
|
||||
let sn2 = np2.normal = p1.normal;
|
||||
let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1);
|
||||
let nv2 = calc_vertex(sn1, sn2, offset, np2);
|
||||
if (split_right) {
|
||||
right.push(v2pr(nv1), v2pr(nv2));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(nv1), v2pl(nv2));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let l2 = left.peek(2);
|
||||
let r2 = right.peek(2);
|
||||
faces.push([l0, r0, l1], [r0, r1, l1], [l1, r1, l2], [r1, r2, l2]);
|
||||
}
|
||||
} else {
|
||||
if (split_right) {
|
||||
right.push(v2pr(vn));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(vn));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
faces.push([l0, r0, l1], [r0, r1, l1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { left, right, faces, normals };
|
||||
},
|
||||
pathTo3D: function(path, height, z) {
|
||||
const { left, right, faces } = path;
|
||||
const vertices = [];
|
||||
const normals = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
let i, j, k, l, r, f, v, n, u;
|
||||
// add left and right vertices
|
||||
for (i = 0; i < left.length; i++) {
|
||||
l = left[i];
|
||||
r = right[i];
|
||||
v = [l.x, l.y, z, r.x, r.y, z];
|
||||
n = [0, 0, 1, 0, 0, 1];
|
||||
u = [0, 0, 1, 0];
|
||||
vertices.push(...v);
|
||||
normals.push(...n);
|
||||
uvs.push(...u);
|
||||
}
|
||||
// add top vertices
|
||||
for (i = 0; i < left.length; i++) {
|
||||
l = left[i];
|
||||
r = right[i];
|
||||
v = [l.x, l.y, z + height, r.x, r.y, z + height];
|
||||
n = [0, 0, 1, 0, 0, 1];
|
||||
u = [0, 1, 1, 1];
|
||||
vertices.push(...v);
|
||||
normals.push(...n);
|
||||
uvs.push(...u);
|
||||
}
|
||||
// add faces
|
||||
for (i = 0; i < faces.length; i++) {
|
||||
f = faces[i];
|
||||
indices.push(f[0], f[1], f[2]);
|
||||
}
|
||||
// add top faces
|
||||
for (i = 0; i < faces.length; i++) {
|
||||
f = faces[i];
|
||||
indices.push(f[0] + left.length, f[1] + left.length, f[2] + left.length);
|
||||
}
|
||||
// add side faces
|
||||
for (i = 0; i < left.length - 1; i++) {
|
||||
j = i * 2;
|
||||
k = j + 2;
|
||||
indices.push(j, k, j + 1, j + 1, k, k + 1);
|
||||
}
|
||||
return { vertices, normals, uvs, indices };
|
||||
}
|
||||
},
|
||||
newPoint
|
||||
};
|
||||
|
||||
// Helper functions for paths
|
||||
function calc_normal(p1, p2) {
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = p2.y - p1.y;
|
||||
let len = Math.sqrt(dx * dx + dy * dy);
|
||||
let mn = (1 / len);
|
||||
dx *= mn;
|
||||
dy *= mn;
|
||||
return({ dx: dy, dy: -dx, p1, p2, len });
|
||||
}
|
||||
|
||||
function end_vertex(n1, n2, off, start) {
|
||||
let dx, dy;
|
||||
if (start) {
|
||||
dx = n2.dx * off;
|
||||
dy = n2.dy * off;
|
||||
} else {
|
||||
dx = n1.dx * off;
|
||||
dy = n1.dy * off;
|
||||
}
|
||||
return { dx, dy, vp: n1.p2 };
|
||||
}
|
||||
|
||||
function calc_vertex(n1, n2, off, vp) {
|
||||
let dx, dy, io, vl, q, r;
|
||||
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
|
||||
q = off / r;
|
||||
// handle spurs that switch back 180 degrees
|
||||
if (q === Infinity) {
|
||||
q = 0;
|
||||
}
|
||||
dx = (n1.dx + n2.dx) * q;
|
||||
dy = (n1.dy + n2.dy) * q;
|
||||
// io tells us whether we're turning left or right
|
||||
io = (n1.dx * n2.dy - n2.dx * n1.dy);
|
||||
// vertex length can be compared to the previons and next
|
||||
// segment lengths to see if we're highly acute
|
||||
vl = Math.sqrt(dx * dx + dy * dy);
|
||||
return { dx, dy, vp: vp || n1.p2, io, vl };
|
||||
}
|
||||
|
||||
function v2pl(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x += rec.dx;
|
||||
p.y += rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
function v2pr(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x -= rec.dx;
|
||||
p.y -= rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
const earcut = window.earcut;
|
||||
|
||||
// Export the base object and its properties
|
||||
export { base };
|
||||
export { earcut };
|
||||
export const { config, key, util, paths } = base;
|
||||
export { newPoint };
|
||||
133
src2/geo/bounds.js
Normal file
133
src2/geo/bounds.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
import { config, util } from './base.js';
|
||||
import { newPoint } from './point.js';
|
||||
|
||||
export class Bounds {
|
||||
constructor() {
|
||||
this.minx = 10e7;
|
||||
this.miny = 10e7;
|
||||
this.maxx = -10e7;
|
||||
this.maxy = -10e7;
|
||||
}
|
||||
|
||||
set(minx, maxx, miny, maxy) {
|
||||
this.minx = minx;
|
||||
this.miny = miny;
|
||||
this.maxx = maxx;
|
||||
this.maxy = maxy;
|
||||
return this;
|
||||
}
|
||||
|
||||
clone() {
|
||||
let b = new Bounds();
|
||||
b.minx = this.minx;
|
||||
b.miny = this.miny;
|
||||
b.maxx = this.maxx;
|
||||
b.maxy = this.maxy;
|
||||
b.maxy = this.maxy;
|
||||
return b;
|
||||
}
|
||||
|
||||
equals(bounds, margin) {
|
||||
if (!margin) margin = config.precision_offset;
|
||||
return util.isCloseTo(this.minx, bounds.minx, margin) &&
|
||||
util.isCloseTo(this.miny, bounds.miny, margin) &&
|
||||
util.isCloseTo(this.maxx, bounds.maxx, margin) &&
|
||||
util.isCloseTo(this.maxy, bounds.maxy, margin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number} absolute delta in x,y coordinate space
|
||||
*/
|
||||
delta(bounds) {
|
||||
return 0 +
|
||||
Math.abs(this.minx - bounds.minx) +
|
||||
Math.abs(this.miny - bounds.miny) +
|
||||
Math.abs(this.maxx - bounds.maxx) +
|
||||
Math.abs(this.maxy - bounds.maxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} b
|
||||
*/
|
||||
merge(b) {
|
||||
this.minx = Math.min(this.minx, b.minx);
|
||||
this.maxx = Math.max(this.maxx, b.maxx);
|
||||
this.miny = Math.min(this.miny, b.miny);
|
||||
this.maxy = Math.max(this.maxy, b.maxy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} p
|
||||
*/
|
||||
update(p) {
|
||||
this.minx = Math.min(this.minx, p.x);
|
||||
this.maxx = Math.max(this.maxx, p.x);
|
||||
this.miny = Math.min(this.miny, p.y);
|
||||
this.maxy = Math.max(this.maxy, p.y);
|
||||
return this;
|
||||
}
|
||||
|
||||
contains(bounds) {
|
||||
return bounds.isNested(this);
|
||||
}
|
||||
|
||||
containsXY(x, y) {
|
||||
return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy;
|
||||
}
|
||||
|
||||
containsOffsetXY(x, y, offset) {
|
||||
return x >= this.minx - offset && x <= this.maxx + offset && y >= this.miny - offset && y <= this.maxy + offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} parent
|
||||
* @returns {boolean} true if fully inside parent bounds
|
||||
*/
|
||||
isNested(parent, precision = config.precision_bounds) {
|
||||
return (
|
||||
this.minx >= parent.minx - precision && // min-x
|
||||
this.maxx <= parent.maxx + precision && // max-x
|
||||
this.miny >= parent.miny - precision && // min-y
|
||||
this.maxy <= parent.maxy + precision // max-y
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} b
|
||||
* @param {number} precision
|
||||
* @returns {boolean}
|
||||
*/
|
||||
overlaps(b, precision = config.precision_bounds) {
|
||||
return (
|
||||
Math.abs(this.centerx() - b.centerx()) * 2 - precision < this.width() + b.width() &&
|
||||
Math.abs(this.centery() - b.centery()) * 2 - precision < this.height() + b.height()
|
||||
);
|
||||
}
|
||||
|
||||
width() {
|
||||
return this.maxx - this.minx;
|
||||
}
|
||||
|
||||
height() {
|
||||
return this.maxy - this.miny;
|
||||
}
|
||||
|
||||
center(z = 0) {
|
||||
return newPoint(this.centerx(), this.centery(), z);
|
||||
}
|
||||
|
||||
centerx() {
|
||||
return this.minx + this.width() / 2;
|
||||
}
|
||||
|
||||
centery() {
|
||||
return this.miny + this.height() / 2;
|
||||
}
|
||||
}
|
||||
|
||||
export function newBounds() {
|
||||
return new Bounds();
|
||||
}
|
||||
132
src2/geo/csg.js
Normal file
132
src2/geo/csg.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { base } from './base.js';
|
||||
import { THREE } from '../ext/three.js';
|
||||
import manifold from '../ext/manifold.js';
|
||||
const { Mesh, BufferGeometry, Vector3 } = THREE;
|
||||
|
||||
const debug = true;
|
||||
const precision = 0.001;
|
||||
const factor = 1/precision;
|
||||
|
||||
function log() {
|
||||
if (root?.mesh?.log) {
|
||||
root.mesh.log(...arguments)
|
||||
} else if (root.debug === true) {
|
||||
console.log(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
export const CSG = {
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
union() {
|
||||
return CSG.moduleOp('manifold.union', 'union', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
// the fist argument is the target mesh. the rest are negatives
|
||||
subtract() {
|
||||
return CSG.moduleOp('manifold.subtract', 'difference', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
intersect() {
|
||||
return CSG.moduleOp('manifold.intersect', 'intersection', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
difference() {
|
||||
return CSG.moduleOp('manifold.difference', 'difference', ...arguments);
|
||||
},
|
||||
|
||||
moduleOp(name, op) {
|
||||
log(`${name} ${arguments.length} meshes`);
|
||||
const args = [...arguments].slice(2).map(a => new Instance.Manifold(a));
|
||||
if (args.length < 2) {
|
||||
throw "missing one or more meshes";
|
||||
}
|
||||
const result = Instance.Manifold[op](...args);
|
||||
const output = result.getMesh();
|
||||
const errors = [ ...args, result ].map(m => m.status().value);
|
||||
for (let m of [ ...args, result ]) {
|
||||
m.delete();
|
||||
}
|
||||
if (errors.reduce((a,b) => a+b)) {
|
||||
throw `${errors}`;
|
||||
}
|
||||
return output;
|
||||
},
|
||||
|
||||
toPositionArray(mesh) {
|
||||
let { vertProperties, triVerts } = mesh;
|
||||
let vertPos = vertProperties;
|
||||
if (vertPos && triVerts) {
|
||||
const vertices = new Float32Array(triVerts.length * 3);
|
||||
for (let p=0, i=0, l=triVerts.length; i<l; i++) {
|
||||
let vi = triVerts[i] * 3;
|
||||
vertices[p++] = vertPos[vi++];
|
||||
vertices[p++] = vertPos[vi++];
|
||||
vertices[p++] = vertPos[vi++];
|
||||
}
|
||||
return vertices.map(v => v/factor);
|
||||
}
|
||||
throw "mesh missing required fields";
|
||||
},
|
||||
|
||||
fromPositionArray(pos) {
|
||||
const { index, vertices } = indexVertices(pos);
|
||||
let mesh = new Instance.Mesh({
|
||||
vertProperties: vertices.map(v => (v*factor)|0).toFloat32(),
|
||||
triVerts: index.toUint32()
|
||||
});
|
||||
return mesh;
|
||||
},
|
||||
|
||||
toBox3(mesh) {
|
||||
const { min, max } = mesh._boundingBox();
|
||||
const vmin = new THREE.Vector3(min.x, min.y, min.z);
|
||||
const vmax = new THREE.Vector3(max.x, max.y, max.z);
|
||||
return new THREE.Box3(vmin, vmax);
|
||||
},
|
||||
|
||||
indexVertices
|
||||
};
|
||||
|
||||
function indexVertices(pos) {
|
||||
log(`indexing ${pos.length/3} vertices`);
|
||||
let ipos = 0;
|
||||
const index = [];
|
||||
const vertices = [];
|
||||
const cache = {};
|
||||
const temp = { x: 0, y: 0, z: 0 };
|
||||
for (let i=0, length = pos.length; i<length; ) {
|
||||
temp.x = pos[i++];
|
||||
temp.y = pos[i++];
|
||||
temp.z = pos[i++];
|
||||
let key = [
|
||||
((temp.x * 100000) | 0),
|
||||
((temp.y * 100000) | 0),
|
||||
((temp.z * 100000) | 0)
|
||||
].join('');
|
||||
let ip = cache[key];
|
||||
if (ip >= 0) {
|
||||
index.push(ip);
|
||||
} else {
|
||||
index.push(ipos);
|
||||
cache[key] = (ipos++);
|
||||
vertices.push(temp.x, temp.y, temp.z);
|
||||
}
|
||||
}
|
||||
return { index, vertices };
|
||||
}
|
||||
|
||||
let Instance;
|
||||
manifold({
|
||||
locateFile() { return "/wasm/manifold.wasm" }
|
||||
}).then(inst => {
|
||||
inst.setup();
|
||||
Instance = inst;
|
||||
CSG.Instance = () => { return Instance };
|
||||
});
|
||||
209
src2/geo/gyroid.js
Normal file
209
src2/geo/gyroid.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/** Copyright Stewart Allen -- All Rights Reserved */
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
|
||||
let cache = {};
|
||||
let lastVal;
|
||||
let lastRes = 0;
|
||||
let lastSlice = 0;
|
||||
|
||||
/**
|
||||
* @param off {number} z offset value from 0-1
|
||||
* @param res {number} resolution (pixels/slices per side)
|
||||
*/
|
||||
export function slice(off, res, val) {
|
||||
// auto clear cach if it hasn't been hit in the last 20 seconds
|
||||
// or the requested resolution or tip values have changed
|
||||
let now = Date.now();
|
||||
if (res !== lastRes || val !== lastVal || now - lastSlice > 20000) {
|
||||
cache = {};
|
||||
}
|
||||
lastVal = val;
|
||||
lastRes = res;
|
||||
lastSlice = now;
|
||||
let rez = parseInt(res || 200);
|
||||
let inc = PI2 / rez;
|
||||
let z = PI2 * off;
|
||||
let key = (z % PI2).round(3);
|
||||
let hit = cache[key];
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
let tip = val || 0;
|
||||
let edge = [];
|
||||
let vals = [];
|
||||
let points = 0;
|
||||
let points_lr = 0;
|
||||
let points_td = 0;
|
||||
for (let x=0; x<PI2; x += inc) {
|
||||
let vrow = []; // raw values row
|
||||
let erow = []; // edge values row
|
||||
edge.push(erow);
|
||||
vals.push(vrow);
|
||||
for (let y=0; y<PI2; y += inc) {
|
||||
erow.push(0);
|
||||
vrow.push(
|
||||
Math.sin(x) * Math.cos(y) +
|
||||
Math.sin(y) * Math.cos(z) +
|
||||
Math.sin(z) * Math.cos(x)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// left-right threshold search (red)
|
||||
vals.forEach((vrow, y) => {
|
||||
let erow = edge[y];
|
||||
let lval = vrow[vrow.length - 1];
|
||||
vrow.forEach((val, x) => {
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
erow[x] = 1;
|
||||
points++;
|
||||
points_lr++;
|
||||
}
|
||||
lval = val;
|
||||
})
|
||||
});
|
||||
|
||||
// top-down threshold search (green)
|
||||
for (let x=0; x<rez; x++) {
|
||||
let lval = vals[vals.length-1][x];
|
||||
for (let y=0; y<rez; y++) {
|
||||
let val = vals[y][x];
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
if (edge[y][x]) {
|
||||
edge[y][x] = 3;
|
||||
} else {
|
||||
edge[y][x] = 2;
|
||||
points++
|
||||
}
|
||||
points_td++;
|
||||
}
|
||||
lval = val;
|
||||
}
|
||||
}
|
||||
|
||||
// deterime prevailing direction for chaining
|
||||
let dir = points_td > points_lr ? 'lr' : 'td';
|
||||
|
||||
// create sparse representation
|
||||
let sparse = [];
|
||||
let center = rez / 2;
|
||||
edge.forEach((row,y) => {
|
||||
row.forEach((val,x) => {
|
||||
if (val) {
|
||||
let dx = Math.abs(x - center);
|
||||
let dy = Math.abs(y - center);
|
||||
sparse.push({x: x/rez, y: y/rez, val, dist: Math.max(dx,dy)});
|
||||
}
|
||||
});
|
||||
});
|
||||
sparse.sort((a,b) => {
|
||||
return b.dist - a.dist;
|
||||
});
|
||||
|
||||
// join sparse points array by closest distance
|
||||
let polys = [];
|
||||
let chain;
|
||||
let added;
|
||||
let cleared = 0;
|
||||
let maxdist = 0.05;
|
||||
|
||||
do {
|
||||
chain = null;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
if (sparse[i]) {
|
||||
chain = [ sparse[i] ];
|
||||
polys.push(chain);
|
||||
sparse[i] = null;
|
||||
cleared++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
do {
|
||||
added = false;
|
||||
let target = chain[chain.length - 1];
|
||||
let cl_elm = null;
|
||||
let cl_idx = null;
|
||||
let cl_dst = Infinity;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
let test_el = sparse[i];
|
||||
if (test_el) {
|
||||
let dst = distTo(target, test_el, dir);
|
||||
if (cl_idx === null || dst < cl_dst) {
|
||||
cl_idx = i;
|
||||
cl_elm = test_el;
|
||||
cl_dst = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cl_elm) {
|
||||
if (cl_dst > maxdist) {
|
||||
break;
|
||||
}
|
||||
sparse[cl_idx] = null;
|
||||
cleared++;
|
||||
chain.push(cl_elm);
|
||||
added = true;
|
||||
}
|
||||
} while (added);
|
||||
} while (cleared < sparse.length);
|
||||
|
||||
let psimple = polys
|
||||
.map(poly => filter(poly, 0))
|
||||
.map(poly => filter(poly, inc));
|
||||
|
||||
let slice = {edge, points, dir, polys: psimple};
|
||||
cache[key] = slice;
|
||||
return slice;
|
||||
}
|
||||
|
||||
// merge co-linear and distance threshold
|
||||
export function filter(poly, inc) {
|
||||
if (poly.length <= 2) {
|
||||
return poly;
|
||||
}
|
||||
let nupoly = [ poly[0] ];
|
||||
let e1 = poly[1];
|
||||
let e2 = null;
|
||||
let last = poly.length - 2;
|
||||
for (let i=1; i<poly.length; i++) {
|
||||
let el = poly[i];
|
||||
let drop = inc ?
|
||||
(distTo(e1, el) <= inc) :
|
||||
(e1.x === el.x || e1.y === el.y);
|
||||
if (drop) {
|
||||
e2 = el;
|
||||
if (i < last) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (e2) {
|
||||
nupoly.push({x:(e1.x + e2.x)/2, y:(e1.y + e2.y)/2});
|
||||
e2 = null;
|
||||
} else {
|
||||
nupoly.push(e1);
|
||||
if (i === last) {
|
||||
nupoly.push(el);
|
||||
}
|
||||
}
|
||||
e1 = el;
|
||||
}
|
||||
return nupoly;
|
||||
}
|
||||
|
||||
export function distTo(a, b, dir) {
|
||||
let dx = a.x - b.x;
|
||||
let dy = a.y - b.y;
|
||||
if (dir === 'lr') {
|
||||
return Math.abs(dx);
|
||||
} else if (dir === 'td') {
|
||||
return Math.abs(dy);
|
||||
}
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
56
src2/geo/line.js
Normal file
56
src2/geo/line.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
export class Line {
|
||||
constructor(p1, p2, key) {
|
||||
if (!key) key = [p1.key, p2.key].join(';');
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
length() {
|
||||
return Math.sqrt(this.length2());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} square of length
|
||||
*/
|
||||
length2() {
|
||||
return this.p1.distToSq2D(this.p2);
|
||||
}
|
||||
|
||||
slope() {
|
||||
return base.newSlope(this.p1.slopeTo(this.p2));
|
||||
}
|
||||
|
||||
reverse() {
|
||||
let t = this.p1;
|
||||
this.p1 = this.p2;
|
||||
this.p2 = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
midpoint() {
|
||||
return this.p1.midPointTo(this.p2);
|
||||
}
|
||||
|
||||
isCollinear(line) {
|
||||
let p1 = this.p1,
|
||||
p2 = this.p2,
|
||||
p3 = line.p1,
|
||||
p4 = line.p2,
|
||||
d1x = (p2.x - p1.x),
|
||||
d1y = (p2.y - p1.y),
|
||||
d2x = (p4.x - p3.x),
|
||||
d2y = (p4.y - p3.y);
|
||||
return Math.abs( (d2y * d1x) - (d2x * d1y) ) < 0.0001;
|
||||
}
|
||||
}
|
||||
|
||||
export function newLine(p1, p2, key) {
|
||||
return new Line(p1, p2, key);
|
||||
}
|
||||
|
||||
export function newOrderedLine(p1, p2, key) {
|
||||
return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key);
|
||||
}
|
||||
553
src2/geo/paths.js
Normal file
553
src2/geo/paths.js
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base } from './base.js';
|
||||
|
||||
const { util, config } = base;
|
||||
const { sqr, numOrDefault } = util;
|
||||
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* emit each element in an array based on
|
||||
* the next closest endpoint. arrays contain
|
||||
* elements with { first, last } points and
|
||||
* may be open polys, unlike poly2polyEmit
|
||||
*/
|
||||
export function tip2tipEmit(array, startPoint, emitter) {
|
||||
let mindist, dist, found, count = 0;
|
||||
for (;;) {
|
||||
found = null;
|
||||
mindist = Infinity;
|
||||
array.forEach(function(el) {
|
||||
if (el.delete) return;
|
||||
dist = startPoint.distTo2D(el.first);
|
||||
if (dist < mindist) {
|
||||
found = {el:el, first:el.first, last:el.last};
|
||||
mindist = dist;
|
||||
}
|
||||
dist = startPoint.distTo2D(el.last);
|
||||
if (dist < mindist) {
|
||||
found = {el:el, first:el.last, last:el.first};
|
||||
mindist = dist;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
found.el.delete = true;
|
||||
startPoint = found.last;
|
||||
emitter(found.el, found.first, ++count);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return startPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* like tip2tipEmit but accepts an array of polygons and the next closest
|
||||
* point can be anywhere in the adjacent polygon. should be re-written
|
||||
* to be more like outputOrderClosest() and have the option to account for
|
||||
* depth in determining distance
|
||||
*/
|
||||
export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
|
||||
let marker = opt.mark || 'delete';
|
||||
let mindist, dist, found, count = 0;
|
||||
for (;;) {
|
||||
found = null;
|
||||
mindist = Infinity;
|
||||
for (let poly of array) {
|
||||
if (poly[marker]) {
|
||||
continue;
|
||||
}
|
||||
if (poly.isOpen()) {
|
||||
const d2f = startPoint.distTo2D(poly.first());
|
||||
const d2l = startPoint.distTo2D(poly.last());
|
||||
if (d2f > mindist && d2l > mindist) {
|
||||
continue;
|
||||
}
|
||||
if (d2l < mindist && d2l < d2f && opt.swapdir !== false) {
|
||||
poly.reverse();
|
||||
found = {poly:poly, index:0, point:poly.first()};
|
||||
mindist = d2l;
|
||||
} else if (d2f < mindist) {
|
||||
found = {poly:poly, index:0, point:poly.first()};
|
||||
mindist = d2f;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let area = poly.open ? 1 : poly.area();
|
||||
poly.forEachPoint(function(point, index) {
|
||||
dist = opt.weight ?
|
||||
startPoint.distTo3D(point) * area * area :
|
||||
startPoint.distTo2D(point);
|
||||
if (dist < mindist) {
|
||||
found = {poly:poly, index:index, point:point};
|
||||
mindist = dist;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!found || opt.term) {
|
||||
break;
|
||||
}
|
||||
found.poly[marker] = true;
|
||||
startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point;
|
||||
}
|
||||
|
||||
// undo delete marks
|
||||
if (opt.perm !== true) {
|
||||
array.forEach(function(poly) { poly[marker] = false });
|
||||
}
|
||||
|
||||
return startPoint;
|
||||
}
|
||||
|
||||
export function calc_normal(p1, p2) {
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = p2.y - p1.y;
|
||||
let len = Math.sqrt(dx * dx + dy * dy);
|
||||
let mn = (1 / len);
|
||||
dx *= mn;
|
||||
dy *= mn;
|
||||
return({ dx: dy, dy: -dx, p1, p2, len });
|
||||
}
|
||||
|
||||
export function end_vertex(n1, n2, off, start) {
|
||||
let dx, dy;
|
||||
if (start) {
|
||||
dx = n2.dx * off;
|
||||
dy = n2.dy * off;
|
||||
} else {
|
||||
dx = n1.dx * off;
|
||||
dy = n1.dy * off;
|
||||
}
|
||||
return { dx, dy, vp: n1.p2 };
|
||||
}
|
||||
|
||||
export function calc_vertex(n1, n2, off, vp) {
|
||||
let dx, dy, io, vl, q, r;
|
||||
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
|
||||
q = off / r;
|
||||
// handle spurs that switch back 180 degrees
|
||||
if (q === Infinity) {
|
||||
q = 0;
|
||||
}
|
||||
dx = (n1.dx + n2.dx) * q;
|
||||
dy = (n1.dy + n2.dy) * q;
|
||||
// io tells us whether we're turning left or right
|
||||
io = (n1.dx * n2.dy - n2.dx * n1.dy);
|
||||
// vertex length can be compared to the previons and next
|
||||
// segment lengths to see if we're highly acute
|
||||
vl = Math.sqrt(dx * dx + dy * dy);
|
||||
return { dx, dy, vp: vp || n1.p2, io, vl };
|
||||
}
|
||||
|
||||
export function v2pl(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x += rec.dx;
|
||||
p.y += rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function v2pr(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x -= rec.dx;
|
||||
p.y -= rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function pointsToPath(points, offset, open, miter = 1.5) {
|
||||
const absoff = Math.abs(offset);
|
||||
// calculate segment normals which are used to calculate vertex normals
|
||||
// next segment info is associated with the current point
|
||||
const nupoints = [];
|
||||
const length = points.length;
|
||||
if (length === 2 && points[0].isEqual(points[1])) {
|
||||
return { };
|
||||
}
|
||||
const dedup = (open && length > 2) || (!open && length > 3);
|
||||
for (let i=0; i<length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
p1.normal = calc_normal(p1, p2);
|
||||
// drop next duplicate point if segment length is 0
|
||||
// is possible there are 3 dups in a row and this is
|
||||
// not handled. could make if a while, but that could
|
||||
// end up in a loop without additional checks. ignore
|
||||
// the case when the last and first point are the same
|
||||
// which is valid when the line is an open path
|
||||
if (dedup && p1.normal.len === 0 && i !== length - 1) {
|
||||
p1.normal = calc_normal(p1, points[(i + 2) % length]);
|
||||
i++;
|
||||
}
|
||||
nupoints.push(p1);
|
||||
}
|
||||
if (nupoints.length === 1) {
|
||||
console.log({points, nupoints});
|
||||
}
|
||||
// when points are dropped, we need the new array
|
||||
points = nupoints;
|
||||
// generate left / right paths and triangle faces
|
||||
const left = [];
|
||||
const right = [];
|
||||
const faces = [];
|
||||
const normals = [];
|
||||
const zn = -1;
|
||||
// calculate vertex normals from segments normals
|
||||
// vertex info is associated with the origin point
|
||||
let fl, fr;
|
||||
for (let i=0, l=points.length; i<l; i++) {
|
||||
let n1 = points[(i+l-1)%l].normal;
|
||||
let n2 = points[(i+l)%l].normal;
|
||||
let vn = open && (i === 0 || i === l-1) ?
|
||||
end_vertex(n1, n2, offset, i === 0) :
|
||||
calc_vertex(n1, n2, offset);
|
||||
let { p1, p2 } = n2;
|
||||
let { io, vl } = vn;
|
||||
if (offset < 0) {
|
||||
io = -io;
|
||||
}
|
||||
let split_left = false, split_right = false;
|
||||
if (io > 0) { // right
|
||||
split_left = vl > absoff * miter;
|
||||
split_right = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
} else { // left
|
||||
split_right = vl > absoff * miter;
|
||||
split_left = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
}
|
||||
let l0 = left.peek(1);
|
||||
let r0 = right.peek(1);
|
||||
if (split_left || split_right) {
|
||||
// shorten each leg and insert new point
|
||||
let delta = 0.1;
|
||||
let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 });
|
||||
let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 });
|
||||
let sn1 = np1.normal = calc_normal(np1, np2);
|
||||
let sn2 = np2.normal = p1.normal;
|
||||
let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1);
|
||||
let nv2 = calc_vertex(sn1, sn2, offset, np2);
|
||||
if (split_right) {
|
||||
right.push(v2pr(nv1), v2pr(nv2));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(nv1), v2pl(nv2));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let l2 = left.peek(2);
|
||||
let r2 = right.peek(2);
|
||||
let ln = l1.vp.normal;
|
||||
let rn = r1.vp.normal;
|
||||
if (split_left && split_right) {
|
||||
faces.push(l1, l2, r1);
|
||||
faces.push(r2, r1, l2);
|
||||
fl = fl || l2;
|
||||
fr = fr || r2;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
} else if (split_left) {
|
||||
faces.push(l1, l2, r1);
|
||||
fl = fl || l2;
|
||||
fr = fr || r1;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
} else { // split right
|
||||
faces.push(r2, r1, l1);
|
||||
fl = fl || l1;
|
||||
fr = fr || r2;
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
right.push(v2pr(vn));
|
||||
fl = fl || left.peek(1);
|
||||
fr = fr || right.peek(1);
|
||||
}
|
||||
if (faces && l0 && r0) {
|
||||
let l1 = left.peek(split_left ? 2 : 1);
|
||||
let r1 = right.peek(split_right ? 2 : 1);
|
||||
faces.push(l1, l0, r1);
|
||||
faces.push(r0, r1, l0);
|
||||
let ln = l0.vp.normal;
|
||||
let rn = r0.vp.normal;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
}
|
||||
if (open) {
|
||||
// move open ends by offset length
|
||||
const p0 = points[0].normal;
|
||||
const l0 = left[0];
|
||||
const r0 = right[0];
|
||||
// improve visuals of open but 90 degree overlapping ends
|
||||
const move = offset * 0.99;
|
||||
l0.x += p0.dy * move;
|
||||
l0.y -= p0.dx * move;
|
||||
r0.x += p0.dy * move;
|
||||
r0.y -= p0.dx * move;
|
||||
const pn = points.peek(2).normal;
|
||||
const ln = left.peek();
|
||||
const rn = right.peek();
|
||||
ln.x -= pn.dy * move;
|
||||
ln.y += pn.dx * move;
|
||||
rn.x -= pn.dy * move;
|
||||
rn.y += pn.dx * move;
|
||||
}
|
||||
if (!open && faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let ln = l1.vp.normal;
|
||||
let rn = r1.vp.normal;
|
||||
faces.push(fl, l1, fr);
|
||||
faces.push(r1, fr, l1);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
|
||||
return { left, right, faces, normals, open };
|
||||
}
|
||||
|
||||
export function pathTo3D(path, height, z) {
|
||||
const { faces, normals, left, right, open } = path;
|
||||
const out = [];
|
||||
const nrm = [];
|
||||
if (!(faces && left && right)) {
|
||||
return [];
|
||||
}
|
||||
if (z !== undefined) {
|
||||
for (let p of faces) {
|
||||
p.z = z;
|
||||
}
|
||||
}
|
||||
for (let p of faces) {
|
||||
out.push(p.x, p.y, p.z - height);
|
||||
}
|
||||
for (let p of faces.slice().reverse()) {
|
||||
out.push(p.x, p.y, p.z + height);
|
||||
}
|
||||
nrm.appendAll(normals);
|
||||
// reverse normals to match faces, but underside so reverse Z as well
|
||||
for (let i=normals.length-1; i>0; i-=3) {
|
||||
nrm.push(normals[i-2]);
|
||||
nrm.push(normals[i-1]);
|
||||
nrm.push(-normals[i-0]);
|
||||
}
|
||||
for (let i=0, l=left.length, tl = open ? l-1 : l; i<tl; i++) {
|
||||
let p0 = left[i];
|
||||
let p1 = left[(i+1)%l];
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p0.x, p0.y, p0.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p1.x, p1.y, p1.z + height);
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
let ln = p0.vp.normal;
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
}
|
||||
for (let i=0, l=right.length, tl = open ? l-1 : l; i<tl; i++) {
|
||||
let p0 = right[i];
|
||||
let p1 = right[(i+1)%l];
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p0.x, p0.y, p0.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p1.x, p1.y, p1.z + height);
|
||||
let rn = p0.vp.normal;
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
}
|
||||
if (open) {
|
||||
// begin cap
|
||||
let l0 = left[0];
|
||||
let r0 = right[0];
|
||||
out.push(l0.x, l0.y, l0.z + height);
|
||||
out.push(r0.x, r0.y, r0.z - height);
|
||||
out.push(l0.x, l0.y, l0.z - height);
|
||||
out.push(r0.x, r0.y, r0.z + height);
|
||||
out.push(r0.x, r0.y, r0.z - height);
|
||||
out.push(l0.x, l0.y, l0.z + height);
|
||||
let ln = l0.vp.normal;
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
// end cap
|
||||
let le = left.peek();
|
||||
let re = right.peek();
|
||||
out.push(le.x, le.y, le.z + height);
|
||||
out.push(le.x, le.y, le.z - height);
|
||||
out.push(re.x, re.y, re.z - height);
|
||||
out.push(re.x, re.y, re.z + height);
|
||||
out.push(le.x, le.y, le.z + height);
|
||||
out.push(re.x, re.y, re.z - height);
|
||||
ln = re.vp.normal;
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
}
|
||||
return { faces: out, normals: nrm };
|
||||
}
|
||||
|
||||
// produces indexed geometry which isn't ideal for rendering because
|
||||
// the default threejs generated vertex normals aren't accurate
|
||||
export function shapeToPath(shape, points, closed) {
|
||||
closed = closed !== undefined ? closed : true;
|
||||
|
||||
const profileGeometry = new THREE.ShapeGeometry(shape);
|
||||
profileGeometry.rotateX(Math.PI * .5);
|
||||
|
||||
const profile = profileGeometry.attributes.position;
|
||||
const faces = new Float32Array(profile.count * points.length * 3);
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const v1 = new THREE.Vector2().subVectors(points[i - 1 < 0 ? points.length - 1 : i - 1], points[i]);
|
||||
const v2 = new THREE.Vector2().subVectors(points[i + 1 == points.length ? 0 : i + 1], points[i]);
|
||||
const angle = v2.angle() - v1.angle();
|
||||
const halfAngle = angle * .5;
|
||||
let hA = halfAngle;
|
||||
let tA = v2.angle() + Math.PI * .5;
|
||||
|
||||
if (!closed){
|
||||
if (i == 0 || i == points.length - 1) {hA = Math.PI * .5;}
|
||||
if (i == points.length - 1) {tA = v1.angle() - Math.PI * .5;}
|
||||
}
|
||||
|
||||
const shift = Math.tan(hA - Math.PI * .5);
|
||||
const shiftMatrix = new THREE.Matrix4().set(
|
||||
1, 0, 0, 0,
|
||||
-shift, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
|
||||
const tempAngle = tA;
|
||||
const rotationMatrix = new THREE.Matrix4().set(
|
||||
Math.cos(tempAngle), -Math.sin(tempAngle), 0, 0,
|
||||
Math.sin(tempAngle), Math.cos(tempAngle), 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
|
||||
const translationMatrix = new THREE.Matrix4().set(
|
||||
1, 0, 0, points[i].x,
|
||||
0, 1, 0, points[i].y,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
);
|
||||
|
||||
const cloneProfile = profile.clone();
|
||||
cloneProfile.applyMatrix4(shiftMatrix);
|
||||
cloneProfile.applyMatrix4(rotationMatrix);
|
||||
cloneProfile.applyMatrix4(translationMatrix);
|
||||
|
||||
faces.set(cloneProfile.array, cloneProfile.count * i * 3);
|
||||
}
|
||||
|
||||
const index = [];
|
||||
const lastCorner = closed == false ? points.length - 1: points.length;
|
||||
|
||||
for (let i = 0; i < lastCorner; i++) {
|
||||
for (let j = 0; j < profile.count; j++) {
|
||||
const currCorner = i;
|
||||
const nextCorner = i + 1 == points.length ? 0 : i + 1;
|
||||
const currPoint = j;
|
||||
const nextPoint = j + 1 == profile.count ? 0 : j + 1;
|
||||
|
||||
const a = nextPoint + profile.count * currCorner;
|
||||
const b = currPoint + profile.count * currCorner;
|
||||
const c = currPoint + profile.count * nextCorner;
|
||||
const d = nextPoint + profile.count * nextCorner;
|
||||
|
||||
index.push(a, b, d);
|
||||
index.push(b, c, d);
|
||||
}
|
||||
}
|
||||
|
||||
if (!closed) {
|
||||
// cheating because we know the profile length is 4 (for now)
|
||||
const p1 = 0 + profile.count * 0;
|
||||
const p2 = 1 + profile.count * 0;
|
||||
const p3 = 2 + profile.count * 0;
|
||||
const p4 = 3 + profile.count * 0;
|
||||
index.push(p1, p2, p3);
|
||||
index.push(p1, p3, p4);
|
||||
const lc = lastCorner;
|
||||
const p5 = 0 + profile.count * lc;
|
||||
const p6 = 1 + profile.count * lc;
|
||||
const p7 = 2 + profile.count * lc;
|
||||
const p8 = 3 + profile.count * lc;
|
||||
index.push(p7, p6, p5);
|
||||
index.push(p8, p7, p5);
|
||||
}
|
||||
|
||||
return {index, faces};
|
||||
}
|
||||
|
||||
export class FloatPacker {
|
||||
constructor(size, factor) {
|
||||
this.size = size;
|
||||
this.factor = Math.min(factor || 1.2, 1.1);
|
||||
this.array = new Float32Array(size);
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
push() {
|
||||
const array = this.array;
|
||||
const size = this.size;
|
||||
const args = arguments.length;
|
||||
if (this.pos + args >= size) {
|
||||
let nusize = ((size * this.factor) | 0) + args;
|
||||
let nuarray = new Float32Array(nusize);
|
||||
nuarray.set(array);
|
||||
this.array = nuarray;
|
||||
this.size = nusize;
|
||||
}
|
||||
for (let i=0; i<args; i++) {
|
||||
array[this.pos++] = arguments[i];
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if (this.pos / this.size >= 0.9) {
|
||||
return this.array.subarray(0, this.pos);
|
||||
} else {
|
||||
return this.array.slice(0, this.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
633
src2/geo/point.js
Normal file
633
src2/geo/point.js
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Vector3 } = THREE;
|
||||
|
||||
import { util, config, key } from './base.js';
|
||||
|
||||
export class Point {
|
||||
constructor(x = 0, y = 0, z = 0, key) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
if (key) {
|
||||
this._key = key;
|
||||
}
|
||||
}
|
||||
|
||||
get key() {
|
||||
if (this._key) {
|
||||
return this._key;
|
||||
}
|
||||
return this._key = [
|
||||
((this.x * 100000) | 0),
|
||||
((this.y * 100000) | 0),
|
||||
((this.z * 100000) | 0)
|
||||
].join('');
|
||||
}
|
||||
|
||||
toClipper() {
|
||||
return {
|
||||
X: this.x * config.clipper,
|
||||
Y: this.y * config.clipper
|
||||
};
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [ this.x, this.y, this.z ];
|
||||
}
|
||||
|
||||
toVector3() {
|
||||
return new THREE.Vector3(this.x, this.y, this.z);
|
||||
}
|
||||
|
||||
set(x, y, z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
delete this._key;
|
||||
return this;
|
||||
}
|
||||
|
||||
setX(x) {
|
||||
this.x = x;
|
||||
return this;
|
||||
}
|
||||
|
||||
setY(y) {
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
setZ(z) {
|
||||
this.z = z;
|
||||
return this;
|
||||
}
|
||||
|
||||
setA(a) {
|
||||
this.a = a;
|
||||
return this;
|
||||
}
|
||||
|
||||
swapXZ() {
|
||||
let p = this,
|
||||
t = p.x;
|
||||
p.x = p.z;
|
||||
p.z = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
swapYZ() {
|
||||
let p = this,
|
||||
t = p.y;
|
||||
p.y = p.z;
|
||||
p.z = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
scale(x = 1, y = 1, z = 1) {
|
||||
this.x *= x;
|
||||
this.y *= y;
|
||||
this.z *= z;
|
||||
return this;
|
||||
}
|
||||
|
||||
round(precision) {
|
||||
return newPoint(
|
||||
this.x.round(precision),
|
||||
this.y.round(precision),
|
||||
this.z.round(precision));
|
||||
}
|
||||
|
||||
addFacet(facet) {
|
||||
if (!this.group) this.group = [];
|
||||
this.group.push(facet);
|
||||
return this;
|
||||
}
|
||||
|
||||
rekey() {
|
||||
this._key = undefined;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
clone() {
|
||||
let p = newPoint(this.x, this.y, this.z, this._key);
|
||||
if (this.a !== undefined) {
|
||||
p.a = this.a;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
slopeTo(p) {
|
||||
return base.newSlope(this, p);
|
||||
}
|
||||
|
||||
lineTo(p, k) {
|
||||
return base.newLine(this, p, k);
|
||||
}
|
||||
|
||||
isNear(p, dist) {
|
||||
return util.isCloseTo(this.x, p.x, dist) && util.isCloseTo(this.y, p.y, dist);
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance to line connecting points p1, p2
|
||||
* distance is calculated on the perpendicular (normal) to line
|
||||
*
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @returns {number}
|
||||
*/
|
||||
distToLine(p1, p2) {
|
||||
return Math.sqrt(this.distToLineSq(p1, p2));
|
||||
}
|
||||
|
||||
distToLine3D(lp1, lp2) {
|
||||
// Convert points to vectors
|
||||
const p0 = [this.x, this.y, this.z];
|
||||
const p1 = [lp1.x, lp1.y, lp1.z];
|
||||
const p2 = [lp2.x, lp2.y, lp2.z];
|
||||
|
||||
// Calculate the direction vector of the line
|
||||
const lineDir = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
||||
|
||||
// Calculate the vector from p1 to the point
|
||||
const p1ToP = [p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
|
||||
|
||||
// Calculate the cross product of lineDir and p1ToP
|
||||
const crossProd = [
|
||||
lineDir[1] * p1ToP[2] - lineDir[2] * p1ToP[1],
|
||||
lineDir[2] * p1ToP[0] - lineDir[0] * p1ToP[2],
|
||||
lineDir[0] * p1ToP[1] - lineDir[1] * p1ToP[0]
|
||||
];
|
||||
|
||||
// Calculate the magnitude of the cross product
|
||||
const crossProdMag = Math.sqrt(crossProd[0] ** 2 + crossProd[1] ** 2 + crossProd[2] ** 2);
|
||||
|
||||
// Calculate the magnitude of the direction vector of the line
|
||||
const lineDirMag = Math.sqrt(lineDir[0] ** 2 + lineDir[1] ** 2 + lineDir[2] ** 2);
|
||||
|
||||
// Distance from point to line is the magnitude of the cross product divided by the magnitude of the direction vector
|
||||
const distance = crossProdMag / lineDirMag;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* used exclusively in new fill code. output does not agree with
|
||||
* old distToLine, but is the only method that seems to work for
|
||||
* fill. using new distToLine as a global replacement breaks support
|
||||
* offset clipping. both need to be investigated and a single line
|
||||
* normal distance needs to be formulated to replace both functions.
|
||||
*/
|
||||
distToLineNew(p1, p2) {
|
||||
return p2l(this, p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* return square of distance to line connecting points p1, p2
|
||||
* distance is calculated on the perpendicular (normal) to line
|
||||
*/
|
||||
distToLineSq(p1, p2) {
|
||||
let p = this,
|
||||
d = util.distSq(p1, p2);
|
||||
|
||||
let t = ((p.x - p1.x) * (p2.x - p1.x) + (p.y - p1.y) * (p2.y - p1.y)) / d;
|
||||
|
||||
if (t < 0) return util.distSq(p, p1);
|
||||
if (t > 1) return util.distSq(p, p2);
|
||||
|
||||
return util.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y));
|
||||
}
|
||||
|
||||
withinDist2(p1, p2, dist2) {
|
||||
let ll2 = p1.distToSq2D(p2),
|
||||
dp1 = this.distToSq2D(p1),
|
||||
dp2 = this.distToSq2D(p2);
|
||||
// if the line segment described is less than dist2
|
||||
// then add dist2 to ll2. if this point is not closer
|
||||
// than newll2 to either point, then it can't be closer
|
||||
// to the described segment than dist2
|
||||
if (ll2 < dist2) {
|
||||
ll2 += dist2;
|
||||
if (dp1 > ll2 && dp2 > ll2) return false;
|
||||
}
|
||||
// if point is farther from each point that the distance
|
||||
// between the points and that distance is greater than dist2
|
||||
// then it's not possible for the point to be closer than
|
||||
// dist2 to the described line segment.
|
||||
if (dp1 > ll2 && dp2 > ll2) return false;
|
||||
return this.distToLineSq(p1, p2) < dist2;
|
||||
}
|
||||
|
||||
midPointTo(p2) {
|
||||
return newPoint((this.x + p2.x) / 2, (this.y + p2.y) / 2, this.z);
|
||||
}
|
||||
|
||||
midPointTo3D(p2) {
|
||||
return newPoint(
|
||||
(this.x + p2.x) / 2,
|
||||
(this.y + p2.y) / 2,
|
||||
(this.z + p2.z) / 2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* non-scale corrected version of follow()
|
||||
*/
|
||||
projectOnSlope(slope, mult) {
|
||||
return newPoint(
|
||||
this.x + slope.dx * mult,
|
||||
this.y + slope.dy * mult,
|
||||
this.z);
|
||||
}
|
||||
|
||||
followTo(point, mult) {
|
||||
return this.follow(this.slopeTo(point), mult);
|
||||
}
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are closer to this point.
|
||||
*/
|
||||
offsetPointFrom(p2, dist) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y,
|
||||
ls = dist / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls;
|
||||
return newPoint(p2.x - ox, p2.y - oy, p2.z, key.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are farther from this point.
|
||||
*/
|
||||
offsetPointTo(p2, dist) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y;
|
||||
|
||||
if (dx === 0 && dy === 0) return this;
|
||||
|
||||
let ls = dist / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls;
|
||||
|
||||
return newPoint(p1.x + ox, p1.y + oy, p2.z, key.NONE);
|
||||
}
|
||||
|
||||
offsetLineTo(p2, offset) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y,
|
||||
ls = offset / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls,
|
||||
np1 = newPoint(p1.x - oy, p1.y + ox, p1.z, key.NONE),
|
||||
np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE);
|
||||
np1.op = p1;
|
||||
np2.op = p2;
|
||||
return base.newLine(np1, np2, key.NONE);
|
||||
}
|
||||
|
||||
offset(x, y, z) {
|
||||
return newPoint(this.x + x, this.y + y, this.z + z);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a point is inside of a polygon
|
||||
* does not check children/holes
|
||||
*/
|
||||
inPolygon(poly) {
|
||||
if (!poly.bounds.containsXY(this.x, this.y)) return false;
|
||||
|
||||
let p = poly.points,
|
||||
pl = p.length,
|
||||
p1, p2, i, inside = false;
|
||||
|
||||
for (i = 0; i < pl; i++) {
|
||||
p1 = p[i];
|
||||
p2 = p[(i + 1) % pl];
|
||||
if ((p1.y >= this.y) != (p2.y >= this.y) &&
|
||||
(this.x <= (p2.x - p1.x) * (this.y - p1.y) / (p2.y - p1.y) + p1.x)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the point is inside of a polygon but
|
||||
* not inside any of it's children
|
||||
*/
|
||||
isInPolygon(poly) {
|
||||
let point = this,
|
||||
i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i = 0; i < poly.length; i++) {
|
||||
if (point.isInPolygon(poly[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let holes = poly.inner;
|
||||
if (point.inPolygon(poly) || point.nearPolygon(poly, config.precision_merge_sq)) {
|
||||
for (i = 0; holes && i < holes.length; i++) {
|
||||
if (point.inPolygon(holes[i]) && !point.nearPolygon(holes[i], config.precision_merge_sq)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the point is inside of a polygon but
|
||||
* not inside any of it's children
|
||||
*/
|
||||
isInPolygonOnly(poly) {
|
||||
let point = this,
|
||||
i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i = 0; i < poly.length; i++) {
|
||||
if (point.isInPolygonOnly(poly[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let holes = poly.inner;
|
||||
if (point.inPolygon(poly)) {
|
||||
for (i = 0; holes && i < holes.length; i++) {
|
||||
if (point.inPolygon(holes[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if point is near polygon edge. distance is squared.
|
||||
* @param {boolean} [inner] process inner polygons
|
||||
*/
|
||||
nearPolygon(poly, dist2, inner) {
|
||||
// throw new Error("nearPolygon");
|
||||
for (let i = 0, p = poly.points, pl = p.length; i < pl; i++) {
|
||||
if (this.withinDist2(p[i], p[(i + 1) % pl], dist2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (inner && poly.inner) {
|
||||
for (let i = 0; i < poly.inner.length; i++) {
|
||||
if (this.nearPolygon(poly.inner[i], dist2)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point will not be trimmed later
|
||||
*/
|
||||
insideOffset(poly, offset, mindist2) {
|
||||
return this.inPolygon(poly) === (offset > 0) && !this.nearPolygon(poly, mindist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new point following given slope for given distance
|
||||
* same as projectOnSlope() but scaled
|
||||
*/
|
||||
follow(slope, distance) {
|
||||
let ls = distance / Math.sqrt(slope.dx * slope.dx + slope.dy * slope.dy);
|
||||
return newPoint(this.x + slope.dx * ls, this.y + slope.dy * ls, this.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* for point, return intersecting point on z to next point if points
|
||||
* are on either size of z
|
||||
*/
|
||||
intersectZ(p, z) {
|
||||
let dx = p.x - this.x,
|
||||
dy = p.y - this.y,
|
||||
dz = p.z - this.z,
|
||||
pct = 1 - ((p.z - z) / dz);
|
||||
return newPoint(this.x + dx * pct, this.y + dy * pct, this.z + dz * pct);
|
||||
}
|
||||
|
||||
isEqual2D(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if points are close enough to be considered equivalent
|
||||
*/
|
||||
isMergable2D(p) {
|
||||
return this.isEqual2D(p) || (this.distToSq2D(p) < config.precision_merge_sq);
|
||||
}
|
||||
|
||||
/**
|
||||
* compares 3D point
|
||||
*/
|
||||
isEqual(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y && this.z === p.z);
|
||||
}
|
||||
|
||||
isEqual2D(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if points are close enough to be considered equivalent
|
||||
*/
|
||||
isMergable3D(p) {
|
||||
return this.isEqual(p) || (this.distToSq3D(p) < config.precision_merge_sq);
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if point is inside 2D square size dist*2 around p
|
||||
*/
|
||||
isInBox(p, dist) {
|
||||
return Math.abs(this.x - p.x) < dist && Math.abs(this.y - p.y) < dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* return min distance from point to a polygon
|
||||
* stops searching if any point is closer than threshold
|
||||
*
|
||||
* @param {Polygon} poly
|
||||
* @param {number} [threshold] stop looking if under threshold
|
||||
*/
|
||||
distToPolySegments(poly, threshold) {
|
||||
let point = this,
|
||||
mindist = Infinity;
|
||||
poly.forEachSegment(function(p1, p2) {
|
||||
const nextdist = Math.min(mindist, point.distToLine(p1, p2));
|
||||
mindist = Math.min(nextdist, mindist);
|
||||
// returning true terminates forEachSegment()
|
||||
if (mindist <= threshold) return true;
|
||||
});
|
||||
return mindist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Polygon} poly
|
||||
* @param {number} [threshold] stop looking if under threshold
|
||||
*/
|
||||
distToPolyPoints(poly, threshold) {
|
||||
let point = this,
|
||||
mindist = Infinity;
|
||||
poly.forEachPoint(function(pp) {
|
||||
mindist = Math.min(mindist, point.distTo2D(pp));
|
||||
if (mindist < threshold) return true;
|
||||
});
|
||||
return mindist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Point} nearest point (less than max) from array to this point
|
||||
*/
|
||||
nearestTo(points, max) {
|
||||
if (!max) throw "missing max";
|
||||
let mind = Infinity,
|
||||
minp = null,
|
||||
i, p, d;
|
||||
for (i = 0; i < points.length; i++) {
|
||||
p = points[i];
|
||||
if (p === this || p.del) continue;
|
||||
d = this.distToSq2D(p);
|
||||
if (d < max && d < mind) {
|
||||
mind = d;
|
||||
minp = p;
|
||||
}
|
||||
}
|
||||
return minp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point[]} points
|
||||
* @return {number} average square dist to cloud of points
|
||||
*/
|
||||
averageDistTo(points) {
|
||||
let sum = 0.0,
|
||||
count = 0,
|
||||
i;
|
||||
for (i = 0; i < points.length; i++) {
|
||||
if (points[i] != this) {
|
||||
sum += this.distToSq2D(points[i]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
/**
|
||||
* dist to point in 2D
|
||||
*/
|
||||
distTo2D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* square of distance in 2D
|
||||
*/
|
||||
distToSq2D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
distTo3D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y,
|
||||
dz = this.z - p.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* square of distance in 3D
|
||||
*/
|
||||
distToSq3D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y,
|
||||
dz = this.z - p.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point is inside triangle described by three points
|
||||
*/
|
||||
inTriangle(a, b, c) {
|
||||
let as_x = this.x - a.x,
|
||||
as_y = this.y - a.y,
|
||||
s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 0;
|
||||
if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab) return false;
|
||||
if ((c.x - b.x) * (this.y - b.y) - (c.y - b.y) * (this.x - b.x) > 0 != s_ab) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point is on a line described by two points.
|
||||
* test sum of distances p1->this + this->p2 ~= p1->p2 whens
|
||||
* slopes from p1->this same as this->p2
|
||||
*/
|
||||
onLine(p1, p2) {
|
||||
return this.distToLine(p1, p2) < config.precision_point_on_line;
|
||||
}
|
||||
|
||||
add(delta) {
|
||||
return newPoint(this.x + delta.x, this.y + delta.y, this.z + delta.z);
|
||||
}
|
||||
|
||||
sub(delta) {
|
||||
return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z);
|
||||
}
|
||||
|
||||
move(delta) {
|
||||
this.x += delta.x;
|
||||
this.y += delta.y;
|
||||
this.z += delta.z;
|
||||
return this;
|
||||
}
|
||||
|
||||
// radians rotatition in XY around origin
|
||||
rotate(angle) {
|
||||
const { x, y } = this;
|
||||
this.x = x * Math.cos(angle) - y * Math.sin(angle);
|
||||
this.y = y * Math.cos(angle) + x * Math.sin(angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function dot(u, v) {
|
||||
return u.x * v.x + u.y * v.y;
|
||||
}
|
||||
|
||||
function norm(v) {
|
||||
return Math.sqrt(dot(v, v));
|
||||
}
|
||||
|
||||
function d(u, v) {
|
||||
return Math.sqrt(sqr(u.x - v.x) + sqr(u.y - v.y));
|
||||
}
|
||||
|
||||
function p2l(p, l1, l2) {
|
||||
let v = l2.clone().sub(l1);
|
||||
let w = p.clone().sub(l1);
|
||||
let proj = w.dot(v) / v.dot(v);
|
||||
if (proj < 0) return w.length();
|
||||
if (proj > 1) return p.distTo(l2);
|
||||
return w.sub(v.multiplyScalar(proj)).length();
|
||||
}
|
||||
|
||||
export function newPoint(x, y, z, key) {
|
||||
return new Point(x, y, z, key);
|
||||
}
|
||||
|
||||
export function pointFromClipper(cp, z) {
|
||||
return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z);
|
||||
}
|
||||
128
src2/geo/points.js
Normal file
128
src2/geo/points.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { newPoint } from '../geo/point.js';
|
||||
|
||||
const { config } = base;
|
||||
|
||||
/**
|
||||
* converts a geometry point array into a kiri point array
|
||||
* with optional decimation
|
||||
*/
|
||||
export function verticesToPoints(array, options) {
|
||||
let parr = new Array(array.length / 3),
|
||||
i = 0,
|
||||
j = 0,
|
||||
t = Date.now(),
|
||||
hash = {},
|
||||
unique = 0,
|
||||
passes = 0,
|
||||
points,
|
||||
oldpoints = parr.length,
|
||||
newpoints;
|
||||
|
||||
let {threshold, precision, maxpass} = options || {};
|
||||
// threshold = point count for triggering decimation
|
||||
// precision = under which points are merged
|
||||
// maxpass = max number of decimations
|
||||
threshold = threshold > 0 ? threshold : config.decimate_threshold;
|
||||
precision = precision >= 0 ? precision : config.precision_decimate;
|
||||
maxpass = maxpass >= 0 ? maxpass : 0;
|
||||
|
||||
// replace point objects with their equivalents
|
||||
if (maxpass) {
|
||||
while (i < array.length) {
|
||||
let p = newPoint(array[i++], array[i++], array[i++]),
|
||||
k = p.key,
|
||||
m = hash[k];
|
||||
if (!m) {
|
||||
m = p;
|
||||
hash[k] = p;
|
||||
unique++;
|
||||
}
|
||||
parr[j++] = m;
|
||||
}
|
||||
} else {
|
||||
while (i < array.length) {
|
||||
parr[j++] = newPoint(array[i++], array[i++], array[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
// decimate until all point spacing > precision
|
||||
if (maxpass && precision > 0.0)
|
||||
while (parr.length > threshold) {
|
||||
let lines = [], line, dec = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
lines.push( {p1:p1, p2:p2, d:Math.sqrt(p1.distToSq3D(p2))} );
|
||||
lines.push( {p1:p1, p2:p3, d:Math.sqrt(p1.distToSq3D(p3))} );
|
||||
lines.push( {p1:p2, p2:p3, d:Math.sqrt(p2.distToSq3D(p3))} );
|
||||
}
|
||||
// sort by ascending line length
|
||||
lines.sort(function(a,b) {
|
||||
return a.d - b.d
|
||||
});
|
||||
// create offset mid-points
|
||||
for (i=0; i<lines.length; i++) {
|
||||
line = lines[i];
|
||||
// skip lines longer than precision threshold
|
||||
if (line.d >= precision) break;
|
||||
// skip lines where one of the points is already offset
|
||||
if (line.p1.op || line.p2.op) continue;
|
||||
// todo skip dropping lines where either point is a "sharp" on 3 vectors
|
||||
// todo skip dropping lines where either point connects to a "long" line
|
||||
line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2);
|
||||
dec++;
|
||||
}
|
||||
// exit if nothing to decimate
|
||||
if (dec === 0) break;
|
||||
passes++;
|
||||
// create new facets
|
||||
points = new Array(oldpoints);
|
||||
newpoints = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
// drop facets with two offset points
|
||||
if (p1.op && p1.op === p2.op) continue;
|
||||
if (p1.op && p1.op === p3.op) continue;
|
||||
if (p2.op && p2.op === p3.op) continue;
|
||||
// otherwise emit altered facet
|
||||
points[newpoints++] = p1.op || p1;
|
||||
points[newpoints++] = p2.op || p2;
|
||||
points[newpoints++] = p3.op || p3;
|
||||
}
|
||||
parr = points.slice(0,newpoints);
|
||||
oldpoints = newpoints;
|
||||
if (passes >= maxpass) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if (passes) console.trace({passes, threshold, precision, maxpass});
|
||||
|
||||
if (passes) console.log({
|
||||
before: array.length / 3,
|
||||
after: parr.length,
|
||||
unique: unique,
|
||||
decimations: passes,
|
||||
time: (Date.now() - t)
|
||||
});
|
||||
|
||||
return parr;
|
||||
}
|
||||
|
||||
export function pointsToVertices(points) {
|
||||
let vertices = new Float32Array(points.length * 3),
|
||||
i = 0, vi = 0;
|
||||
while (i < points.length) {
|
||||
vertices[vi++] = points[i].x;
|
||||
vertices[vi++] = points[i].y;
|
||||
vertices[vi++] = points[i++].z;
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
2386
src2/geo/polygon.js
Normal file
2386
src2/geo/polygon.js
Normal file
File diff suppressed because it is too large
Load diff
1109
src2/geo/polygons.js
Normal file
1109
src2/geo/polygons.js
Normal file
File diff suppressed because it is too large
Load diff
884
src2/geo/slicer.js
Normal file
884
src2/geo/slicer.js
Normal file
|
|
@ -0,0 +1,884 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* basic slice and line connection. In future, replace kiri's fdm and cam slicers
|
||||
* with wrappers on this one.
|
||||
*/
|
||||
// dep: geo.base
|
||||
// dep: geo.line
|
||||
// dep: geo.point
|
||||
// dep: geo.polygon
|
||||
// dep: geo.polygons
|
||||
import { newPolygon, newPoint, newOrderedLine } from '../geo/polygon.js';
|
||||
gapp.register("geo.slicer", [], (root, exports) => {
|
||||
|
||||
const { base } = root;
|
||||
const { config, util, polygons } = base
|
||||
|
||||
const POLY = base.polygons;
|
||||
|
||||
function dval(v, dv) {
|
||||
return v !== undefined ? v : dv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of points as triples, a bounding box and a set of
|
||||
* slicing controls, emit an array of Slice objects to the ondone()
|
||||
* function. onupdate() will be called with two parameters (% completion
|
||||
* and an optional message) so that the UI can report progress to the user.
|
||||
*
|
||||
* @param {Point[]} points vertex array
|
||||
* @param {Object} options slicing parameters
|
||||
*/
|
||||
async function slice(points, options = {}) {
|
||||
let zMin = options.zMin || 0,
|
||||
zMax = options.zMax || 0,
|
||||
zInc = options.zInc || 0,
|
||||
zGen = options.zGen, // optional z index generator function
|
||||
zIndexes = options.indices || [],
|
||||
minStep = options.minstep || 0,
|
||||
zFlat = {}, // map area of z index flat areas
|
||||
zList = {}, // fast map of z indexes
|
||||
zLine = [], // map of line counts co-linear to z plane
|
||||
zScale, // bucket span in z units
|
||||
zSum = 0.0, // sanity check that points enclose non-zere volume
|
||||
buckets = [], // banded/grouped faces to speed up slice/search
|
||||
overlapMax = options.overlap || 0.75,
|
||||
bucketMax = options.bucketMax || 100,
|
||||
onupdate = options.onupdate || function() {},
|
||||
sliceFn = dval(options.slicer, sliceZ),
|
||||
{ debug, flat, autoDim } = options,
|
||||
i, j, p1, p2, p3;
|
||||
|
||||
if (!(points && points.length)) {
|
||||
throw "missing points array";
|
||||
}
|
||||
|
||||
// convert threejs position array into points array
|
||||
if (flat) {
|
||||
let array = [];
|
||||
for (i=0, j=points.length; i<j; ) {
|
||||
array.push(newPoint(
|
||||
points[i++].round(3),
|
||||
points[i++].round(3),
|
||||
points[i++].round(3)
|
||||
));
|
||||
}
|
||||
points = array;
|
||||
} else {
|
||||
// round points
|
||||
for (i = 0; i < points.length; i++) {
|
||||
points[i] = points[i].round(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// gather z-index stats
|
||||
for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
// used to calculate buckets (rough sum of z span)
|
||||
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
|
||||
// use co-flat and co-line detection to adjust slice Z
|
||||
if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) {
|
||||
// detect faces co-planar with Z and sum the enclosed area
|
||||
let zkey = p1.z,
|
||||
area = Math.abs(util.area2(p1,p2,p3)) / 2;
|
||||
if (!zFlat[zkey]) {
|
||||
zFlat[zkey] = area;
|
||||
} else {
|
||||
zFlat[zkey] += area;
|
||||
}
|
||||
zLine[zkey] = (zLine[zkey] || 0) + 1;
|
||||
} else if (p1.z === p2.z) {
|
||||
zLine[p1.z] = (zLine[p1.z] || 0) + 1;
|
||||
} else if (p2.z === p3.z) {
|
||||
zLine[p2.z] = (zLine[p2.z] || 0) + 1;
|
||||
} else if (p3.z === p1.z) {
|
||||
zLine[p3.z] = (zLine[p3.z] || 0) + 1;
|
||||
}
|
||||
if (autoDim) {
|
||||
zMin = Math.min(zMin, p1.z, p2.z, p3.z);
|
||||
zMax = Math.max(zMax, p1.z, p2.z, p3.z);
|
||||
}
|
||||
zList[p1.z] = p1.z;
|
||||
zList[p2.z] = p2.z;
|
||||
zList[p3.z] = p3.z;
|
||||
}
|
||||
|
||||
if (zInc) {
|
||||
for (i = zMin; i <= zMax; i += zInc) {
|
||||
zIndexes.push(i);
|
||||
}
|
||||
} else if (!zIndexes.length) {
|
||||
zIndexes = Object.values(zList).sort((a,b) => a - b);
|
||||
if (minStep > 0) {
|
||||
let lastOut;
|
||||
zIndexes = zIndexes.filter(v => {
|
||||
if (lastOut !== undefined && v - lastOut < minStep) {
|
||||
return false;
|
||||
} else {
|
||||
lastOut = v;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// allow zGen to override or update zIndexes
|
||||
// FDM, CAM, Laser slicers will use this to align or interpolate layers
|
||||
if (zGen) {
|
||||
zIndexes = zGen({ zMin, zMax, zLine, zFlat, zIndexes, options });
|
||||
}
|
||||
|
||||
// ensure bucket aligmnent
|
||||
zIndexes = zIndexes.map(v => v.round(3));
|
||||
|
||||
/**
|
||||
* bucket polygons into z-bounded groups (inside or crossing)
|
||||
* to reduce the search space in complex models
|
||||
*/
|
||||
let zSpan = zMax - zMin;
|
||||
let zSpanAvg = zSum / points.length;
|
||||
let bucketCount = options.bucket !== false ?
|
||||
Math.min(bucketMax, Math.max(1, Math.floor(zSpan / zSpanAvg))) : 1;
|
||||
|
||||
zScale = 1 / (zMax / bucketCount);
|
||||
|
||||
if (debug) {
|
||||
console.log({
|
||||
zMin, zMax, zIndexes, zScale, zSum, zSpanAvg,
|
||||
points, bucketCount,
|
||||
options, buckets
|
||||
});
|
||||
}
|
||||
|
||||
/** short-circuit for microscopic and invalid objects */
|
||||
if (zSpan == 0 || zSum == 0 || points.length == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// create empty buckets
|
||||
for (i = 0; i < bucketCount; i++) {
|
||||
buckets.push({ points: [], slices: [] });
|
||||
}
|
||||
|
||||
if (bucketCount > 1) {
|
||||
let failAt = (points.length * overlapMax) | 0, bucket;
|
||||
// copy triples into all matching z-buckets
|
||||
outer: for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let zm = Math.max(0, Math.min(p1.z, p2.z, p3.z)),
|
||||
zM = Math.max(p1.z, p2.z, p3.z),
|
||||
bm = Math.floor(zm * zScale),
|
||||
bM = Math.min(Math.ceil(zM * zScale), bucketCount);
|
||||
// add point to all buckets in range
|
||||
for (j = bm; j < bM; j++) {
|
||||
bucket = buckets[j].points;
|
||||
bucket.push(p1);
|
||||
bucket.push(p2);
|
||||
bucket.push(p3);
|
||||
// fail if single bucket exceeds threshold
|
||||
if (bucket.length > failAt) {
|
||||
if (debug) console.log({ bucketFail: bucket.length });
|
||||
bucketCount = 1;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback if we can't partition point space
|
||||
if (bucketCount === 1) {
|
||||
buckets = [{ points, slices: [] }];
|
||||
}
|
||||
|
||||
// create buckets data structure
|
||||
for (let i = 0, l = zIndexes.length; i < l; i++) {
|
||||
let z = zIndexes[i],
|
||||
index = bucketCount <= 1 ? 0 :
|
||||
Math.min(Math.floor(z * zScale), bucketCount - 1),
|
||||
bucket = buckets[index];
|
||||
if (bucket) {
|
||||
bucket.slices.push(z);
|
||||
} else {
|
||||
console.log({ missing_bucket: z, index });
|
||||
}
|
||||
onupdate((i / zIndexes.length) * 0.1);
|
||||
}
|
||||
|
||||
async function sliceBuckets() {
|
||||
let output = [];
|
||||
let count = 0;
|
||||
let opt = { ...options, zMin, zMax, zIndexes };
|
||||
let ps = [];
|
||||
|
||||
for (let i = 0, l = buckets.length; i < l; i++) {
|
||||
let bucket = buckets[i];
|
||||
let { points, slices } = bucket;
|
||||
if (slices.length)
|
||||
ps.push(sliceFn(slices, points, {
|
||||
...opt,
|
||||
each(rval) {
|
||||
output.push(rval);
|
||||
onupdate(0.1 + (count++ / zIndexes.length) * 0.9);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// join all returned promises
|
||||
await Promise.all(ps);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// create slices from each bucketed region
|
||||
let slices = sliceFn ? await sliceBuckets() : [];
|
||||
slices = slices.sort((a,b) => a.z - b.z);
|
||||
|
||||
return { slices, points, zMin, zMax, zIndexes, zFlat };
|
||||
}
|
||||
|
||||
/**
|
||||
* given a point, append to the correct
|
||||
* 'where' objec tarray (on, over or under)
|
||||
*
|
||||
* @param {Point} p
|
||||
* @param {number} z offset
|
||||
* @param {Obejct} where
|
||||
*/
|
||||
function checkUnderOverOn(p, z, where) {
|
||||
let delta = p.z - z;
|
||||
if (Math.abs(delta) < config.precision_slice_z) { // on
|
||||
where.on.push(p);
|
||||
} else if (delta < 0) { // under
|
||||
where.under.push(p);
|
||||
} else { // over
|
||||
where.over.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a point over and under a z offset, calculate
|
||||
* and return the intersection point on that z plane
|
||||
*
|
||||
* @param {Point} over
|
||||
* @param {Point} under
|
||||
* @param {number} z offset
|
||||
* @returns {Point} intersection point
|
||||
*/
|
||||
function intersectPoints(over, under, z) {
|
||||
let ip = [];
|
||||
for (let i = 0; i < over.length; i++) {
|
||||
for (let j = 0; j < under.length; j++) {
|
||||
ip.push(over[i].intersectZ(under[j], z));
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure points are unique with a cache/key algorithm
|
||||
*/
|
||||
function getCachedPoint(phash, p) {
|
||||
let cached = phash[p.key];
|
||||
if (!cached) {
|
||||
phash[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two points and hints about their edges,
|
||||
* return a new Line object with points sorted
|
||||
* lexicographically by key. This allows for future
|
||||
* line de-duplication and joins.
|
||||
*
|
||||
* @param {Object} phash
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {boolean} [coplanar]
|
||||
* @param {boolean} [edge]
|
||||
* @returns {Line}
|
||||
*/
|
||||
function makeZLine(phash, p1, p2, coplanar, edge) {
|
||||
p1 = getCachedPoint(phash, p1);
|
||||
p2 = getCachedPoint(phash, p2);
|
||||
let line = newOrderedLine(p1,p2);
|
||||
line.coplanar = coplanar || false;
|
||||
line.edge = edge || false;
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* process a single z-slice on a single mesh and
|
||||
* add to slices array
|
||||
*
|
||||
* @param {number} z
|
||||
*/
|
||||
async function sliceZ(z, points, options = {}) {
|
||||
if (Array.isArray(z)) {
|
||||
return Promise.all(z.map(z => sliceZ(z, points, options)));
|
||||
}
|
||||
|
||||
let { zMin, zMax, under, over, both, each } = options,
|
||||
groupFn = dval(options.groupr, both ? null : sliceConnect),
|
||||
phash = {},
|
||||
lines = [],
|
||||
p1, p2, p3;
|
||||
|
||||
// default to 'over' selection with 2 points on a line
|
||||
if (!under && !both) over = true;
|
||||
|
||||
// iterate over matching buckets for this z offset
|
||||
for (let i = 0; i < points.length; ) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let where = {under: [], over: [], on: []};
|
||||
checkUnderOverOn(p1, z, where);
|
||||
checkUnderOverOn(p2, z, where);
|
||||
checkUnderOverOn(p3, z, where);
|
||||
if (where.under.length === 3 || where.over.length === 3) {
|
||||
// does not intersect (all 3 above or below)
|
||||
} else if (where.on.length === 2) {
|
||||
// one side of triangle is on the Z plane and 3rd is below
|
||||
// drop lines with 3rd above because that leads to ambiguities
|
||||
// with complex nested polygons on flat surface
|
||||
let add2 = both ||
|
||||
(over && (where.over.length === 1 || z === zMax)) ||
|
||||
(under && (where.under.length === 1 || z === zMin));
|
||||
if (add2) {
|
||||
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
|
||||
}
|
||||
} else if (where.on.length === 3) {
|
||||
// triangle is coplanar with Z
|
||||
// we drop these because this face is attached to 3 others
|
||||
// that will satisfy the if above (line) with 2 points
|
||||
} else if (where.under.length === 0 || where.over.length === 0) {
|
||||
// does not intersect but one point is on the slice Z plane
|
||||
} else {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length < 2 && where.on.length === 1) {
|
||||
line.push(where.on[0]);
|
||||
}
|
||||
if (line.length === 2) {
|
||||
lines.push(makeZLine(phash, line[0], line[1]));
|
||||
} else {
|
||||
console.log({msg: "invalid ips", line: line, where: where});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length == 0 && options.noEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// de-dup and group lines
|
||||
lines = removeDuplicateLines(lines);
|
||||
|
||||
let rval = { z, lines };
|
||||
if (groupFn) {
|
||||
let groups = groupFn(lines, z, options);
|
||||
if (options.xor) {
|
||||
groups = polygons.xor(groups);
|
||||
}
|
||||
if (options.union) {
|
||||
let points = groups.map(p => p.length);
|
||||
if (points.length > 1) points = points.reduce((a,b) => a + b);
|
||||
// simplistic healing of non-manifold meshes
|
||||
let opt = { x: 1 };
|
||||
let union = polygons.union(polygons.nest(groups), 0.1, true, opt);
|
||||
// fall back to xor'ing polygons that might overlap
|
||||
// when one does not cleanly contain the other and we lose lots of points
|
||||
// trigger when 2 polygons and we lose > 40% of points in the union
|
||||
let delta = opt.changes < 0 ? Math.abs(opt.changes / points) : 0;
|
||||
if (groups.length === 2 && delta >= 0.4) {
|
||||
let xor = groups[0].xor(groups[1]);
|
||||
console.log({ points, pct: (opt.changes / points).round(3), xor: xor.length });
|
||||
if (xor.length) {
|
||||
union = xor;
|
||||
}
|
||||
}
|
||||
// track total poly length changes to determine if healed
|
||||
rval.changes = opt.changes;
|
||||
groups = polygons.flatten(union, null, true);
|
||||
}
|
||||
rval.groups = groups;
|
||||
}
|
||||
|
||||
// look for driver-specific slice post-processor
|
||||
if (options.post) {
|
||||
let fn = base.slicePost[options.post];
|
||||
if (fn) fn(rval, options);
|
||||
}
|
||||
|
||||
// free objects to be re-claimed and reduce memory pressure
|
||||
if (false) {
|
||||
delete rval.groups
|
||||
delete rval.lines
|
||||
|
||||
if (rval.tops)
|
||||
for (let top of rval.tops) {
|
||||
if (top.poly) {
|
||||
top.poly.freeParentRefs();
|
||||
top.simple.freeParentRefs();
|
||||
for (let shell of top.shells) shell.freeParentRefs();
|
||||
for (let fillo of top.fill_off) fillo.freeParentRefs();
|
||||
for (let last of top.last) last.freeParentRefs();
|
||||
} else {
|
||||
top.freeParentRefs();
|
||||
}
|
||||
}
|
||||
|
||||
if (rval.clip) {
|
||||
for (let clip of rval.clip) clip.freeParentRefs();
|
||||
if (rval.clip.m_AllPolys) delete rval.clip.m_AllPolys
|
||||
}
|
||||
}
|
||||
|
||||
if (each) each(rval);
|
||||
return rval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of input lines (line soup), find the path through
|
||||
* joining line ends that encompasses the greatest area without self
|
||||
* interesection. Eliminate used points and repeat. Unjoined lines
|
||||
* are permitted and handled after all other cases are handled.
|
||||
*
|
||||
* @param {Line[]} input
|
||||
* @param {number} [index]
|
||||
* @returns {Array}
|
||||
*/
|
||||
function sliceConnect(input, z, opt = {}) {
|
||||
let { debug, both } = opt;
|
||||
|
||||
if (both) {
|
||||
if (debug) console.log('unable to connect lines sliced with "both" option');
|
||||
return [];
|
||||
}
|
||||
|
||||
// map points to all other points they're connected to
|
||||
let pmap = {},
|
||||
points = [],
|
||||
output = [],
|
||||
connect = [],
|
||||
emitted = 0,
|
||||
forks = false,
|
||||
frays = false,
|
||||
bridge = config.bridgeLineGapDistance,
|
||||
bridgeMax = config.bridgeLineGapDistanceMax,
|
||||
p1, p2, gl;
|
||||
|
||||
function cachedPoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
function addConnected(p1, p2) {
|
||||
if (!p1.group) p1.group = [ p2 ];
|
||||
else p1.group.push(p2);
|
||||
}
|
||||
|
||||
function perimeter(array) {
|
||||
if (!array.perimeter) {
|
||||
array.perimeter = newPolygon().addPoints(array).perimeter();
|
||||
}
|
||||
return array.perimeter;
|
||||
}
|
||||
|
||||
/**
|
||||
* follow points through connected lines to form candidate output paths
|
||||
*/
|
||||
function findNextPath(point, current, branches, depth = 1) {
|
||||
let path = [];
|
||||
if (current) {
|
||||
current.push(path);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// prevent point re-use
|
||||
point.del = true;
|
||||
// add point to path
|
||||
path.push(point);
|
||||
|
||||
let links = point.group.filter(p => !p.del);
|
||||
|
||||
// no need to recurse at the start
|
||||
if (links.length === 2 && depth === 1) {
|
||||
point = links[0];
|
||||
// if (debug) console.log({start_mid: point, depth});
|
||||
continue;
|
||||
}
|
||||
|
||||
// if fork in the road, follow all paths to their end
|
||||
// and find the longest path
|
||||
let root = !current, nc;
|
||||
if (links.length > 1) {
|
||||
// if (debug) console.log('fork!', {links: links.length, depth, root});
|
||||
if (root) {
|
||||
current = [ path ];
|
||||
branches = [ ];
|
||||
}
|
||||
if (branches.length < 500)
|
||||
for (let p of links) {
|
||||
branches.push(nc = current.slice());
|
||||
let rpath = findNextPath(p, nc, branches, depth + 1);
|
||||
// allow point re-use in other path searches
|
||||
for (let p of rpath) p.del = false;
|
||||
}
|
||||
// flatten and sort in ascending perimeter
|
||||
let flat = branches.map(b => b.flat()).sort((a,b) => {
|
||||
return perimeter(b) - perimeter(a);
|
||||
});
|
||||
let npath = flat[0];
|
||||
if (debug) console.log({
|
||||
root,
|
||||
branches: branches.slice(),
|
||||
flat, path, npath
|
||||
});
|
||||
if (root) {
|
||||
for (let p of npath) p.del = true;
|
||||
return npath;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
// return root ? npath : path;
|
||||
} else {
|
||||
// choose next (unused) point
|
||||
point = links[0];
|
||||
}
|
||||
|
||||
// hit an open end or branch
|
||||
if (!point || point.del) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
throw "invalid state";
|
||||
}
|
||||
|
||||
// emit a polygon if it can be cleaned and still have 2 or more points
|
||||
function emit(poly) {
|
||||
emitted += poly.length;
|
||||
if (!opt.dirty) poly = poly.clean();
|
||||
if (poly.length > 2 || true) output.push(poly);
|
||||
if (debug) console.log('xray',poly);
|
||||
}
|
||||
|
||||
// given an array of paths, emit longest to shortest
|
||||
// eliminating points from the paths as they are emitted
|
||||
// shorter paths any point eliminated are eliminated as candidates.
|
||||
function emitPath(path) {
|
||||
let closed = path[0].group.indexOf(path.peek()) >= 0;
|
||||
if (closed && path.length > 2) {
|
||||
if (debug) console.log({ closed: path.length, path });
|
||||
emit(newPolygon().addPoints(path));
|
||||
} else if (path.length > 1) {
|
||||
let gap = path[0].distTo2D(path.peek()).round(4);
|
||||
if (debug) console.log({ open: path.length, gap, path });
|
||||
connect.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
// create point map, unique point list and point group arrays
|
||||
input.forEach(function(line) {
|
||||
p1 = cachedPoint(line.p1);
|
||||
p2 = cachedPoint(line.p2);
|
||||
addConnected(p1,p2);
|
||||
addConnected(p2,p1);
|
||||
});
|
||||
|
||||
// console.log({points, forks: points.filter(p => p.group.length !== 2)});
|
||||
// for each unused point, find the longest non-intersecting path
|
||||
|
||||
for (let point of points) {
|
||||
gl = point.group.length;
|
||||
forks = forks || gl > 2;
|
||||
frays = frays || gl < 2;
|
||||
}
|
||||
if (debug && (forks || frays)) console.log({forks, frays});
|
||||
|
||||
// process paths starting with forks
|
||||
if (forks) {
|
||||
if (debug) console.log('process forks');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del && point.group.length > 2) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
} }
|
||||
|
||||
// process paths with dangling endpoints
|
||||
if (frays) {
|
||||
if (debug) console.log('process frays');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del && point.group.length === 1) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
} }
|
||||
|
||||
// process normal paths
|
||||
if (debug) console.log('process mids');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) console.log({
|
||||
points,
|
||||
emitted,
|
||||
used: points.filter(p => p.del),
|
||||
free: points.filter(p => !p.del),
|
||||
});
|
||||
|
||||
if (debug && connect.length) console.log({connect});
|
||||
if (debug) connect = connect.map(a => a.slice());
|
||||
|
||||
// progressively connect open polygons within a bridge distance
|
||||
let iter = 1000;
|
||||
let mingap;
|
||||
if (true) do {
|
||||
mingap = Infinity;
|
||||
|
||||
outer: for (let i=0; i<connect.length; i++) {
|
||||
if (!bridge) {
|
||||
emit(newPolygon().addPoints(root).setOpen());
|
||||
continue;
|
||||
}
|
||||
|
||||
// rollup root with arrays after until no more ends match
|
||||
inner: while (true) {
|
||||
let root = connect[i];
|
||||
|
||||
if (root.delete) break;
|
||||
|
||||
let rfirst = root[0],
|
||||
rlast = root.peek(),
|
||||
dist = rfirst.distToSq2D(rlast),
|
||||
closest = { dist };
|
||||
|
||||
for (let j=i+1; j<connect.length; j++) {
|
||||
let next = connect[j];
|
||||
|
||||
if (next.delete) continue;
|
||||
|
||||
let nfirst = next[0];
|
||||
let nlast = next.peek();
|
||||
|
||||
// test last to next first
|
||||
dist = rlast.distToSq2D(nfirst);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next }
|
||||
}
|
||||
|
||||
// test last to next last
|
||||
dist = rlast.distToSq2D(nlast);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, reverse: next };
|
||||
}
|
||||
|
||||
// test first to next first
|
||||
dist = rfirst.distToSq2D(nfirst);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, reverse: root };
|
||||
}
|
||||
|
||||
// test last to next last
|
||||
dist = rfirst.distToSq2D(nlast);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, swap: j };
|
||||
}
|
||||
}
|
||||
|
||||
let { next, reverse, swap } = closest;
|
||||
|
||||
if (next && closest.dist < bridge) {
|
||||
if (debug) console.log({
|
||||
rollup: root.slice(),
|
||||
next: next.slice(),
|
||||
reverse,
|
||||
swap
|
||||
});
|
||||
if (reverse) {
|
||||
reverse.reverse();
|
||||
}
|
||||
if (swap) {
|
||||
next.appendAll(root);
|
||||
connect[i] = next;
|
||||
connect[swap] = root;
|
||||
root.delete = true;
|
||||
next.merged = true;
|
||||
} else {
|
||||
root.appendAll(next);
|
||||
next.delete = true;
|
||||
root.merged = true;
|
||||
}
|
||||
} else {
|
||||
break inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bridge = mingap < Infinity ? Math.max(mingap + 0.01, bridge + 0.1) : bridge + 0.1;
|
||||
if (debug) console.log({iter, bridge, mingap, bridgeMax});
|
||||
|
||||
} while (iter-- > 0 && bridge && bridge < bridgeMax && mingap < bridgeMax);
|
||||
|
||||
if (debug) console.log({ remain: connect.filter(c => !c.delete) });
|
||||
|
||||
for (let array of connect) {
|
||||
if (array.delete) continue;
|
||||
|
||||
if (debug) {
|
||||
let first = array[0];
|
||||
let last = array.peek();
|
||||
let dist = first.distToSq2D(last);
|
||||
console.log({
|
||||
dist: dist.round(4),
|
||||
merged: array.merged || false,
|
||||
array
|
||||
});
|
||||
}
|
||||
|
||||
emit(newPolygon().addPoints(array));
|
||||
}
|
||||
|
||||
if (debug) console.log({ emitted, output });
|
||||
if (debug && emitted < points.length) console.log({ leftovers:points.length - emitted });
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* eliminate duplicate lines and interior-only lines (coplanar)
|
||||
*
|
||||
* lines are sorted using lexicographic point keys such that
|
||||
* they are comparable even if their points are reversed. hinting
|
||||
* for deletion, co-planar and suspect shared edge is detectable at
|
||||
* this time.
|
||||
*
|
||||
* @param {Line[]} lines
|
||||
* @returns {Line[]}
|
||||
*/
|
||||
function removeDuplicateLines(lines) {
|
||||
let output = [],
|
||||
tmplines = [],
|
||||
points = [],
|
||||
pmap = {};
|
||||
|
||||
function cachePoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
function addLinesToPoint(point, line) {
|
||||
cachePoint(point);
|
||||
if (!point.group) point.group = [ line ];
|
||||
else point.group.push(line);
|
||||
}
|
||||
|
||||
// mark duplicates for deletion preserving edges
|
||||
lines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = !l1.edge;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// associate points with their lines, cull deleted
|
||||
for (let line of lines) {
|
||||
if (!line.del) {
|
||||
tmplines.push(line);
|
||||
addLinesToPoint(line.p1, line);
|
||||
addLinesToPoint(line.p2, line);
|
||||
}
|
||||
}
|
||||
|
||||
// merge collinear lines
|
||||
for (let point of points) {
|
||||
// only merge when point connects to exactly one other point
|
||||
if (point.group.length != 2) {
|
||||
continue;
|
||||
}
|
||||
let l1 = point.group[0],
|
||||
l2 = point.group[1];
|
||||
if (l1.isCollinear(l2)) {
|
||||
l1.del = true;
|
||||
l2.del = true;
|
||||
// find new endpoints that are not shared point
|
||||
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
|
||||
p2 = l2.p1 != point ? l2.p1 : l2.p2,
|
||||
newline = newOrderedLine(p1,p2);
|
||||
// remove deleted lines from associated points
|
||||
p1.group.remove(l1);
|
||||
p1.group.remove(l2);
|
||||
p2.group.remove(l1);
|
||||
p2.group.remove(l2);
|
||||
// associate new line with points
|
||||
p1.group.push(newline);
|
||||
p2.group.push(newline);
|
||||
// add new line to lines array
|
||||
newline.edge = l1.edge || l2.edge;
|
||||
tmplines.push(newline);
|
||||
}
|
||||
}
|
||||
|
||||
// mark duplicates for deletion
|
||||
// but preserve one if it's an edge
|
||||
tmplines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = true;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// create new line array culling deleted
|
||||
for (let line of tmplines) {
|
||||
if (!line.del) {
|
||||
output.push(line);
|
||||
line.p1.group = null;
|
||||
line.p2.group = null;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
gapp.overlay(base, {
|
||||
slice,
|
||||
sliceZ,
|
||||
slicePost: {},
|
||||
sliceDedup: removeDuplicateLines,
|
||||
sliceConnect
|
||||
});
|
||||
|
||||
});
|
||||
100
src2/geo/slope.js
Normal file
100
src2/geo/slope.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { config } from './base.js';
|
||||
|
||||
const ABS = Math.abs,
|
||||
DEG2RAD = Math.PI / 180,
|
||||
RAD2DEG = 180 / Math.PI;
|
||||
|
||||
export class Slope {
|
||||
constructor(p1, p2, dx, dy) {
|
||||
this.dx = p1 && p2 ? p2.x - p1.x : dx;
|
||||
this.dy = p1 && p2 ? p2.y - p1.y : dy;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return [this.dx, this.dy, this.angle].join(',');
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Slope(null, null, this.dx, this.dy);
|
||||
}
|
||||
|
||||
isSame(s) {
|
||||
// if very close to vertical or horizontal, they're the same
|
||||
if (ABS(this.dx) <= config.precision_merge && ABS(s.dx) <= config.precision_merge) return true;
|
||||
if (ABS(this.dy) <= config.precision_merge && ABS(s.dy) <= config.precision_merge) return true;
|
||||
// check angle within a range
|
||||
let prec = Math.min(1/Math.sqrt(this.dx * this.dx + this.dy * this.dy), config.precision_slope_merge);
|
||||
return angleWithinDelta(this.angle, s.angle, prec || config.precision_slope);
|
||||
}
|
||||
|
||||
normal() {
|
||||
let t = this.dx;
|
||||
this.dx = -this.dy;
|
||||
this.dy = t;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
return this;
|
||||
}
|
||||
|
||||
invert() {
|
||||
this.dx = -this.dx;
|
||||
this.dy = -this.dy;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
return this;
|
||||
}
|
||||
|
||||
toUnit() {
|
||||
let max = Math.max(ABS(this.dx), ABS(this.dy));
|
||||
this.dx = this.dx / max;
|
||||
this.dy = this.dy / max;
|
||||
return this;
|
||||
}
|
||||
|
||||
factor(f) {
|
||||
this.dx *= f;
|
||||
this.dy *= f;
|
||||
return this;
|
||||
}
|
||||
|
||||
invert() {
|
||||
this.dx = -this.dx;
|
||||
this.dy = -this.dy;
|
||||
this.angle = 180 - this.angle;
|
||||
return this;
|
||||
}
|
||||
|
||||
angleDiff(s2,sign) {
|
||||
const n1 = this.angle;
|
||||
const n2 = s2.angle;
|
||||
let diff = n2 - n1;
|
||||
while (diff < -180) diff += 360;
|
||||
while (diff > 180) diff -= 360;
|
||||
return sign ? diff : Math.abs(diff);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the difference between a & b is less than v
|
||||
*/
|
||||
function minDeltaABS(a,b,v) {
|
||||
return ABS(a-b) < v;
|
||||
}
|
||||
|
||||
function angleWithinDelta(a1, a2, delta) {
|
||||
return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta);
|
||||
}
|
||||
|
||||
export function newSlope(p1, p2, dx, dy) {
|
||||
return new Slope(p1, p2, dx, dy);
|
||||
}
|
||||
|
||||
export function newSlopeFromAngle(angle) {
|
||||
return newSlope(0,0,
|
||||
Math.cos(angle * DEG2RAD),
|
||||
Math.sin(angle * DEG2RAD)
|
||||
);
|
||||
}
|
||||
202
src2/geo/wasm.js
Normal file
202
src2/geo/wasm.js
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base } from '../geo/base.js';
|
||||
|
||||
const { config } = base;
|
||||
|
||||
const factor = config.clipper;
|
||||
|
||||
export const wasm_ctrl = {
|
||||
enable,
|
||||
disable,
|
||||
count: {
|
||||
offset: 0,
|
||||
union: 0,
|
||||
diff: 0
|
||||
}
|
||||
};
|
||||
|
||||
function log() {
|
||||
console.log(...arguments);
|
||||
}
|
||||
|
||||
function writePolys(view, polys) {
|
||||
let pcount = 0;
|
||||
for (let poly of polys) {
|
||||
pcount += writePoly(view, poly);
|
||||
}
|
||||
return pcount;
|
||||
}
|
||||
|
||||
function writePoly(view, poly, inner) {
|
||||
if (inner) {
|
||||
poly.setCounterClockwise();
|
||||
} else {
|
||||
poly.setClockwise();
|
||||
}
|
||||
let count = 1;
|
||||
let points = poly.points;
|
||||
let inners = poly.inner;
|
||||
view.writeU16(points.length, true);
|
||||
for (let i=0, il=points.length; i<il; i++) {
|
||||
let point = points[i];
|
||||
view.writeI32((point.x * factor)|0, true);
|
||||
view.writeI32((point.y * factor)|0, true);
|
||||
}
|
||||
if (inners) {
|
||||
for (let i=0, il=inners.length; i<il; i++) {
|
||||
count += writePoly(view, inners[i], true);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readPoly(view, z) {
|
||||
let points = view.readU16(true);
|
||||
if (points === 0) return;
|
||||
let poly = base.newPolygon();
|
||||
while (points-- > 0) {
|
||||
poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
function readPolys(view, z, out = []) {
|
||||
for (;;) {
|
||||
let poly = readPoly(view, z);
|
||||
if (poly) {
|
||||
out.push(poly);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function polyOffset(polys, offset, z, clean, simple) {
|
||||
wasm_ctrl.count.offset++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
pcount = writePolys(new DataWriter(wasm.heap, buffer), polys),
|
||||
resat = wasm.fn.offset(buffer, pcount, offset * factor, clean, simple),
|
||||
out = readPolys(new DataReader(wasm.heap, resat), z);
|
||||
return polyNest(out);
|
||||
}
|
||||
|
||||
export function polyUnion(polys, z) {
|
||||
wasm_ctrl.count.union++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
pcount = writePolys(new DataWriter(wasm.heap, buffer), polys),
|
||||
resat = wasm.fn.union(buffer, pcount),
|
||||
out = readPolys(new DataReader(wasm.heap, resat), z);
|
||||
return polyNest(out);
|
||||
}
|
||||
|
||||
export function polyDiff(polysA, polysB, z, AB, BA) {
|
||||
wasm_ctrl.count.diff++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
writer = new DataWriter(wasm.heap, buffer),
|
||||
pcountA = writePolys(writer, polysA),
|
||||
pcountB = writePolys(writer, polysB),
|
||||
resat = wasm.fn.diff(buffer, pcountA, pcountB, AB?1:0, BA?1:0, config.clipperClean),
|
||||
reader = new DataReader(wasm.heap, resat);
|
||||
if (AB) {
|
||||
AB.appendAll(polyNest(readPolys(reader, z)));
|
||||
}
|
||||
if (BA) {
|
||||
BA.appendAll(polyNest(readPolys(reader, z)));
|
||||
}
|
||||
}
|
||||
|
||||
// nest closed polygons without existing parent / child relationships
|
||||
function polyNest(polys) {
|
||||
polys.sort((a,b) => {
|
||||
return b.bounds.minx - a.bounds.minx;
|
||||
});
|
||||
// from smallest to largest, check for enclosing bounds and nest
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let smaller = polys[i];
|
||||
// prevent parent poly from being consumed
|
||||
if (smaller.inner) continue;
|
||||
for (let j=i+1; j<il; j++) {
|
||||
let larger = polys[j];
|
||||
if (larger.bounds.contains(smaller.bounds)) {
|
||||
larger.addInner(smaller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let tops = [];
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let poly = polys[i];
|
||||
if (!poly.parent) {
|
||||
tops.push(poly);
|
||||
}
|
||||
}
|
||||
return tops;
|
||||
}
|
||||
|
||||
function readString(pos, len) {
|
||||
let view = new DataReader(base.wasm.heap, pos);
|
||||
let out = [];
|
||||
while (len-- > 0) {
|
||||
out.push(String.fromCharCode(view.readU8()));
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
export function enable() {
|
||||
if (base.wasm || base._wasm) {
|
||||
return;
|
||||
}
|
||||
base._wasm = 'loading';
|
||||
fetch('/wasm/kiri-geo.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
debug_string: (len, ptr) => { console.log('wasm', readString(ptr, len)) }
|
||||
},
|
||||
wasi_snapshot_preview1: {
|
||||
// args_get: (count,bufsize) => { return 0 },
|
||||
// args_sizes_get: (count,bufsize) => { },
|
||||
// environ_get: (count,bufsize) => { return 0 },
|
||||
// environ_sizes_get: (count,bufsize) => { },
|
||||
proc_exit: (code) => { return code }
|
||||
}
|
||||
}))
|
||||
.then(results => {
|
||||
// console.log({enabled: base.wasm});
|
||||
delete base._wasm;
|
||||
let { module, instance } = results;
|
||||
let { exports } = instance;
|
||||
let heap = new DataView(exports.memory.buffer);
|
||||
let wasm = base.wasm = {
|
||||
heap,
|
||||
exports,
|
||||
memory: exports.memory,
|
||||
memmax: exports.memory.buffer.byteLength,
|
||||
malloc: exports.mem_get,
|
||||
free: exports.mem_clr
|
||||
};
|
||||
wasm.shared = wasm.malloc(1024 * 1024 * 30),
|
||||
wasm.fn = {
|
||||
diff: exports.poly_diff,
|
||||
union: exports.poly_union,
|
||||
offset: exports.poly_offset
|
||||
};
|
||||
wasm.js = {
|
||||
diff: polyDiff,
|
||||
union: polyUnion,
|
||||
offset: polyOffset
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function disable() {
|
||||
if (base.wasm) {
|
||||
base.wasm.free(base.wasm.shared);
|
||||
delete base.wasm;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue