mesh v2 at /v2/mesh

This commit is contained in:
Stewart Allen 2025-06-14 01:05:08 -04:00
commit 6566c45f9a
85 changed files with 138198 additions and 1 deletions

852
src2/geo/base.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

1109
src2/geo/polygons.js Normal file

File diff suppressed because it is too large Load diff

884
src2/geo/slicer.js Normal file
View 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
View 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
View 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;
}
}