extract poly utils into their own static class
This commit is contained in:
parent
55f261d3be
commit
deb3e0faa9
5 changed files with 1280 additions and 942 deletions
325
src/geo/poly/arcs.js
Normal file
325
src/geo/poly/arcs.js
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { base, util } from '../base.js';
|
||||
import { newPoint } from '../point.js';
|
||||
|
||||
/**
|
||||
* Arc detection utility functions for polygons.
|
||||
* These utilities detect and annotate circular arc sequences in polygon points.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate the center point of a circle defined by three points.
|
||||
* Using perimeter length, finds 3 equally spaced points to compute circle center.
|
||||
*
|
||||
* @param {Polygon} poly - polygon assumed to be roughly circular
|
||||
* @returns {Point} center point of the circle
|
||||
*/
|
||||
export function calcCircleCenter(poly) {
|
||||
let pe = poly.perimeter(),
|
||||
pe1 = pe / 3, // point 2
|
||||
pe2 = pe1 * 2, // point 3
|
||||
p = poly.points,
|
||||
l = p.length,
|
||||
i = 1,
|
||||
td = 0,
|
||||
lp = p[0], // first point
|
||||
ap = [ lp ]; // array of 3 points
|
||||
while (i < l) {
|
||||
let np = p[i++];
|
||||
let d = lp.distTo2D(np);
|
||||
td += d;
|
||||
if (ap.length === 1 && td >= pe1) {
|
||||
ap.push(np);
|
||||
} else if (ap.length === 2 && (td >= pe2 || i >= l)) {
|
||||
ap.push(np);
|
||||
break;
|
||||
}
|
||||
lp = np;
|
||||
}
|
||||
let center = util.center2d(...ap);
|
||||
return newPoint(center.x, center.y, p[0].z, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over polygon points and find sequences of 5 or more points
|
||||
* with a common center point and collect as midpoint/radius.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to analyze
|
||||
* @param {Object} opt - options
|
||||
* @param {number} opt.tolerance - center matching tolerance (default 1e-2)
|
||||
* @param {boolean} opt.inside - require centers be inside polygon (default true)
|
||||
* @returns {Array} array of arc center records
|
||||
*/
|
||||
export function findArcCenters(poly, opt = {}) {
|
||||
if (poly.length < 6) return [];
|
||||
let tolerance = opt.tolerance || 1e-2,
|
||||
inside = opt.inside ?? true,
|
||||
seq = poly.points.slice(),
|
||||
util = base.util;
|
||||
if (poly.isClosed()) seq.appendAll(seq.slice(0,5));
|
||||
let recs = [], // accumulated arc points
|
||||
cand = []; // candidate center array
|
||||
for (let pos=3; pos<seq.length; pos++) {
|
||||
let next = util.circleCenter(...seq.slice(pos-3, pos));
|
||||
let prev = cand.peek();
|
||||
if (inside && !newPoint(next.x,next.y).inPolygon(poly)) {
|
||||
// require point be inside current polygon
|
||||
next = null;
|
||||
}
|
||||
if (next && !prev) {
|
||||
// seed candidate list
|
||||
cand.push(next);
|
||||
continue;
|
||||
}
|
||||
if (!next) {
|
||||
// next is a bust, reset candidate list
|
||||
cand.length = 0;
|
||||
continue;
|
||||
}
|
||||
if (util.dist2D(next,prev) < tolerance) {
|
||||
// add new candidate
|
||||
cand.push(next);
|
||||
continue;
|
||||
} else if (cand.length >= 3) {
|
||||
// emit record on 5 points
|
||||
recs.push(cand.peek());
|
||||
}
|
||||
// reset candidate array
|
||||
cand = [ next ];
|
||||
}
|
||||
if (cand.length >= 3) {
|
||||
recs.push(cand.peek());
|
||||
}
|
||||
// filter dups
|
||||
recs.sort((a,b) => {
|
||||
return util.dist2D(a,b);
|
||||
});
|
||||
recs = recs.filter((r,i) => {
|
||||
if (i > 0) {
|
||||
return util.dist2D(r, recs[i-1]) < tolerance*10 ? null : r;
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
});
|
||||
return recs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect and annotate arc sequences in polygon points.
|
||||
* When an arc is detected, the first point is annotated with arc metadata
|
||||
* and intermediate points remain in the array but should be skipped during emission.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to analyze (will be mutated with arc annotations)
|
||||
* @param {Object} opts - detection options
|
||||
* @param {number} opts.tolerance - arc detection tolerance (default 0.1)
|
||||
* @param {number} opts.arcRes - arc resolution in radians (default 0.1)
|
||||
* @param {number} opts.minPoints - minimum points to consider an arc (default 4)
|
||||
* @returns {Polygon} this polygon with arc annotations
|
||||
*/
|
||||
export function detectArcs(poly, opts = {}) {
|
||||
const {
|
||||
tolerance = 0.1,
|
||||
arcRes = 0.1,
|
||||
minPoints = 4
|
||||
} = opts;
|
||||
|
||||
const points = poly.points;
|
||||
const length = points.length;
|
||||
|
||||
if (length < minPoints) {
|
||||
return poly;
|
||||
}
|
||||
|
||||
// Clear any existing arc annotations
|
||||
for (let p of points) {
|
||||
delete p.arc;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
// Process all points except we can't start an arc at the last point
|
||||
// because we need at least minPoints to form an arc
|
||||
while (i < length - minPoints + 1) {
|
||||
// Try to detect arc starting at position i
|
||||
const arcData = detectArcAt(poly, i, points, length, tolerance, arcRes, minPoints);
|
||||
|
||||
if (arcData) {
|
||||
// Annotate the starting point
|
||||
points[i].arc = arcData;
|
||||
// Skip past the arc points
|
||||
i += arcData.skip + 1; // +1 to move to point after arc end
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to detect an arc starting at the given index.
|
||||
* Uses a greedy approach: grow the arc as long as possible, then validate.
|
||||
*
|
||||
* @private
|
||||
* @param {Polygon} poly - polygon being analyzed
|
||||
* @param {number} startIdx - starting index
|
||||
* @param {Point[]} points - polygon points
|
||||
* @param {number} length - points length
|
||||
* @param {number} tolerance - detection tolerance
|
||||
* @param {number} arcRes - arc resolution
|
||||
* @param {number} minPoints - minimum points for arc
|
||||
* @returns {Object|null} arc data or null if no arc detected
|
||||
*/
|
||||
export function detectArcAt(poly, startIdx, points, length, tolerance, arcRes, minPoints) {
|
||||
// Greedy approach: collect as many points as possible that might form an arc
|
||||
let candidates = [];
|
||||
|
||||
for (let idx = startIdx; idx < length; idx++) {
|
||||
const p1 = points[idx];
|
||||
|
||||
// Last point - add and done
|
||||
if (idx >= length - 1) {
|
||||
candidates.push(p1);
|
||||
break;
|
||||
}
|
||||
|
||||
const p2 = points[idx + 1];
|
||||
|
||||
// Skip duplicate points
|
||||
if (p1.distTo2D(p2) < 0.001) {
|
||||
break;
|
||||
}
|
||||
|
||||
candidates.push(p1);
|
||||
|
||||
// Stop if we've collected enough to test
|
||||
if (candidates.length >= minPoints) {
|
||||
// Try to validate the arc with current candidates
|
||||
const arcData = validateArc(poly, candidates, tolerance, arcRes, minPoints);
|
||||
|
||||
if (!arcData) {
|
||||
// Current set doesn't form valid arc, back up one point
|
||||
candidates.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final validation with all collected candidates
|
||||
return validateArc(poly, candidates, tolerance, arcRes, minPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a set of points forms a valid arc.
|
||||
*
|
||||
* @private
|
||||
* @param {Polygon} poly - polygon being analyzed
|
||||
* @param {Point[]} arcPoints - candidate arc points
|
||||
* @param {number} tolerance - detection tolerance
|
||||
* @param {number} arcRes - arc resolution
|
||||
* @param {number} minPoints - minimum points for arc
|
||||
* @returns {Object|null} arc data or null if invalid
|
||||
*/
|
||||
export function validateArc(poly, arcPoints, tolerance, arcRes, minPoints) {
|
||||
if (arcPoints.length < minPoints) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate center using well-distributed points
|
||||
const center = findBestCenter(arcPoints, tolerance);
|
||||
if (!center) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if all points lie on the circle within tolerance
|
||||
let maxRadiusError = 0;
|
||||
let sumRadiusError = 0;
|
||||
|
||||
for (let i = 0; i < arcPoints.length; i++) {
|
||||
const p = arcPoints[i];
|
||||
const radius = Math.hypot(p.x - center.x, p.y - center.y);
|
||||
const radiusError = Math.abs(radius - center.r);
|
||||
|
||||
maxRadiusError = Math.max(maxRadiusError, radiusError);
|
||||
sumRadiusError += radiusError;
|
||||
|
||||
// Hard limit on any single point
|
||||
if (radiusError > tolerance * 2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check average error is reasonable
|
||||
const avgRadiusError = sumRadiusError / arcPoints.length;
|
||||
if (avgRadiusError > tolerance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check angular resolution (don't want points too far apart)
|
||||
for (let i = 0; i < arcPoints.length - 1; i++) {
|
||||
const curr = arcPoints[i];
|
||||
const next = arcPoints[i + 1];
|
||||
const dist = curr.distTo2D(next);
|
||||
const radius = Math.hypot(curr.x - center.x, curr.y - center.y);
|
||||
|
||||
if (radius > 0) {
|
||||
const angle = 2 * Math.asin(Math.min(1, dist / (2 * radius)));
|
||||
if (Math.abs(angle) > arcRes) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine arc direction
|
||||
const p0 = arcPoints[0];
|
||||
const p1 = arcPoints[Math.min(1, arcPoints.length - 1)];
|
||||
const vec1 = { x: p1.x - p0.x, y: p1.y - p0.y };
|
||||
const vec2 = { x: center.x - p0.x, y: center.y - p0.y };
|
||||
const cross = vec1.x * vec2.y - vec1.y * vec2.x;
|
||||
const clockwise = cross < 0;
|
||||
|
||||
return {
|
||||
center: newPoint(center.x, center.y, p0.z),
|
||||
clockwise,
|
||||
skip: arcPoints.length - 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best-fit center for a set of arc points.
|
||||
*
|
||||
* @private
|
||||
* @param {Point[]} arcPoints - candidate arc points
|
||||
* @param {number} tolerance - detection tolerance
|
||||
* @returns {Object|null} center with x, y, r properties or null
|
||||
*/
|
||||
export function findBestCenter(arcPoints, tolerance) {
|
||||
const len = arcPoints.length;
|
||||
|
||||
// Use 3 well-spaced points for initial center calculation
|
||||
let idx1 = 0;
|
||||
let idx2 = Math.floor(len / 2);
|
||||
let idx3 = len - 1;
|
||||
|
||||
// If first and last are very close (near-circle), use different points
|
||||
if (arcPoints[idx1].distTo2D(arcPoints[idx3]) < tolerance * 2) {
|
||||
idx1 = Math.floor(len * 0.25);
|
||||
idx2 = Math.floor(len * 0.5);
|
||||
idx3 = Math.floor(len * 0.75);
|
||||
}
|
||||
|
||||
const center = util.center2d(
|
||||
arcPoints[idx1],
|
||||
arcPoints[idx2],
|
||||
arcPoints[idx3],
|
||||
1
|
||||
);
|
||||
|
||||
if (!center || center.hasNaN?.() || !isFinite(center.r)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return center;
|
||||
}
|
||||
280
src/geo/poly/geo.js
Normal file
280
src/geo/poly/geo.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { base, config, util } from '../base.js';
|
||||
import { calc_normal, calc_vertex } from '../paths.js';
|
||||
import { newPolygon, slopeDiff } from '../polygon.js';
|
||||
|
||||
/**
|
||||
* Geometric utility functions for polygons.
|
||||
* These are extracted from the Polygon class to improve modularity.
|
||||
* All functions take a polygon as the first parameter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remove points that are closer than `dist` to the previous point.
|
||||
* Returns a new polygon with filtered points, or null if result would have < 2 points.
|
||||
*
|
||||
* @param {Polygon} poly - source polygon
|
||||
* @param {number} dist - minimum distance between points (defaults to precision_merge)
|
||||
* @returns {Polygon|null} new filtered polygon or null
|
||||
*/
|
||||
export function debur(poly, dist) {
|
||||
if (poly.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const pa = poly.points,
|
||||
pln = pa.length,
|
||||
open = poly.open,
|
||||
newp = newPolygon().copyZ(poly.getZ()),
|
||||
min = dist || base.config.precision_merge;
|
||||
let lo;
|
||||
newp.push(lo = pa[0]);
|
||||
for (let i = 1; i < pln; i++) {
|
||||
if (lo.distTo2D(pa[i]) >= min) {
|
||||
newp.push(lo = pa[i]);
|
||||
}
|
||||
}
|
||||
newp.open = open;
|
||||
newp.parent = poly.parent;
|
||||
if (newp.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return newp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate miter joins for polygon points where angle exceeds 90 degrees.
|
||||
* Returns either the original polygon or a new one with miter adjustments.
|
||||
*
|
||||
* @param {Polygon} poly - source polygon
|
||||
* @param {boolean} debug - debug flag
|
||||
* @returns {Polygon} polygon with miters applied
|
||||
*/
|
||||
export function miter(poly, debug) {
|
||||
if (poly.length < 3) return poly;
|
||||
|
||||
const slo = [],
|
||||
pa = poly.points,
|
||||
pln = pa.length,
|
||||
open = poly.open;
|
||||
let last;
|
||||
for (let i = 1; i < pln; i++) {
|
||||
slo.push(pa[i - 1].slopeTo(last = pa[i]));
|
||||
}
|
||||
if (!open) {
|
||||
slo.push(last.slopeTo(pa[0]));
|
||||
}
|
||||
|
||||
const ang = new Array(pln).fill(0);
|
||||
let redo = false;
|
||||
const aln = open ? pln - 1 : pln;
|
||||
for (let i = 1; i < aln; i++) {
|
||||
ang[i] = slopeDiff(slo[i - 1], slo[i]);
|
||||
redo |= ang[i] > 90;
|
||||
}
|
||||
if (!open) {
|
||||
ang[0] = slopeDiff(slo[pln - 1], slo[0]);
|
||||
redo |= ang[pln - 1] > 90;
|
||||
redo |= ang[0] > 90;
|
||||
}
|
||||
if (redo) {
|
||||
const newp = newPolygon().copyZ(poly.z);
|
||||
newp.open = open;
|
||||
for (let i = 0; i < pln; i++) {
|
||||
const p = pa[(i + pln) % pln];
|
||||
const d = ang[(i + pln) % pln];
|
||||
if (d > 179) {
|
||||
const s = slo[(i + pln) % pln];
|
||||
const pp = pa[(i + pln - 1) % pln];
|
||||
const ps = slo[(i + pln - 1) % pln];
|
||||
newp.push(p.follow(p.slopeTo(pp).normal(), 0.001));
|
||||
newp.push(p.follow(s.clone().normal().invert(), 0.001));
|
||||
} else if (d > 90) {
|
||||
const s = slo[(i + pln) % pln];
|
||||
const pp = pa[(i + pln - 1) % pln];
|
||||
const ps = slo[(i + pln - 1) % pln];
|
||||
newp.push(p.follow(p.slopeTo(pp), 0.001));
|
||||
newp.push(p.follow(s, 0.001));
|
||||
} else {
|
||||
p.parent = newp;
|
||||
newp.push(p);
|
||||
}
|
||||
}
|
||||
return newp;
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create convex hull from points and add to polygon (mutates polygon).
|
||||
* Only used in fdm/slice.js for enclosing supports.
|
||||
*
|
||||
* @param {Polygon} poly - target polygon (will be mutated)
|
||||
* @param {Point[]} points - points to form convex hull
|
||||
* @returns {Polygon} the mutated polygon
|
||||
*/
|
||||
export function createConvexHull(poly, points) {
|
||||
function removeMiddle(a, b, c) {
|
||||
let cross = (a.x - b.x) * (c.y - b.y) - (a.y - b.y) * (c.x - b.x);
|
||||
let dot = (a.x - b.x) * (c.x - b.x) + (a.y - b.y) * (c.y - b.y);
|
||||
return cross < 0 || cross == 0 && dot <= 0;
|
||||
}
|
||||
|
||||
points.sort(function(a, b) {
|
||||
return a.x != b.x ? a.x - b.x : a.y - b.y;
|
||||
});
|
||||
|
||||
let n = points.length;
|
||||
let hull = [];
|
||||
|
||||
for (let i = 0; i < 2 * n; i++) {
|
||||
let j = i < n ? i : 2 * n - 1 - i;
|
||||
while (hull.length >= 2 && removeMiddle(hull[hull.length - 2], hull[hull.length - 1], points[j]))
|
||||
hull.pop();
|
||||
hull.push(points[j]);
|
||||
}
|
||||
|
||||
hull.pop();
|
||||
poly.addPoints(hull);
|
||||
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all intersections of a line segment with polygon edges.
|
||||
* Results are sorted by distance from lp1.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to test
|
||||
* @param {Point} lp1 - line segment start
|
||||
* @param {Point} lp2 - line segment end
|
||||
* @param {boolean} deep - recurse into inner polygons
|
||||
* @returns {Point[]} array of intersection points
|
||||
*/
|
||||
export function intersections(poly, lp1, lp2, deep) {
|
||||
let list = [];
|
||||
poly.forEachSegment(function(pp1, pp2, ip1, ip2) {
|
||||
let int = util.intersect(lp1, lp2, pp1, pp2, base.key.SEGINT, false);
|
||||
if (int) {
|
||||
list.push(int);
|
||||
pp1.pos = ip1;
|
||||
pp2.pos = ip2;
|
||||
}
|
||||
});
|
||||
if (deep && poly.inner) {
|
||||
poly.inner.forEach(p => {
|
||||
let ints = intersections(p, lp1, lp2);
|
||||
if (ints) list.appendAll(ints);
|
||||
});
|
||||
}
|
||||
list.sort(function(p1, p2) {
|
||||
return util.distSq(lp1, p1) - util.distSq(lp1, p2);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk points noting z deltas and smoothing z sawtooth patterns.
|
||||
* Used to smooth low-rez surface contouring (mutates polygon).
|
||||
*
|
||||
* @param {Polygon} poly - polygon to smooth (mutated)
|
||||
* @param {number} passes - number of smoothing passes
|
||||
* @returns {Polygon} the mutated polygon
|
||||
*/
|
||||
export function refine(poly, passes = 0) {
|
||||
for (let j = 0; j < passes; j++) {
|
||||
let points = poly.points,
|
||||
length = points.length,
|
||||
sn = [], // segment normals
|
||||
vn = []; // vertex normals
|
||||
for (let i = 0; i < length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
sn.push(calc_normal(p1, p2));
|
||||
}
|
||||
for (let i = 0; i < length; i++) {
|
||||
let n1 = sn[(i + length - 1) % length];
|
||||
let n2 = sn[i];
|
||||
let vi = calc_vertex(n1, n2, 1);
|
||||
vn.push(vi);
|
||||
let vl = Math.abs(1 - vi.vl).round(2);
|
||||
// vl should be close to zero on smooth / continuous curves
|
||||
// factoring out hard turns, we smooth the z using the weighted
|
||||
// z values of the points before and after the current point
|
||||
if (vl === 0) {
|
||||
let p0 = points[(i + length - 1) % length];
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
p1.z = (p0.z + p2.z + p1.z) / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if two polygons are essentially equivalent in shape and position.
|
||||
* Checks area, bounds, circularity, and point-to-segment distances.
|
||||
*
|
||||
* @param {Polygon} poly1 - first polygon
|
||||
* @param {Polygon} poly2 - second polygon
|
||||
* @param {boolean} recurse - also check inner polygons
|
||||
* @param {number} precision - tolerance for comparisons
|
||||
* @returns {boolean} true if polygons are equivalent
|
||||
*/
|
||||
export function isEquivalent(poly1, poly2, recurse, precision) {
|
||||
let area1 = Math.abs(poly1.area());
|
||||
let area2 = Math.abs(poly2.area());
|
||||
if (util.isCloseTo(area1, area2, precision || config.precision_poly_area) &&
|
||||
poly1.bounds.equals(poly2.bounds, precision || config.precision_poly_bounds)) {
|
||||
// use circularity near 1 to eliminate the extensive check below
|
||||
let c1 = poly1.circularity(),
|
||||
c2 = poly2.circularity();
|
||||
if (Math.abs(c1 - c2) < config.precision_circularity && ((1 - c1) < config.precision_circularity)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recurse) {
|
||||
let i, ai = poly1.inner,
|
||||
bi = poly2.inner;
|
||||
if (ai !== bi) {
|
||||
if (ai === null || bi === null || ai.length != bi.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < ai.length; i++) {
|
||||
if (!isEquivalent(ai[i], bi[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let exit = true,
|
||||
pointok,
|
||||
dist,
|
||||
min;
|
||||
|
||||
poly1.forEachPoint(i2p => {
|
||||
pointok = false;
|
||||
poly2.forEachSegment((i1p1, i1p2) => {
|
||||
// if point is close to poly, terminate search, go to next point
|
||||
if ((dist = i2p.distToLine(i1p1, i1p2)) < config.precision_poly_merge) {
|
||||
return pointok = true;
|
||||
}
|
||||
// otherwise track min and keep searching
|
||||
min = Math.min(min, dist);
|
||||
});
|
||||
// fail poly if one point is bad
|
||||
if (!pointok) {
|
||||
exit = false;
|
||||
// terminate search
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return exit;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
276
src/geo/poly/mesh.js
Normal file
276
src/geo/poly/mesh.js
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { earcut as earcutLib } from '../base.js';
|
||||
import { newPolygon } from '../polygon.js';
|
||||
import { polygons as POLY } from '../polygons.js';
|
||||
|
||||
/**
|
||||
* Mesh generation utility functions for polygons.
|
||||
* These utilities convert 2D polygons into 3D meshes and geometry.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Triangulate polygon using earcut library.
|
||||
* Returns array of triangular polygons with preserved alignment/swap info.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to triangulate
|
||||
* @returns {Polygon[]} array of triangular polygons
|
||||
*/
|
||||
export function earcut(poly) {
|
||||
// gather all points into a single array including inner polys
|
||||
// keeping track of array offset indices for inners
|
||||
let out = [];
|
||||
let holes = [];
|
||||
|
||||
// flatten points into array for earcut()
|
||||
poly.points.forEach(p => {
|
||||
out.push(p.x, p.y, p.z);
|
||||
});
|
||||
|
||||
// add hole offsets for inner polygons
|
||||
if (poly.inner) {
|
||||
poly.inner.forEach(p => {
|
||||
holes.push(out.length / 3);
|
||||
p.points.forEach(p => {
|
||||
out.push(p.x, p.y, p.z);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// perform earcut()
|
||||
let cut = earcutLib(out, holes, 3);
|
||||
let ret = [];
|
||||
|
||||
// preserve swaps in new polys
|
||||
for (let i = 0; i < cut.length; i += 3) {
|
||||
let p = new (poly.constructor)();
|
||||
p._aligned = poly._aligned;
|
||||
p._swapped = poly._swapped;
|
||||
for (let j = 0; j < 3; j++) {
|
||||
let n = cut[i + j] * 3;
|
||||
p.add(out[n], out[n + 1], out[n + 2]);
|
||||
}
|
||||
ret.push(p);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn 2D polygon into a 2.5D ribbon extruded in Z.
|
||||
* Returns flat array of vertices for mesh rendering.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to extrude as ribbon
|
||||
* @param {number} z - Z height (default 1)
|
||||
* @param {number} zadd - Z offset (default 0)
|
||||
* @param {boolean} rev - reverse winding (default false)
|
||||
* @returns {number[]} flat array of triangle vertices
|
||||
*/
|
||||
export function ribbonZ(poly, z = 1, zadd = 0, rev) {
|
||||
let clone = poly.clone().setClockwise();
|
||||
let faces = [];
|
||||
let points = clone.points;
|
||||
let length = points.length;
|
||||
if (rev) {
|
||||
points = points.slice().reverse();
|
||||
}
|
||||
for (let i=0; i<length; i++) {
|
||||
let p0 = points[i];
|
||||
let p1 = points[(i + 1) % length];
|
||||
faces.push(p0.x, p0.y, p0.z + zadd);
|
||||
faces.push(p1.x, p1.y, p1.z + z + zadd);
|
||||
faces.push(p1.x, p1.y, p0.z + zadd);
|
||||
faces.push(p0.x, p0.y, p0.z + zadd);
|
||||
faces.push(p0.x, p0.y, p0.z + z + zadd);
|
||||
faces.push(p1.x, p1.y, p1.z + z + zadd);
|
||||
}
|
||||
return faces;
|
||||
}
|
||||
|
||||
/**
|
||||
* For turning a poly with an inner offset into a 3D mesh.
|
||||
* Requires inner has same circularity and <= num points.
|
||||
* Primarily used to make chamfers in mesh:tool.
|
||||
*
|
||||
* @param {Polygon} poly - polygon with exactly one inner
|
||||
* @param {boolean} swap - swap winding for normals
|
||||
* @returns {number[]|undefined} flat array of triangle vertices or undefined if invalid
|
||||
*/
|
||||
export function ribbonMesh(poly, swap) {
|
||||
if (!(poly.inner && poly.inner.length === 1)) {
|
||||
return undefined;
|
||||
}
|
||||
let outer = poly.clone().setClockwise();
|
||||
let inner = poly.inner[0].clone().setClockwise();
|
||||
let c0 = outer.circularity();
|
||||
let c1 = inner.circularity();
|
||||
let n0 = outer.points.length;
|
||||
let n1 = inner.points.length;
|
||||
let p0 = outer.points.slice();
|
||||
let p1 = inner.points.slice();
|
||||
let min = { d: Infinity, i:0, j:0 };
|
||||
// find the closests two points inner/outer
|
||||
for (let i=0; i<p0.length; i++) {
|
||||
for (let j=0; j<p1.length; j++) {
|
||||
let d = p0[i].distTo2D(p1[j]);
|
||||
if (d < min.d) {
|
||||
min = { d, i, j };
|
||||
}
|
||||
}
|
||||
}
|
||||
// slide the arrays until the closest points are aligned at index = 0
|
||||
p0 = p0.slice(min.i).concat(p0.slice(0, min.i)); p0.push(p0[0]);
|
||||
p1 = p1.slice(min.j).concat(p1.slice(0, min.j)); p1.push(p1[0]);
|
||||
// walk both arrays moving to the next poly + point that forms
|
||||
// the shortest line segment between the two points (inner / outer)
|
||||
let faces = [];
|
||||
let pi0 = 0;
|
||||
let pi1 = 0;
|
||||
let pp0 = p0[pi0];
|
||||
let pp1 = p1[pi1];
|
||||
for (;;) {
|
||||
let pn0 = p0[pi0 + 1];
|
||||
let pn1 = p1[pi1 + 1];
|
||||
if ((!pn0 && pn1) || (pn1 && pp0.distTo2D(pn1) < pp1.distTo2D(pn0))) {
|
||||
// emit and increment bottom
|
||||
faces.push(pp0.x, pp0.y, pp0.z);
|
||||
if (swap) {
|
||||
faces.push(pp1.x, pp1.y, pp1.z);
|
||||
faces.push(pn1.x, pn1.y, pn1.z);
|
||||
} else {
|
||||
faces.push(pn1.x, pn1.y, pn1.z);
|
||||
faces.push(pp1.x, pp1.y, pp1.z);
|
||||
}
|
||||
pi1++;
|
||||
pp1 = p1[pi1];
|
||||
} else if (pn0) {
|
||||
// emit and increment top
|
||||
faces.push(pp0.x, pp0.y, pp0.z);
|
||||
if (swap) {
|
||||
faces.push(pp1.x, pp1.y, pp1.z);
|
||||
faces.push(pn0.x, pn0.y, pn0.z);
|
||||
} else {
|
||||
faces.push(pn0.x, pn0.y, pn0.z);
|
||||
faces.push(pp1.x, pp1.y, pp1.z);
|
||||
}
|
||||
pi0++;
|
||||
pp0 = p0[pi0];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return faces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrude polygon (with inner voids) into 3D mesh.
|
||||
* Supports chamfers on top and/or bottom edges.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to extrude
|
||||
* @param {number} z - Z height (default 1)
|
||||
* @param {Object} opt - options
|
||||
* @param {number} opt.zadd - Z offset for bottom (default 0)
|
||||
* @param {number} opt.chamfer - chamfer size for both top and bottom (default 0)
|
||||
* @param {number} opt.chamfer_top - chamfer size for top (overrides chamfer)
|
||||
* @param {number} opt.chamfer_bottom - chamfer size for bottom (overrides chamfer)
|
||||
* @returns {number[]} flat array of triangle vertices
|
||||
*/
|
||||
export function extrude(poly, z = 1, opt = {}) {
|
||||
let earcutFaces = earcut(poly); // array of 3-point polygons
|
||||
|
||||
// return just the 2D face when no Z depth specified
|
||||
// used primarily by mesh.sketch render()
|
||||
if (z === 0) {
|
||||
return earcutFaces.map(face => face.points.map(p => [ p.x, p.y, p.z ])).flat().flat();
|
||||
}
|
||||
|
||||
let inv = z < 0;
|
||||
|
||||
if (inv) {
|
||||
z = -z;
|
||||
}
|
||||
|
||||
let chamfer = opt.chamfer || 0;
|
||||
let chamfer_top = opt.chamfer_top || chamfer;
|
||||
let chamfer_bottom = opt.chamfer_bottom || chamfer;
|
||||
|
||||
if (inv) {
|
||||
let tmp = chamfer_top;
|
||||
chamfer_top = chamfer_bottom;
|
||||
chamfer_bottom = tmp;
|
||||
}
|
||||
|
||||
let zadd = (typeof opt === 'number' ? opt : opt.zadd || 0); // z bottom
|
||||
let obj = []; // flat output vertex array (float-x,float-y,float-z,...)
|
||||
let top_face = earcutFaces;
|
||||
let bottom_face = earcutFaces;
|
||||
let z_top = z + zadd;
|
||||
let z_bottom = zadd;
|
||||
let z_side_top = z;
|
||||
let z_side_bottom = z_bottom;
|
||||
|
||||
// chamfer bottom only on negative chamfer
|
||||
if (chamfer < 0) {
|
||||
chamfer_top = 0;
|
||||
chamfer_bottom = -chamfer;
|
||||
}
|
||||
|
||||
// create chamfers (when defined)
|
||||
if (chamfer_top) {
|
||||
let inset = poly.offset(chamfer_top);
|
||||
if (inset.length === 1) {
|
||||
inset[0].setZ(z_top);
|
||||
top_face = earcut(inset[0]);
|
||||
z_side_top -= chamfer_top;
|
||||
let renest = POLY.renest([poly.clone(true).setZ(z_side_top), inset[0]]);
|
||||
for (let rnpoly of renest) {
|
||||
obj.appendAll(ribbonMesh(rnpoly, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chamfer_bottom) {
|
||||
let inset = poly.offset(chamfer_bottom);
|
||||
if (inset.length === 1) {
|
||||
inset[0].setZ(0);
|
||||
bottom_face = earcut(inset[0]);
|
||||
z_side_top -= chamfer_top || chamfer_bottom;
|
||||
z_side_bottom += chamfer_bottom;
|
||||
let renest = POLY.renest([poly.clone(true).setZ(z_side_bottom), inset[0]]);
|
||||
for (let rnpoly of renest) {
|
||||
obj.appendAll(ribbonMesh(rnpoly, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let p of top_face) {
|
||||
for (let point of p.points) {
|
||||
obj.push(point.x, point.y, z_top);
|
||||
}
|
||||
}
|
||||
|
||||
// bottom face (reversed to reverse normals)
|
||||
for (let p of bottom_face) {
|
||||
for (let point of p.points.reverse()) {
|
||||
obj.push(point.x, point.y, z_bottom);
|
||||
}
|
||||
}
|
||||
|
||||
// outside wall
|
||||
let rib = z - chamfer_top - chamfer_bottom;
|
||||
obj.appendAll(ribbonZ(poly, rib, z_side_bottom));
|
||||
for (let inner of poly.inner || []) {
|
||||
// inside wall(s)
|
||||
obj.appendAll(ribbonZ(inner, rib, z_side_bottom, true));
|
||||
}
|
||||
|
||||
if (inv) {
|
||||
for (let i=2; i<obj.length; i+=3) {
|
||||
obj[i] -= z;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
374
src/geo/poly/path.js
Normal file
374
src/geo/poly/path.js
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { util } from '../base.js';
|
||||
import { newPoint } from '../point.js';
|
||||
import { newPolygon } from '../polygon.js';
|
||||
import { newSlopeFromAngle } from '../slope.js';
|
||||
import { polygons as POLY } from '../polygons.js';
|
||||
|
||||
/**
|
||||
* Toolpath and CNC utility functions for polygons.
|
||||
* These utilities handle path generation, segmentation, and CNC-specific operations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate a trace path around the inside of a polygon including inner polys.
|
||||
* Returns the noodle and the remainder of the polygon with the noodle removed.
|
||||
* TODO: relocate this code to fdm/post.js
|
||||
*
|
||||
* @param {Polygon} poly - polygon to trace
|
||||
* @param {number} width - trace width
|
||||
* @returns {Object} { noodle: Polygon[], remain: Polygon[] }
|
||||
*/
|
||||
export function noodle(poly, width) {
|
||||
let clone = poly.clone(true);
|
||||
let ins = clone.offset(width) ?? [];
|
||||
let remain = ins.clone(true);
|
||||
let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true));
|
||||
return { noodle: nood, remain };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate center crossing point cloud for thin wall filling.
|
||||
* Only used for fdm thin-wall type 1 (fdm/post.js).
|
||||
* TODO: relocate this code to fdm/post.js
|
||||
*
|
||||
* @param {Polygon} poly - polygon to analyze
|
||||
* @param {number} step - step size for sampling
|
||||
* @param {number} z - Z height
|
||||
* @param {number} min - minimum distance threshold
|
||||
* @param {number} max - maximum distance threshold
|
||||
* @param {Object} opt - options
|
||||
* @param {boolean} opt.lines - return lines instead of points (default false)
|
||||
* @param {number} opt.mindist - minimum distance for point joining (default step * 1.5)
|
||||
* @returns {Polygon[]|Point[]} array of polygons or points
|
||||
*/
|
||||
export function centers(poly, step, z, min, max, opt = {}) {
|
||||
let cloud = [],
|
||||
bounds = poly.bounds,
|
||||
lines = opt.lines || false,
|
||||
stepoff = step / 2,
|
||||
set = [poly.points];
|
||||
|
||||
if (poly.inner) {
|
||||
for (let inner of poly.inner) {
|
||||
set.push(inner.points);
|
||||
}
|
||||
}
|
||||
|
||||
for (let y of util.lerp(bounds.miny + stepoff, bounds.maxy - stepoff, step, true)) {
|
||||
let ints = [];
|
||||
for (let points of set) {
|
||||
let length = points.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let p1 = points[i % length];
|
||||
let p2 = points[(i + 1) % length];
|
||||
if (
|
||||
(p1.y <= y && p2.y > y) ||
|
||||
(p1.y > y && p2.y <= y)
|
||||
) ints.push([p1, p2]);
|
||||
}
|
||||
}
|
||||
let cntr = [];
|
||||
if (ints.length && ints.length % 2 === 0) {
|
||||
for (let int of ints) {
|
||||
let [p1, p2] = int;
|
||||
if (p2.y < p1.y) {
|
||||
let tp = p1;
|
||||
p1 = p2;
|
||||
p2 = tp;
|
||||
}
|
||||
let minx = Math.min(p1.x, p2.x);
|
||||
let maxx = Math.max(p1.x, p2.x);
|
||||
let miny = Math.min(p1.y, p2.y);
|
||||
let maxy = Math.max(p1.y, p2.y);
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = maxy - miny;
|
||||
let pct = (y - miny) / dy;
|
||||
let xpo = p1.x + pct * dx;
|
||||
cntr.push(xpo);
|
||||
}
|
||||
}
|
||||
cntr.sort((a, b) => {
|
||||
return b - a;
|
||||
});
|
||||
let lp, eo = 0;
|
||||
for (let x of cntr) {
|
||||
let p = newPoint(x, y, z);
|
||||
if (eo++ % 2) {
|
||||
let d = lp.distTo2D(p);
|
||||
if (d >= min && d <= max) {
|
||||
if (lines) {
|
||||
cloud.push(lp);
|
||||
cloud.push(p);
|
||||
} else {
|
||||
cloud.push(newPoint(
|
||||
(lp.x + p.x) / 2, y, z
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lp = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let x of util.lerp(bounds.minx + stepoff, bounds.maxx - stepoff, step, true)) {
|
||||
let ints = [];
|
||||
for (let points of set) {
|
||||
let length = points.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let p1 = points[i % length];
|
||||
let p2 = points[(i + 1) % length];
|
||||
if (
|
||||
(p1.x <= x && p2.x > x) ||
|
||||
(p1.x > x && p2.x <= x)
|
||||
) ints.push([p1, p2]);
|
||||
}
|
||||
}
|
||||
let cntr = [];
|
||||
if (ints.length && ints.length % 2 === 0) {
|
||||
for (let int of ints) {
|
||||
let [p1, p2] = int;
|
||||
if (p2.x < p1.x) {
|
||||
let tp = p1;
|
||||
p1 = p2;
|
||||
p2 = tp;
|
||||
}
|
||||
let minx = Math.min(p1.x, p2.x);
|
||||
let maxx = Math.max(p1.x, p2.x);
|
||||
let miny = Math.min(p1.y, p2.y);
|
||||
let maxy = Math.max(p1.y, p2.y);
|
||||
let dx = maxx - minx;
|
||||
let dy = p2.y - p1.y;
|
||||
let pct = (x - minx) / dx;
|
||||
let ypo = p1.y + pct * dy;
|
||||
cntr.push(ypo);
|
||||
}
|
||||
}
|
||||
cntr.sort((a, b) => {
|
||||
return b - a;
|
||||
});
|
||||
let lp, eo = 0;
|
||||
for (let y of cntr) {
|
||||
let p = newPoint(x, y, z);
|
||||
if (eo++ % 2) {
|
||||
let d = lp.distTo2D(p);
|
||||
if (d >= min && d <= max) {
|
||||
if (lines) {
|
||||
cloud.push(lp);
|
||||
cloud.push(p);
|
||||
} else {
|
||||
cloud.push(newPoint(
|
||||
x, (lp.y + p.y) / 2, z
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lp = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines) {
|
||||
return cloud;
|
||||
}
|
||||
|
||||
let mindist = opt.mindist || step * 1.5;
|
||||
|
||||
function build(polyPoints) {
|
||||
let lastp = polyPoints.last();
|
||||
let minp;
|
||||
let mind = Infinity;
|
||||
for (let point of cloud) {
|
||||
let dist = point.distTo2D(lastp);
|
||||
if (dist < mindist && dist < mind) {
|
||||
mind = dist;
|
||||
minp = point;
|
||||
}
|
||||
}
|
||||
if (minp) {
|
||||
cloud = cloud.filter(p => p !== minp);
|
||||
polyPoints.push(minp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// join points into polys
|
||||
let polys = [];
|
||||
let polyPoints = [];
|
||||
while (cloud.length) {
|
||||
if (polyPoints.length === 0) {
|
||||
polyPoints = [cloud.shift()];
|
||||
polys.push(polyPoints);
|
||||
continue;
|
||||
}
|
||||
if (build(polyPoints)) {
|
||||
continue;
|
||||
}
|
||||
if (!polyPoints.flip) {
|
||||
polyPoints.reverse();
|
||||
polyPoints.flip = true;
|
||||
continue;
|
||||
}
|
||||
if (polyPoints.length) {
|
||||
polyPoints = [];
|
||||
} else {
|
||||
throw "whoop there it is";
|
||||
}
|
||||
}
|
||||
|
||||
return polys
|
||||
.filter(polyPoints => polyPoints.length > 1)
|
||||
.map(polyPoints => {
|
||||
let np = newPolygon().setOpen();
|
||||
for (let p of polyPoints) {
|
||||
np.push(p);
|
||||
}
|
||||
if (np.last().distTo2D(np.first()) <= max) {
|
||||
np.setClosed();
|
||||
}
|
||||
np = np.clean();
|
||||
return np;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split long straight lines into segments no longer than `max`.
|
||||
* Returns a new polygon. Optionally `mark` points with their derived
|
||||
* segment start point (hinting for thin walls), `wrap` the poly first
|
||||
* point by appending to the end (adaptive walls), or stop segmentation at `maxoff`.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to segment
|
||||
* @param {number} max - maximum segment length (default 1)
|
||||
* @param {boolean} mark - mark points with segment start (default false)
|
||||
* @param {boolean} wrap - wrap first point to end (default false)
|
||||
* @param {number} maxoff - maximum offset for segmentation (default Infinity)
|
||||
* @returns {Polygon} new segmented polygon
|
||||
*/
|
||||
export function segment(poly, max = 1, mark = false, wrap = false, maxoff = Infinity) {
|
||||
const newp = [];
|
||||
const points = poly.points;
|
||||
const length = points.length;
|
||||
const l0 = poly.open ? length - 1 : length;
|
||||
for (let i=0, p=points, l1=length, l2=l1+1; i<l0; i++) {
|
||||
const p1 = p[i];
|
||||
const p2 = p[(i + 1) % l1];
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const dl = Math.sqrt(dx * dx + dy * dy);
|
||||
newp.push(p1);
|
||||
if (mark) p1.segment = p1;
|
||||
// if segment shorter than max (delta) or
|
||||
// maxoff is exhausted (only segmenting up to some length)
|
||||
// then skip sub-segmenting the current segment
|
||||
if (dl < max || maxoff < 0) {
|
||||
continue;
|
||||
}
|
||||
maxoff -= dl;
|
||||
const div = dl / max;
|
||||
const fit = div | 0;
|
||||
const add = fit - 1;
|
||||
const ix = dx / fit;
|
||||
const iy = dy / fit;
|
||||
let ox = p1.x + ix;
|
||||
let oy = p1.y + iy;
|
||||
for (let i=0; i<add; i++) {
|
||||
newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2));
|
||||
ox += ix;
|
||||
oy += iy;
|
||||
// mark new point with first point of originating segment
|
||||
if (mark) newp.peek().segment = p1;
|
||||
}
|
||||
}
|
||||
if (newp.length > length) {
|
||||
if (wrap) {
|
||||
newp.push(newp[0]);
|
||||
}
|
||||
return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(poly.open);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* For any poly points closer than `dist`, replace them with their midpoint.
|
||||
* Only used by cam/op-pocket.js.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to process
|
||||
* @param {number} dist - distance threshold (default 0.01)
|
||||
* @returns {Polygon} new polygon with midpoints or original if no changes
|
||||
*/
|
||||
export function midpoints(poly, dist = 0.01) {
|
||||
const newp = [];
|
||||
const points = poly.points;
|
||||
const length = points.length;
|
||||
const l0 = poly.open ? length - 1 : length;
|
||||
let mod = 0;
|
||||
for (let i=0, p=points; i<l0; i++) {
|
||||
const p1 = p[i];
|
||||
const p2 = p[(i + 1) % length];
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const ln = Math.sqrt(dx * dx + dy * dy);
|
||||
if (ln < dist) {
|
||||
newp.push(p1.midPointTo(p2));
|
||||
mod++;
|
||||
} else {
|
||||
newp.push(p1);
|
||||
}
|
||||
}
|
||||
if (mod) {
|
||||
return newPolygon().addPoints(newp.map(p => p.clone())).setOpenValue(poly.open);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dogbone joints for CNC operations at sharp interior corners.
|
||||
* Mutates the polygon in place. Recursively processes inner polygons.
|
||||
*
|
||||
* @param {Polygon} poly - polygon to modify (mutated)
|
||||
* @param {number} dist - dogbone distance/size
|
||||
* @param {boolean} reverse - reverse winding direction
|
||||
* @returns {Polygon} the mutated polygon
|
||||
*/
|
||||
export function addDogbones(poly, dist, reverse) {
|
||||
let open = poly.open;
|
||||
let isCW = poly.isClockwise();
|
||||
if (reverse || poly.parent) isCW = !isCW;
|
||||
let oldpts = poly.points.slice();
|
||||
let lastpt = oldpts[oldpts.length - 1];
|
||||
let lastsl = lastpt.slopeTo(oldpts[0]).toUnit();
|
||||
let length = oldpts.length + (open ? 0 : 1);
|
||||
let newpts = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
let nextpt = oldpts[i % oldpts.length];
|
||||
let nextsl = lastpt.slopeTo(nextpt).toUnit();
|
||||
let adiff = lastsl.angleDiff(nextsl, true);
|
||||
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
|
||||
if (!open || (i > 1 && i < length)) {
|
||||
if (isCW && adiff > 45) {
|
||||
let newa = newSlopeFromAngle(lastsl.angle + bdiff);
|
||||
newpts.push(lastpt.projectOnSlope(newa, dist));
|
||||
newpts.push(lastpt.clone());
|
||||
} else if (!isCW && adiff < -45) {
|
||||
let newa = newSlopeFromAngle(lastsl.angle - bdiff);
|
||||
newpts.push(lastpt.projectOnSlope(newa, dist));
|
||||
newpts.push(lastpt.clone());
|
||||
}
|
||||
}
|
||||
lastsl = nextsl;
|
||||
lastpt = nextpt;
|
||||
if (i < oldpts.length) {
|
||||
newpts.push(nextpt);
|
||||
}
|
||||
}
|
||||
poly.points = newpts;
|
||||
if (poly.inner) {
|
||||
poly.inner.forEach(inner => addDogbones(inner, dist, true));
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue