CAM routing refactor, area operation, and taper ball tool implementation
Major routing improvements: - Refactor depth-first routing algorithm for better path optimization - Reimplemented outline, roughing, pocket, and trace as area operation wrappers - Add pocket shadow travel routing with optimized descent using travel bounds - Improve contour routing with arc detection and polyEmit arc strategy - Add area smoothing and ease-down options - Fix outline handling with thru holes and surface reversal on tip2tip - Move widget shadow methods into widget class for faster hole detection - Better camInnerFirst support and proper feed/plunge defaults Tool system improvements: - Implement taper ball tool type with full support - Add renderTool2() for profile-based visualization - Convert tool menu to init code with bound vars - Fix auto tool and auto spindle for prep operations - Update tool profile generation for taperball geometry - Add calcTaperBallExtent() for proper ball tangency calculations Code cleanup: - Remove duplicated slicer code from specialized slicers - Asyncify shadowAt operations - Deprecate pocket engrave operation - Simplify upNover implementation - Fix animation with new tool+settings requirements
This commit is contained in:
parent
761568b533
commit
e0b1c4e31f
44 changed files with 1607 additions and 2894 deletions
|
|
@ -518,27 +518,22 @@ export function shapeToPath(shape, points, closed) {
|
|||
/**
|
||||
* Generate a list of points approximating a circular arc.
|
||||
* @param {Point} start - the starting point of the arc.
|
||||
* @param {Point} end - the ending point of the arc.
|
||||
* @param {number} [arcdivs= 24] - the number of lines to use to represent PI radians
|
||||
* @param {number} opts.radius - the radius of the arc. If undefined, will use the
|
||||
* start and end points to infer the radius.
|
||||
* @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise.
|
||||
* generating the points.
|
||||
*
|
||||
* @param {Point} end - the ending point of the arc (not included).
|
||||
* @param {number} [arcdivs = 24] - the number of lines to use to represent PI radians
|
||||
* @param {number} opts.radius - the radius of the arc. If undefined, will use the start and end points to infer the radius.
|
||||
* @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. generating the points.
|
||||
* @return {Array<Point>} an array of points representing the arc.
|
||||
*/
|
||||
export function arcToPath(start, end, arcdivs = 24, opts) {
|
||||
|
||||
let { clockwise, center, radius } = opts;
|
||||
|
||||
// @type {Point}
|
||||
if (end.x === undefined && end.x === undefined && center === undefined) {
|
||||
// bambu generates loop z or wipe loop arcs in place
|
||||
// console.log({ skip_empty_arc: rec });
|
||||
return;
|
||||
}
|
||||
|
||||
if(arcdivs <= 2){
|
||||
if (arcdivs <= 2) {
|
||||
return [start.clone(), end.clone()];
|
||||
}
|
||||
|
||||
|
|
@ -573,9 +568,9 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
|
|||
let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI;
|
||||
let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians
|
||||
let samePoint = Math.abs(ad) < 0.001
|
||||
let ofFull = Math.abs(ad)/(2*Math.PI);
|
||||
let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4);
|
||||
let step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps;
|
||||
let ofFull = Math.abs(ad) / (2 * Math.PI);
|
||||
let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4);
|
||||
let step = (samePoint ? (Math.PI * 2) * (clockwise ? -1 : 1) : ad) / steps;
|
||||
let zStart = start.z;
|
||||
let zStep = -dz / steps;
|
||||
let rot = a1;
|
||||
|
|
@ -597,6 +592,7 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
|
|||
zStart += zStep;
|
||||
rot += step;
|
||||
}
|
||||
|
||||
// console.log(arr,start,end);
|
||||
return arr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { base, config, earcut, util } from './base.js';
|
||||
import { ClipperLib } from '../ext/clip2.esm.js';
|
||||
import { newBounds } from './bounds.js';
|
||||
import { paths } from './paths.js';
|
||||
import { calc_normal, calc_vertex, paths } from './paths.js';
|
||||
import { newPoint, pointFromClipper } from './point.js';
|
||||
import { polygons as POLY } from './polygons.js';
|
||||
|
||||
|
|
@ -268,6 +268,7 @@ export class Polygon {
|
|||
// generate a trace path around the inside of a polygon
|
||||
// including inner polys. return the noodle and the remainder
|
||||
// of the polygon with the noodle removed (for the next pass)
|
||||
// todo: relocate this code to post.js
|
||||
noodle(width) {
|
||||
let clone = this.clone(true);
|
||||
let ins = clone.offset(width) ?? [];
|
||||
|
|
@ -278,6 +279,7 @@ export class Polygon {
|
|||
|
||||
// generate center crossing point cloud
|
||||
// only used for fdm thin-wall type 1 (fdm/post.js)
|
||||
// todo: relocate this code to post.js
|
||||
centers(step, z, min, max, opt = {}) {
|
||||
let cloud = [],
|
||||
bounds = this.bounds,
|
||||
|
|
@ -736,6 +738,203 @@ export class Polygon {
|
|||
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 {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
|
||||
*/
|
||||
detectArcs(opts = {}) {
|
||||
const {
|
||||
tolerance = 0.1,
|
||||
arcRes = 0.1,
|
||||
minPoints = 4
|
||||
} = opts;
|
||||
|
||||
const points = this.points;
|
||||
const length = points.length;
|
||||
|
||||
if (length < minPoints) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// 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 = this._detectArcAt(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 this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to detect an arc starting at the given index
|
||||
* Uses a greedy approach: grow the arc as long as possible, then validate
|
||||
* @private
|
||||
* @returns {Object|null} arc data or null if no arc detected
|
||||
*/
|
||||
_detectArcAt(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 = this._validateArc(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 this._validateArc(candidates, tolerance, arcRes, minPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a set of points forms a valid arc
|
||||
* @private
|
||||
*/
|
||||
_validateArc(arcPoints, tolerance, arcRes, minPoints) {
|
||||
if (arcPoints.length < minPoints) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate center using well-distributed points
|
||||
const center = this._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
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* add points forming a rectangle around a center point
|
||||
*
|
||||
|
|
@ -1226,77 +1425,6 @@ export class Polygon {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ease down along the polygonal path.
|
||||
*
|
||||
* 1. Travel from fromPoint to closest point on polygon, to rampZ above that that point,
|
||||
* 2. ease-down starts, following the polygonal path, decreasing Z at a fixed slope until target Z is hit,
|
||||
* 3. then the rest of the path is completed and repeated at target Z until touchdown point is reached.
|
||||
* 4. this function should probably move to CAM prepare since it's only called from there
|
||||
*
|
||||
* possibly no longer used anywhere
|
||||
*/
|
||||
forEachPointEaseDown(fn, fromPoint, degrees = 45) {
|
||||
let index = this.findClosestPointTo(fromPoint).index,
|
||||
fromZ = fromPoint.z,
|
||||
offset = 0,
|
||||
points = this.points,
|
||||
length = points.length,
|
||||
touch = -1, // first point to touch target z
|
||||
targetZ = points[0].z,
|
||||
dist2next,
|
||||
last,
|
||||
next,
|
||||
done;
|
||||
|
||||
// Slope for computations.
|
||||
const slope = Math.tan((degrees * Math.PI) / 180);
|
||||
// Z height above polygon Z from which to start the ease-down.
|
||||
// Machine will travel from "fromPoint" to "nearest point x, y, z' => with z' = point z + rampZ",
|
||||
// then start the ease down along path.
|
||||
const rampZ = 2.0;
|
||||
while (true) {
|
||||
next = points[index % length];
|
||||
if (last && next.z < fromZ) {
|
||||
// When "in Ease-Down" (ie. while target Z not yet reached) - follow path while slowly decreasing Z.
|
||||
let deltaZ = fromZ - next.z;
|
||||
dist2next = last.distTo2D(next);
|
||||
let deltaZFullMove = dist2next * slope;
|
||||
|
||||
if (deltaZFullMove > deltaZ) {
|
||||
// Too long: easing along full path would overshoot depth, synth intermediate point at target Z.
|
||||
//
|
||||
// XXX: please check my super basic trig - this should follow from `last` to `next` up until the
|
||||
// intersect at the target Z distance.
|
||||
fn(last.followTo(next, dist2next * deltaZ / deltaZFullMove).setZ(next.z), offset++);
|
||||
} else {
|
||||
// Ok: execute full move at desired slope.
|
||||
next = next.clone().setZ(fromZ - deltaZFullMove);
|
||||
}
|
||||
|
||||
fromZ = next.z;
|
||||
} else if (offset === 0 && next.z < fromZ) {
|
||||
// First point, move to rampZ height above next.
|
||||
let deltaZ = fromZ - next.z;
|
||||
fromZ = next.z + Math.min(deltaZ, rampZ)
|
||||
next = next.clone().setZ(fromZ);
|
||||
}
|
||||
last = next;
|
||||
fn(next, offset++);
|
||||
if ((index % length) === touch) {
|
||||
break;
|
||||
}
|
||||
if (touch < 0 && next.z <= targetZ) {
|
||||
// Save touch-down index so as to be able to "complete" the full cut at target Z,
|
||||
// i.e. keep following the path loop until the touch down point is reached again.
|
||||
touch = ((index + length) % length);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
forEachPoint(fn, close, start) {
|
||||
let index = start || 0,
|
||||
points = this.points,
|
||||
|
|
@ -1328,7 +1456,8 @@ export class Polygon {
|
|||
}
|
||||
|
||||
/**
|
||||
* returns intersections sorted by closest to lp1
|
||||
* given two endpoints of a line
|
||||
* find all intersections sorted by closest to lp1
|
||||
*/
|
||||
intersections(lp1, lp2, deep) {
|
||||
let list = [];
|
||||
|
|
@ -1893,9 +2022,10 @@ export class Polygon {
|
|||
});
|
||||
|
||||
return {
|
||||
point: closest,
|
||||
distance: mindist,
|
||||
index: index
|
||||
point: closest,
|
||||
index: index,
|
||||
poly: this
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2195,7 +2325,8 @@ export class Polygon {
|
|||
// for turning a poly with an inner offset into a
|
||||
// 3d mesh if and only if the inner has the same
|
||||
// circularity and <= num points
|
||||
// primarily used to make chamfers
|
||||
// primarily used to make chamfers in mesh:tool
|
||||
// todo: relocate
|
||||
ribbonMesh(swap) {
|
||||
if (!(this.inner && this.inner.length === 1)) {
|
||||
return undefined;
|
||||
|
|
@ -2437,6 +2568,75 @@ export class Polygon {
|
|||
return this;
|
||||
}
|
||||
|
||||
// walk points noting z deltas and smoothing z sawtooth patterns
|
||||
// used to smooth low-rez surface contouring
|
||||
refine(passes = 0) {
|
||||
for (let j = 0; j < passes; j++) {
|
||||
let points = this.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addDogbones(dist, reverse) {
|
||||
let poly = this;
|
||||
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 => inner.addDogbones(dist, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function slopeDiff(s1, s2) {
|
||||
|
|
|
|||
|
|
@ -41,36 +41,36 @@ const CleanPolygon = Clipper.CleanPolygon,
|
|||
ClipIntersect = ClipType.ctIntersection;
|
||||
|
||||
const POLYS = {
|
||||
clearInner,
|
||||
rayIntersect,
|
||||
alignWindings,
|
||||
setWinding,
|
||||
fillArea,
|
||||
subtract,
|
||||
flatten,
|
||||
offset,
|
||||
trimTo,
|
||||
expand,
|
||||
points,
|
||||
length,
|
||||
renest,
|
||||
route,
|
||||
union,
|
||||
inset,
|
||||
outer,
|
||||
inner,
|
||||
nest,
|
||||
cleanClipperTree,
|
||||
clearInner,
|
||||
diff,
|
||||
xor,
|
||||
setZ,
|
||||
expand,
|
||||
fillArea,
|
||||
filter,
|
||||
toClipper,
|
||||
fingerprint,
|
||||
fingerprintCompare,
|
||||
flatten,
|
||||
fromClipperNode,
|
||||
fromClipperTree,
|
||||
fromClipperTreeUnion,
|
||||
cleanClipperTree,
|
||||
fingerprintCompare,
|
||||
fingerprint
|
||||
inner,
|
||||
inset,
|
||||
length,
|
||||
nest,
|
||||
offset,
|
||||
outer,
|
||||
points,
|
||||
rayIntersect,
|
||||
renest,
|
||||
route,
|
||||
setWinding,
|
||||
setZ,
|
||||
subtract,
|
||||
toClipper,
|
||||
trimTo,
|
||||
union,
|
||||
xor,
|
||||
};
|
||||
|
||||
export { POLYS };
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export async function slice(points, options = {}) {
|
|||
* @param {number} z offset
|
||||
* @param {Obejct} where
|
||||
*/
|
||||
function checkUnderOverOn(p, z, where) {
|
||||
export function checkOverUnderOn(p, z, where) {
|
||||
let delta = p.z - z;
|
||||
if (Math.abs(delta) < config.precision_slice_z) { // on
|
||||
where.on.push(p);
|
||||
|
|
@ -266,7 +266,7 @@ function checkUnderOverOn(p, z, where) {
|
|||
* @param {number} z offset
|
||||
* @returns {Point} intersection point
|
||||
*/
|
||||
function intersectPoints(over, under, z) {
|
||||
export function intersectPoints(over, under, z) {
|
||||
let ip = [];
|
||||
for (let i = 0; i < over.length; i++) {
|
||||
for (let j = 0; j < under.length; j++) {
|
||||
|
|
@ -279,7 +279,7 @@ function intersectPoints(over, under, z) {
|
|||
/**
|
||||
* Ensure points are unique with a cache/key algorithm
|
||||
*/
|
||||
function getCachedPoint(phash, p) {
|
||||
export function getCachedPoint(phash, p) {
|
||||
let cached = phash[p.key];
|
||||
if (!cached) {
|
||||
phash[p.key] = p;
|
||||
|
|
@ -301,7 +301,7 @@ function getCachedPoint(phash, p) {
|
|||
* @param {boolean} [edge]
|
||||
* @returns {Line}
|
||||
*/
|
||||
function makeZLine(phash, p1, p2, coplanar, edge) {
|
||||
export function makeZLine(phash, p1, p2, coplanar, edge) {
|
||||
p1 = getCachedPoint(phash, p1);
|
||||
p2 = getCachedPoint(phash, p2);
|
||||
let line = newOrderedLine(p1,p2);
|
||||
|
|
@ -336,9 +336,9 @@ export async function sliceZ(z, points, options = {}) {
|
|||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let where = {under: [], over: [], on: []};
|
||||
checkUnderOverOn(p1, z, where);
|
||||
checkUnderOverOn(p2, z, where);
|
||||
checkUnderOverOn(p3, z, where);
|
||||
checkOverUnderOn(p1, z, where);
|
||||
checkOverUnderOn(p2, z, where);
|
||||
checkOverUnderOn(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) {
|
||||
|
|
@ -871,7 +871,5 @@ export const slicer = {
|
|||
slice,
|
||||
sliceZ,
|
||||
slicePost: {},
|
||||
sliceDedup: removeDuplicateLines,
|
||||
sliceConnect
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ const renamed = {
|
|||
roughingOn: "camRoughOn",
|
||||
roughingOver: "camRoughOver",
|
||||
roughingPlunge: "camRoughPlunge",
|
||||
roughingPocket: "camRoughVoid",
|
||||
roughingSpeed: "camRoughSpeed",
|
||||
roughingSpindle: "camRoughSpindle",
|
||||
roughingStock: "camRoughStock",
|
||||
|
|
@ -428,14 +427,18 @@ export const conf = {
|
|||
camAreaMode: "clear",
|
||||
camAreaTrace: "none",
|
||||
camAreaSurface: "linear",
|
||||
camAreaDirection: "climb",
|
||||
camAreaAngle: 0,
|
||||
camAreaOver: 0.4,
|
||||
camAreaDown: 1,
|
||||
camAreaSpeed: 1000,
|
||||
camAreaPlunge: 100,
|
||||
camAreaFollow: 15,
|
||||
camAreaExpand: 0,
|
||||
camAreaSmooth: 1,
|
||||
camAreaRefine: 0,
|
||||
camAreaSmooth: 1,
|
||||
camAreaDogbones: false,
|
||||
camAreaRevbones: false,
|
||||
camAreaOutline: false,
|
||||
camContourAngle: 85,
|
||||
camContourBottom: false,
|
||||
|
|
@ -528,23 +531,19 @@ export const conf = {
|
|||
camOriginOffZ: 0,
|
||||
camOriginTop: true,
|
||||
camOutlineDogbone: false,
|
||||
camOutlineRevbone: false,
|
||||
camOutlineDown: 3,
|
||||
camOutlineIn: false,
|
||||
camOutlineOmitThru: false,
|
||||
camOutlineOmitVoid: false,
|
||||
camOutlineOn: true,
|
||||
camOutlineOut: true,
|
||||
camOutlineOver: 0.4,
|
||||
camOutlineOverCount: 1,
|
||||
camOutlinePlunge: 250,
|
||||
camOutlineSpeed: 800,
|
||||
camOutlineSpindle: 1000,
|
||||
camOutlineTool: 1000,
|
||||
camOutlineTop: true,
|
||||
camOutlineWide: false,
|
||||
camPocketContour: false,
|
||||
camPocketDown: 1,
|
||||
camPocketEngrave: false,
|
||||
camPocketExpand: 0,
|
||||
camPocketFollow: 5,
|
||||
camPocketOutline: false,
|
||||
|
|
@ -562,7 +561,6 @@ export const conf = {
|
|||
camRegisterThru: 5,
|
||||
camRoughAll: true,
|
||||
camRoughDown: 2,
|
||||
camRoughFlat: true,
|
||||
camRoughIn: true,
|
||||
camRoughOmitThru: false,
|
||||
camRoughOmitVoid: false,
|
||||
|
|
@ -575,7 +573,6 @@ export const conf = {
|
|||
camRoughStockZ: 0,
|
||||
camRoughTool: 1000,
|
||||
camRoughTop: true,
|
||||
camRoughVoid: false,
|
||||
camStockClipTo: false,
|
||||
camStockIndexed: false,
|
||||
camStockIndexGrid: true,
|
||||
|
|
@ -603,7 +600,6 @@ export const conf = {
|
|||
camTraceType: "follow",
|
||||
camTraceZBottom: 0,
|
||||
camTraceZTop: 0,
|
||||
camTrueShadow: false,
|
||||
camZAnchor: "middle",
|
||||
camZBottom: 0,
|
||||
camZClearance: 1,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,18 @@ const LISTS = {
|
|||
surftyp: [
|
||||
{ name: "linear" },
|
||||
{ name: "offset" },
|
||||
],
|
||||
direction: [
|
||||
{ name: "climb" },
|
||||
{ name: "conventional" },
|
||||
{ name: "alternating" },
|
||||
],
|
||||
camtool: [
|
||||
{ name: "flat end", id: "endmill" },
|
||||
{ name: "ball end", id: "ballmill" },
|
||||
{ name: "taper tip", id: "tapermill" },
|
||||
{ name: "taper ball", id: "taperball" },
|
||||
{ name: "drill bit", id: "drill" },
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -918,25 +918,6 @@ function init_one() {
|
|||
deviceExport: $('device-exp'),
|
||||
deviceSave: $('device-save'),
|
||||
|
||||
toolsSave: $('tools-save'),
|
||||
toolsClose: $('tools-close'),
|
||||
toolsImport: $('tools-import'),
|
||||
toolsExport: $('tools-export'),
|
||||
toolSelect: $('tool-select'),
|
||||
toolAdd: $('tool-add'),
|
||||
toolCopy: $('tool-dup'),
|
||||
toolDelete: $('tool-del'),
|
||||
toolType: $('tool-type'),
|
||||
toolName: $('tool-name'),
|
||||
toolNum: $('tool-num'),
|
||||
toolFluteDiam: $('tool-fdiam'),
|
||||
toolFluteLen: $('tool-flen'),
|
||||
toolShaftDiam: $('tool-sdiam'),
|
||||
toolShaftLen: $('tool-slen'),
|
||||
toolTaperAngle: $('tool-tangle'),
|
||||
toolTaperTip: $('tool-ttip'),
|
||||
toolMetric: $('tool-metric'),
|
||||
|
||||
setMenu: $('set-menu'),
|
||||
settings: $('settings'),
|
||||
settingsBody: $('settingsBody'),
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ class Print {
|
|||
};
|
||||
}
|
||||
|
||||
function outputPoint(point,lastP,emit,{center,arcPoints,retract}) {
|
||||
function outputPoint(point,lastP,emit,{retract}) {
|
||||
// non-move in a new plane means burp out
|
||||
// the old sequence and start a new one
|
||||
if (newlayer || (autolayer && seq.z != point.z)) {
|
||||
|
|
@ -508,7 +508,7 @@ class Print {
|
|||
}
|
||||
}
|
||||
// add point to current sequence
|
||||
scope.addOutput(seq, point, emit, pos.F, tool,{retract,arcPoints});
|
||||
scope.addOutput(seq, point, emit, pos.F, tool, {retract});
|
||||
scope.lastPos = Object.assign({}, pos);
|
||||
scope.lastPosE = pos.E;
|
||||
}
|
||||
|
|
@ -520,19 +520,14 @@ class Print {
|
|||
* @param {number} index - The line number of the g-code file that contains the G2 or G3 command.
|
||||
*/
|
||||
function G2G3(g2, line, index) {
|
||||
const axes = {};
|
||||
const {point, prevPoint, center} = processLine(line,axes);
|
||||
|
||||
// console.log(structuredClone({point,prevPoint,center}));
|
||||
|
||||
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? []
|
||||
let emit = g2 ? 2 : 3;
|
||||
|
||||
// console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit}));
|
||||
// console.log("pointer point",{point,prevPoint,center,arcPoints,emit});
|
||||
|
||||
outputPoint(point,prevPoint,emit,{center,arcPoints});
|
||||
// scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints});
|
||||
let axes = {};
|
||||
let { point, prevPoint, center } = processLine(line, axes);
|
||||
let arcPoints = arcToPath(prevPoint, point, 64, { clockwise: g2, center }) ?? [];
|
||||
for (let point of arcPoints) {
|
||||
outputPoint(point, prevPoint, 1, {});
|
||||
prevPoint = point;
|
||||
}
|
||||
outputPoint(point, prevPoint, 1, {});
|
||||
}
|
||||
|
||||
function G0G1(g0, line) {
|
||||
|
|
|
|||
|
|
@ -47,11 +47,12 @@ async function path(levels, update, opts = {}) {
|
|||
return [];
|
||||
}
|
||||
|
||||
const isCAM = is_cam();
|
||||
const dark = is_dark();
|
||||
const tools = opts.tools || {};
|
||||
const flat = opts.flat;
|
||||
const thin = opts.thin && !flat;
|
||||
const ckspeed = opts.speed !== false;
|
||||
const ckspeed = isCAM || opts.speed !== false;
|
||||
const headColor = 0x888888;
|
||||
const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa);
|
||||
const printColor = opts.print >= 0 ? opts.print : 0x777700;
|
||||
|
|
@ -186,27 +187,7 @@ async function path(levels, update, opts = {}) {
|
|||
if (arrowAll || lastOut.emit !== out.emit) {
|
||||
heads.push({p1: lastOutPoint, p2: outPoint});
|
||||
}
|
||||
const op = outPoint, lp = lastOutPoint;
|
||||
// const moved = Math.max(
|
||||
// Math.abs(op.x - lp.x),
|
||||
// Math.abs(op.y - lp.y),
|
||||
// Math.abs(op.z - lp.z));
|
||||
// if (moved < 0.0001) return;
|
||||
if (is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit
|
||||
// checks if a new poly should be started
|
||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||
current = newPolygon().setOpen();
|
||||
current.push(lastOutPoint);
|
||||
current.color = color(out);
|
||||
pushPrint(out.tool, current);
|
||||
}
|
||||
out.arcPoints.forEach(p => {
|
||||
current.push(p);
|
||||
})
|
||||
current.push(outPoint);
|
||||
} else if (out.emit) {
|
||||
// explicity G1 in CAM mode
|
||||
// just a non-move in other modes
|
||||
if (out.emit) {
|
||||
// checks if a new poly should be started
|
||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||
current = newPolygon().setOpen();
|
||||
|
|
|
|||
|
|
@ -434,6 +434,7 @@ function newDiv(opt = {}) {
|
|||
(opt.addto || addTo).appendChild(div);
|
||||
if (opt.addto) lastDiv = addTo = div;
|
||||
if (opt.class) div.setAttribute('class', opt.class);
|
||||
if (opt.content) div.innerHTML = opt.content;
|
||||
if (opt.group !== false) {
|
||||
lastGroup?.push(div);
|
||||
div._group = groupName;
|
||||
|
|
@ -444,9 +445,10 @@ function newDiv(opt = {}) {
|
|||
return div;
|
||||
}
|
||||
|
||||
function newExpand(label, opt = {}, opteach = {}) {
|
||||
function newExpand(label, opt = {}) {
|
||||
let div = DOC.createElement('details');
|
||||
div.setAttribute('class', opt.class || 'f-col');
|
||||
if (opt.open) div.setAttribute('open', true);
|
||||
addModeControls(div, opt);
|
||||
|
||||
let summary = DOC.createElement('summary');
|
||||
|
|
@ -606,7 +608,7 @@ function newText(label, options) {
|
|||
function newInput(label, opt = {}) {
|
||||
let row = newDiv(opt),
|
||||
hide = opt.hide,
|
||||
size = opt.size || 5,
|
||||
size = opt.size ?? 5,
|
||||
height = opt.height || 0,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
|
||||
action = opt.action || bindTo || inputAction;
|
||||
|
|
@ -630,6 +632,7 @@ function newInput(label, opt = {}) {
|
|||
row.style.display = hide ? 'none' : '';
|
||||
if (opt.disabled) ip.setAttribute("disabled", "true");
|
||||
if (opt.title) row.setAttribute("title", opt.title);
|
||||
if (opt.id) ip.setAttribute("id", opt.id);
|
||||
if (opt.convert) ip.convert = opt.convert.bind(ip);
|
||||
if (opt.bound) ip.bound = opt.bound;
|
||||
if (opt.action) action = opt.action;
|
||||
|
|
@ -737,6 +740,7 @@ function newSelect(label, options = {}, source) {
|
|||
}
|
||||
row.setAttribute("class", "var-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options.id) ip.setAttribute("id", options.id);
|
||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) row.setAttribute("title", options.title);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
import { base } from '../../geo/base.js';
|
||||
import { avgc } from './utils.js';
|
||||
import { verticesToPoints } from '../../geo/points.js';
|
||||
import { util as mesh_util } from '../../mesh/util.js';
|
||||
import { verticesToPoints } from '../../geo/points.js';
|
||||
import { newPoint } from '../../geo/point.js';
|
||||
import { newPolygon } from '../../geo/polygon.js';
|
||||
import { polygons as POLY } from '../../geo/polygons.js';
|
||||
import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js';
|
||||
|
||||
const { inRange, time } = base.util;
|
||||
const solid_opacity = 1.0;
|
||||
|
|
@ -707,7 +711,7 @@ class Widget {
|
|||
}
|
||||
// used by CAM.shadowAt()
|
||||
this.cache.geo = pos;
|
||||
this.cache.shadow = undefined;
|
||||
delete this.cache.shadow;
|
||||
return pos;
|
||||
}
|
||||
|
||||
|
|
@ -835,6 +839,115 @@ class Widget {
|
|||
hide() {
|
||||
this.mesh.visible = false;
|
||||
}
|
||||
|
||||
async shadowAt(z) {
|
||||
let shadows = this.cache.shadows;
|
||||
if (!shadows) {
|
||||
shadows = this.cache.shadows = {};
|
||||
}
|
||||
let cached = shadows[z];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
// find closest shadow above and use to speed up delta shadow gen
|
||||
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
||||
let minZabove = Math.min(Infinity, ...zover);
|
||||
let shadow = this.#computeShadowAt(z, minZabove);
|
||||
if (minZabove < Infinity) {
|
||||
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
|
||||
}
|
||||
return shadows[z] = POLY.setZ(shadow, z);
|
||||
}
|
||||
|
||||
#ensureShadowCache() {
|
||||
// cache faces with normals up
|
||||
if (this.cache.shadow) {
|
||||
return;
|
||||
}
|
||||
const geo = this.cache.geo;
|
||||
const length = geo.length;
|
||||
const bounds = this.getBoundingBox();
|
||||
const stack = {};
|
||||
// const faces = [];
|
||||
for (let i = Math.floor(bounds.min.z); i <= Math.ceil(bounds.max.z); i++) {
|
||||
stack[i] = [];
|
||||
}
|
||||
for (let i = 0, ip = 0; i < length; i += 3) {
|
||||
const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const n = THREE.computeFaceNormal(a, b, c);
|
||||
if (n.z < 0.001) {
|
||||
continue;
|
||||
// faces.push(a, b, c);
|
||||
}
|
||||
const minZ = Math.floor(Math.min(a.z, b.z, c.z));
|
||||
const maxZ = Math.ceil(Math.max(a.z, b.z, c.z));
|
||||
for (let z = minZ; z <= maxZ; z++) {
|
||||
stack[z].push(a, b, c);
|
||||
}
|
||||
}
|
||||
this.cache.shadow = stack;
|
||||
}
|
||||
|
||||
// union triangles > z (opt cap < ztop) into polygon(s)
|
||||
#computeShadowAt(z, ztop) {
|
||||
this.#ensureShadowCache();
|
||||
const found = [];
|
||||
const stack = this.cache.shadow;
|
||||
let minZ = Math.floor(z);
|
||||
let maxZ = Math.ceil(z);
|
||||
let slices = [];
|
||||
for (let sz = minZ; sz <= maxZ; sz++) {
|
||||
slices.push(stack[sz] ?? []);
|
||||
}
|
||||
for (let faces of slices)
|
||||
for (let i = 0; i < faces.length; ) {
|
||||
const a = faces[i++];
|
||||
const b = faces[i++];
|
||||
const c = faces[i++];
|
||||
if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) {
|
||||
// skip faces over top threshold
|
||||
continue;
|
||||
}
|
||||
if (a.z < z && b.z < z && c.z < z) {
|
||||
// skip faces under threshold
|
||||
continue;
|
||||
} else if (a.z >= z && b.z >= z && c.z >= z) {
|
||||
found.push([a, b, c]);
|
||||
} else {
|
||||
// check faces straddling threshold
|
||||
const where = { under: [], over: [], on: [] };
|
||||
checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where);
|
||||
checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where);
|
||||
checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where);
|
||||
if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length === 2) {
|
||||
if (where.over.length === 2) {
|
||||
found.push([where.over[1], line[0], line[1]]);
|
||||
found.push([where.over[0], where.over[1], line[0]]);
|
||||
} else {
|
||||
found.push([where.over[0], line[0], line[1]]);
|
||||
}
|
||||
} else {
|
||||
console.log({ msg: "invalid ips", line: line, where: where });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let polys = found.map(a => {
|
||||
return newPolygon()
|
||||
.add(a[0].x, a[0].y, a[0].z)
|
||||
.add(a[1].x, a[1].y, a[1].z)
|
||||
.add(a[2].x, a[2].y, a[2].z);
|
||||
});
|
||||
polys = POLY.union(polys, 0, true);
|
||||
|
||||
return polys;
|
||||
}
|
||||
}
|
||||
|
||||
// Widget Grouping API
|
||||
|
|
|
|||
|
|
@ -9,14 +9,16 @@ const asLines = false;
|
|||
|
||||
let stock, center, grid, gridX, gridY, rez;
|
||||
let path, pathIndex, tool, tools, last, toolID = 1;
|
||||
let settings;
|
||||
|
||||
export function init(worker) {
|
||||
const { dispatch } = worker;
|
||||
|
||||
dispatch.animate_setup = function(data, send) {
|
||||
const { settings } = data;
|
||||
settings = data.settings;
|
||||
|
||||
const { process } = settings;
|
||||
const print = worker.current.print;
|
||||
const { print } = worker.current;
|
||||
const density = parseInt(settings.controller.animesh) * 1000;
|
||||
|
||||
pathIndex = 0;
|
||||
|
|
@ -287,7 +289,7 @@ function updateTool(toolobj, send) {
|
|||
if (tool) {
|
||||
send.data({ mesh_del: toolID });
|
||||
}
|
||||
tool = new Tool({ tools }, toolobj.getID());
|
||||
tool = new Tool(settings, toolobj.getID());
|
||||
tool.generateProfile(rez);
|
||||
const flen = tool.fluteLength() || 15;
|
||||
const slen = tool.shaftLength() || 15;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ let nextMeshID = 1,
|
|||
tool,
|
||||
tools,
|
||||
last,
|
||||
settings,
|
||||
stockZ,
|
||||
stockIndexMsg = false,
|
||||
stockSlices,
|
||||
|
|
@ -34,10 +35,10 @@ export function init(worker) {
|
|||
const { dispatch } = worker;
|
||||
|
||||
dispatch.animate_setup2 = function (data, send) {
|
||||
const { settings } = data;
|
||||
settings = data.settings;
|
||||
|
||||
const { controller, process } = settings;
|
||||
const print = worker.current.print;
|
||||
const density = parseInt(settings.controller.animesh) * 1000;
|
||||
const { print } = worker.current;
|
||||
const isIndexed = process.camStockIndexed;
|
||||
|
||||
pathIndex = 0;
|
||||
|
|
@ -141,7 +142,7 @@ function renderPath(send) {
|
|||
}
|
||||
|
||||
const id = toolID;
|
||||
const rezstep = Math.min(tool.maxDiameter(), 1) / 4;
|
||||
const rezstep = tool ? Math.min(tool.maxDiameter(), 1) / 4 : 1;
|
||||
// console.log({ rezstep });
|
||||
|
||||
if (last) {
|
||||
|
|
@ -273,7 +274,7 @@ function toolUpdate(toolid, send) {
|
|||
if (tool) {
|
||||
send.data({ mesh_del: toolID });
|
||||
}
|
||||
tool = new Tool({ tools }, toolid);
|
||||
tool = new Tool(settings, toolid);
|
||||
const Instance = CSG.Instance();
|
||||
const slen = tool.shaftLength() || 15;
|
||||
const srad = tool.shaftDiameter() / 2;
|
||||
|
|
@ -286,6 +287,16 @@ function toolUpdate(toolid, send) {
|
|||
mesh = cylinder(tlen - frad * 2, frad, frad, 20, true)
|
||||
.add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2))
|
||||
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2));
|
||||
} else if (tool.isTaperBall()) {
|
||||
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
|
||||
const brad = trad; // ball radius equals tip radius
|
||||
const taperLen = flen - brad; // taper length excludes ball
|
||||
// shaft at top
|
||||
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
|
||||
// taper cone in middle
|
||||
.add(cylinder(taperLen, trad, frad, 20, true).translate(0, 0, -taperLen / 2))
|
||||
// ball at bottom
|
||||
.add(sphere(brad, 20).translate(0, 0, -(taperLen + brad)));
|
||||
} else if (tool.isTaperMill()) {
|
||||
const trad = Math.max(tool.tipDiameter() / 2, 0.001);
|
||||
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export function selectHoleToggle(id,mesh) {
|
|||
export function clearHolesRec(widget) {
|
||||
if (widget.adds) {
|
||||
widget.adds.length = 0 //clear adds array
|
||||
delete env.poppedRec.drills[widget.id]
|
||||
if (env.poppedRec?.drills) delete env.poppedRec.drills[widget.id]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export function createPopOp(type, map) {
|
|||
}
|
||||
},
|
||||
addNote: () => {
|
||||
if (!op.note && type !== 'flip') {
|
||||
if (!op.note && type !== 'flip' && !op.rec.deprecated) {
|
||||
const divid = `div-${++seed}`;
|
||||
const noteid = `note-${++seed}`;
|
||||
const div = document.createElement('div');
|
||||
|
|
@ -227,8 +227,6 @@ export function createPopOps() {
|
|||
leave: 'camRoughStock',
|
||||
leavez: 'camRoughStockZ',
|
||||
all: 'camRoughAll',
|
||||
voids: 'camRoughVoid',
|
||||
flats: 'camRoughFlat',
|
||||
inside: 'camRoughIn',
|
||||
omitthru: 'camRoughOmitThru',
|
||||
ov_topz: 0,
|
||||
|
|
@ -247,8 +245,6 @@ export function createPopOps() {
|
|||
leavez: UC.newInput(LANG.cr_lstz_s, { title: LANG.cr_lstz_l, convert: toFloat, bound: UC.bound(0, 10), units }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
all: UC.newBoolean(LANG.cr_clst_s, undefined, { title: LANG.cr_clst_l, show: hasIndexing }),
|
||||
voids: UC.newBoolean(LANG.cr_clrp_s, undefined, { title: LANG.cr_clrp_l }),
|
||||
flats: UC.newBoolean(LANG.cr_clrf_s, undefined, { title: LANG.cr_clrf_l }),
|
||||
inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l }),
|
||||
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
|
|
@ -268,12 +264,9 @@ export function createPopOps() {
|
|||
rate: 'camOutlineSpeed',
|
||||
plunge: 'camOutlinePlunge',
|
||||
dogbones: 'camOutlineDogbone',
|
||||
omitvoid: 'camOutlineOmitVoid',
|
||||
revbones: 'camOutlineRevbone',
|
||||
omitthru: 'camOutlineOmitThru',
|
||||
outside: 'camOutlineOut',
|
||||
inside: 'camOutlineIn',
|
||||
wide: 'camOutlineWide',
|
||||
top: 'camOutlineTop',
|
||||
ov_topz: 0,
|
||||
ov_botz: 0,
|
||||
ov_conv: '~camConventional',
|
||||
|
|
@ -287,14 +280,10 @@ export function createPopOps() {
|
|||
step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }),
|
||||
steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }),
|
||||
inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }),
|
||||
outside: UC.newBoolean(LANG.co_olot_s, undefined, { title: LANG.co_olot_l, show: (op) => { return !op.inputs.inside.checked } }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l, xshow: (op) => { return op.inputs.outside.checked } }),
|
||||
omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, { title: LANG.co_omvd_l, xshow: (op) => { return op.inputs.outside.checked } }),
|
||||
wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l, show: (op) => { return !op.inputs.inside.checked } }),
|
||||
omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
|
||||
wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l }),
|
||||
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }),
|
||||
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
exp: UC.newExpand("overrides"),
|
||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
||||
|
|
@ -444,7 +433,6 @@ export function createPopOps() {
|
|||
refine: 'camPocketRefine',
|
||||
follow: 'camPocketFollow',
|
||||
contour: 'camPocketContour',
|
||||
engrave: 'camPocketEngrave',
|
||||
outline: 'camPocketOutline',
|
||||
ov_topz: 0,
|
||||
ov_botz: 0,
|
||||
|
|
@ -467,7 +455,6 @@ export function createPopOps() {
|
|||
follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }),
|
||||
engrave: UC.newBoolean(LANG.cp_engr_s, undefined, { title: LANG.cp_engr_l, show: () => env.poppedRec.contour }),
|
||||
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
||||
exp: UC.newExpand("overrides"),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
|
|
@ -564,12 +551,12 @@ export function createPopOps() {
|
|||
down: UC.newInput(LANG.ch_sdwn_s, {title:LANG.ch_sdwn_l, convert:toFloat, units:true}),
|
||||
startAng: UC.newInput(LANG.ch_stra_s, {title:LANG.ch_stra_l, convert:UC.toDegsFloat, bound:UC.bound(-360,360),show:() => env.poppedRec.forceStartAng}),
|
||||
offOver: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:toFloat, units:true, bound:UC.bound(0,Infinity)}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
entry: UC.newBoolean(LANG.ch_entr_s,undefined, {title:LANG.ch_entr_l}),
|
||||
entryOffset: UC.newInput(LANG.ch_ento_s, {title:LANG.ch_ento_l, convert:toFloat, units:true, show:() => env.poppedRec.entry}),
|
||||
reverse: UC.newBoolean(LANG.ch_rvrs_s,undefined, {title:LANG.ch_rvrs_l}),
|
||||
clockwise:UC.newBoolean(LANG.ch_clkw_s,undefined, {title:LANG.ch_clkw_l}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
sep: UC.newBlank({class:"pop-sep"}),
|
||||
finish: UC.newBoolean(LANG.ch_fini_s,undefined, { title:LANG.ch_fini_l ,show: ()=>!env.poppedRec.reverse}),
|
||||
forceStartAng: UC.newBoolean(LANG.ch_fsta_s, undefined, {title:LANG.ch_fsta_l }),
|
||||
fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l}),
|
||||
|
|
@ -670,6 +657,8 @@ export function createPopOps() {
|
|||
return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
|
||||
}
|
||||
|
||||
const open = true;
|
||||
|
||||
createPopOp('area', {
|
||||
spindle: 'camAreaSpindle',
|
||||
tool: 'camAreaTool',
|
||||
|
|
@ -683,14 +672,16 @@ export function createPopOps() {
|
|||
plunge: 'camAreaPlunge',
|
||||
expand: 'camAreaExpand',
|
||||
smooth: 'camAreaSmooth',
|
||||
follow: 'camAreaFollow',
|
||||
refine: 'camAreaRefine',
|
||||
outline: 'camAreaOutline',
|
||||
tolerance: 'camTolerance',
|
||||
dogbones: 'camAreaDogbones',
|
||||
revbones: 'camAreaRevbones',
|
||||
ov_topz: 0,
|
||||
ov_botz: 0,
|
||||
ov_conv: '~camConventional',
|
||||
tolerance: 'camTolerance',
|
||||
direction: 'camAreaDirection',
|
||||
}).inputs = {
|
||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||
mode: UC.newSelect(LANG.mo_menu, {}, "opmode"),
|
||||
tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"),
|
||||
sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"),
|
||||
|
|
@ -701,26 +692,29 @@ export function createPopOps() {
|
|||
], { class: "ext-buttons f-row" }),
|
||||
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
|
||||
expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }),
|
||||
follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }),
|
||||
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }),
|
||||
exp: UC.newExpand("tool & stepping", { open }),
|
||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||
direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"),
|
||||
sr_angle: UC.newInput("step angle", { title: "step angle", convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }),
|
||||
over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }),
|
||||
down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }),
|
||||
sep: UC.newBlank({ class: "pop-sep", show: isSurface }),
|
||||
refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }),
|
||||
smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, show: isSurface }),
|
||||
tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
exp: UC.newExpand("feeds & speeds"),
|
||||
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }),
|
||||
revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
|
||||
exp_end: UC.endExpand(),
|
||||
exp: UC.newExpand("feeds & speeds", { open }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }),
|
||||
rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }),
|
||||
plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
|
||||
exp_end: UC.endExpand(),
|
||||
exp: UC.newExpand("overrides"),
|
||||
exp: UC.newExpand("bounds", { open }),
|
||||
sep: UC.newBlank({ class: "pop-sep" }),
|
||||
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }),
|
||||
ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }),
|
||||
ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }),
|
||||
exp_end: UC.endExpand(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -291,7 +291,12 @@ export function opRender() {
|
|||
let clazz = notime ? ["draggable", "notime"] : ["draggable"];
|
||||
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined;
|
||||
if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` }
|
||||
if (notable?.length) label += ` (${notable[0].slice(1)})`;
|
||||
if (notable?.length) {
|
||||
rec.rename = notable[0].slice(1);
|
||||
label += ` (${rec.rename})`;
|
||||
} else {
|
||||
delete rec.rename;
|
||||
}
|
||||
html.appendAll([
|
||||
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
|
||||
`<label class="label">${label}</label>`,
|
||||
|
|
@ -526,10 +531,6 @@ export function opRender() {
|
|||
|
||||
export function init() {
|
||||
|
||||
if (api.devel.enabled) {
|
||||
$('op:area').classList.remove('hide');
|
||||
}
|
||||
|
||||
api.event.on('tool.mesh.face-normal', normal => {
|
||||
// console.log({ env.poppedRec });
|
||||
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2);
|
||||
|
|
|
|||
|
|
@ -256,6 +256,11 @@ export function cam_export(print, online) {
|
|||
return;
|
||||
}
|
||||
|
||||
// skip arc points for display only
|
||||
if (out.emit === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dx = opt.dx || newpos.x - pos.x,
|
||||
dy = opt.dy || newpos.y - pos.y,
|
||||
dz = opt.dz || newpos.z - pos.z,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { api } from '../../core/api.js';
|
||||
import { originReset, originSelect } from './cl-origin.js';
|
||||
import { updateTool } from './tools.js';
|
||||
|
||||
let LANG = api.language.current;
|
||||
let { CAM } = api.const.MODES,
|
||||
|
|
@ -32,8 +33,37 @@ function zAnchorSave() {
|
|||
export function menu() {
|
||||
let anim = ui.anim = {};
|
||||
|
||||
uc.setGroup($('tool-details'));
|
||||
|
||||
return {
|
||||
|
||||
/** Tool Editor Menu */
|
||||
|
||||
_____: uc.setGroup($('tool-details')),
|
||||
toolName: newInput('name', { title:'tool name', id: 'tool-name', size: 0, text: true }),
|
||||
toolType: newSelect('type', { title: 'tool type', id: 'tool-type', action:updateTool }, "camtool"),
|
||||
toolNum: newInput('tool #', { title:'tool number', id: 'tool-num', convert: toInt }),
|
||||
toolMetric: newBoolean('metric', updateTool, { title: 'metric', id: 'tool-metric' }),
|
||||
_____: newGroup(LANG.td_shft),
|
||||
toolShaftDiam: newInput('diameter', { convert: toFloat }),
|
||||
toolShaftLen: newInput('length', { convert: toFloat }),
|
||||
_____: newGroup(LANG.td_flut),
|
||||
toolFluteDiam: newInput('diameter', { convert: toFloat }),
|
||||
toolFluteLen: newInput('length', { convert: toFloat }),
|
||||
_____: newGroup(LANG.td_tapr),
|
||||
toolTaperAngle: newInput('angle', { convert: toFloat }),
|
||||
toolTaperTip: newInput('tip', { convert: toFloat }),
|
||||
// toolTaperAngle: $('tool-tangle'),
|
||||
|
||||
toolsSave: $('tools-save'),
|
||||
toolsClose: $('tools-close'),
|
||||
toolsImport: $('tools-import'),
|
||||
toolsExport: $('tools-export'),
|
||||
toolSelect: $('tool-select'),
|
||||
toolAdd: $('tool-add'),
|
||||
toolCopy: $('tool-dup'),
|
||||
toolDelete: $('tool-del'),
|
||||
|
||||
/** Animation Bar */
|
||||
|
||||
_____: {
|
||||
|
|
@ -92,8 +122,9 @@ export function menu() {
|
|||
camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}),
|
||||
camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }),
|
||||
camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }),
|
||||
camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}),
|
||||
separator: newBlank({ class:"set-sep", driven }),
|
||||
camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}),
|
||||
camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}),
|
||||
_____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }),
|
||||
camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}),
|
||||
camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}),
|
||||
|
|
@ -119,12 +150,10 @@ export function menu() {
|
|||
newButton("reset", originReset),
|
||||
], { class: "ext-buttons f-row" }),
|
||||
_____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }),
|
||||
camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }),
|
||||
camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }),
|
||||
separator: newBlank({ class:"set-sep", driven }),
|
||||
camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, {title:LANG.cx_arce_l}),
|
||||
camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }),
|
||||
camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}),
|
||||
camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.camArcEnabled.checked}),
|
||||
camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, { title:LANG.cx_fast_l }),
|
||||
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
// todo: surface offset pattern
|
||||
// todo: trace dogbones, merge overlap
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
|
|
@ -22,16 +25,19 @@ class OpArea extends CamOp {
|
|||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { tool, mode, down, over, follow, expand, outline, refine, smooth } = op;
|
||||
let { ov_topz, ov_botz, ov_conv } = op;
|
||||
let { tool, mode, down, over, follow, expand, outline, smooth } = op;
|
||||
let { ov_topz, ov_botz, direction, rename } = op;
|
||||
let { settings, widget, tabs, color } = state;
|
||||
let { addSlices, setToolDiam, cutTabs, healPolys, shadowAt, workarea } = state;
|
||||
|
||||
let areaTool = new Tool(settings, tool);
|
||||
let smoothVal = (smooth ?? 0) / 10;
|
||||
let toolDiam = areaTool.fluteDiameter();
|
||||
let toolOver = areaTool.hasTaper() ? over : toolDiam * over;
|
||||
let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_stock;
|
||||
let zBottom = ov_botz ? workarea.bottom_stock + ov_botz : workarea.bottom_part;
|
||||
let toolOver = areaTool.getStepSize(over);
|
||||
let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_z;
|
||||
let zBottom = ov_botz ? workarea.bottom_stock + ov_botz : Math.max(workarea.bottom_z, workarea.bottom_part);
|
||||
let shadowBase = state.shadow.base;
|
||||
let thruHoles = state.shadow.holes;
|
||||
|
||||
// also updates tab offsets
|
||||
setToolDiam(toolDiam);
|
||||
|
|
@ -62,7 +68,7 @@ class OpArea extends CamOp {
|
|||
|
||||
// connect open poly edge segments into closed loops (when possible)
|
||||
// surface and edge selections produce open polygons by default
|
||||
polys = POLY.nest(healPolys(polys));
|
||||
polys = POLY.nest(healPolys(polys, false));
|
||||
|
||||
// gather surface selections
|
||||
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
||||
|
|
@ -83,7 +89,9 @@ class OpArea extends CamOp {
|
|||
// add in unioned surface areas
|
||||
polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), fminz));
|
||||
|
||||
// todo: implement `refine` and `smooth`
|
||||
// smoothing for jaggies usually caused by vertical walls
|
||||
if (smoothVal)
|
||||
polys = polys.map(poly => POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal)).flat();
|
||||
|
||||
// expand selections (flattens z variable polys)
|
||||
if (Math.abs(expand) > 0) {
|
||||
|
|
@ -95,7 +103,9 @@ class OpArea extends CamOp {
|
|||
nupolys.push(...expanded.flat());
|
||||
}
|
||||
}
|
||||
polys = nupolys;
|
||||
// polys = nupolys;
|
||||
// re-merge after expansion in case it produces overlap
|
||||
polys = POLY.union(nupolys, 0.00001, true);
|
||||
}
|
||||
|
||||
// process each area separately
|
||||
|
|
@ -104,17 +114,17 @@ class OpArea extends CamOp {
|
|||
for (let area of polys) {
|
||||
let bounds = area.getBounds3D();
|
||||
|
||||
if (devel) newLayer().output()
|
||||
.setLayer("area", { line: 0xff8800 }, false)
|
||||
.addPolys([ area ]);
|
||||
|
||||
newArea();
|
||||
|
||||
if (outline) {
|
||||
// remove inner voids when processing outline only
|
||||
area.inner = undefined;
|
||||
}
|
||||
|
||||
newLayer().output()
|
||||
.setLayer("area", { line: 0xff8800 }, false)
|
||||
.addPolys([ area ]);
|
||||
|
||||
newArea();
|
||||
|
||||
if (mode === 'clear') {
|
||||
let zs = down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ];
|
||||
let zroc = 0;
|
||||
|
|
@ -126,7 +136,22 @@ class OpArea extends CamOp {
|
|||
let layers = slice.output();
|
||||
let outs = [];
|
||||
let clip = [];
|
||||
let shadow = shadowAt(z);
|
||||
let shadow = await shadowAt(z);
|
||||
let tool_shadow = POLY.offset(shadow, [ toolDiam / 2 - 0.01 ], { count: 1, z });
|
||||
// for roughing backward compatability
|
||||
if (op.omitthru) {
|
||||
shadow = shadow.clone(true);
|
||||
for (let poly of shadow.filter(p => p.inner)) {
|
||||
poly.inner = poly.inner.filter(inner => {
|
||||
for (let ho of thruHoles) {
|
||||
if (inner.isEquivalent(ho)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
POLY.subtract([ area ], shadow, clip, undefined, undefined, 0);
|
||||
POLY.offset(clip, [ -toolDiam / 2, -toolOver ], {
|
||||
count: 999, outs, flat: true, z, minArea: 0
|
||||
|
|
@ -142,16 +167,30 @@ class OpArea extends CamOp {
|
|||
break outer;
|
||||
}
|
||||
// cut tabs when present
|
||||
if (tabs) outs = cutTabs(tabs, outs);
|
||||
if (tabs.length) outs = cutTabs(tabs, outs);
|
||||
// for roughing backward compatability
|
||||
if (op.leave_z) {
|
||||
for (let out of outs)
|
||||
for (let p of out)
|
||||
p.z += op.leave_z;
|
||||
}
|
||||
if (op.leave_xy) {
|
||||
outs = outs.map(poly => poly.offset(-op.leave_xy)).flat();
|
||||
}
|
||||
slice.tool_shadow = tool_shadow;
|
||||
slice.camLines = outs;
|
||||
zroc += zinc;
|
||||
lzo = z;
|
||||
progress(proc + (pinc * zroc), 'clear');
|
||||
if (devel) layers
|
||||
.setLayer("shadow", { line: 0x0088ff }, false)
|
||||
.addPolys(shadow);
|
||||
.setLayer("base", { line: 0xff0000 }, false)
|
||||
.addPolys(shadowBase)
|
||||
.setLayer("shadow", { line: 0x00ff00 }, false)
|
||||
.addPolys(shadow)
|
||||
.setLayer("tool shadow", { line: 0x44ff88 }, false)
|
||||
.addPolys(tool_shadow);
|
||||
layers
|
||||
.setLayer("clear", { line: 0x88ff00 }, false)
|
||||
.setLayer(rename ?? "clear", { line: 0x88ff00 }, false)
|
||||
.addPolys(outs);
|
||||
// of the last output still cuts, we need an escape
|
||||
if (z === zs.peek()) {
|
||||
|
|
@ -166,15 +205,16 @@ class OpArea extends CamOp {
|
|||
let zs = down ? base_util.lerp(zTop, bounds.min.z, down) : [ bounds.min.z ];
|
||||
let zroc = 0;
|
||||
let zinc = 1 / zs.length;
|
||||
let lzo;
|
||||
for (let z of zs) {
|
||||
let slice = newLayer();
|
||||
let layers = slice.output();
|
||||
let outs = [];
|
||||
if (tr_type === 'none') {
|
||||
// todo: move this out of the zs loop and only setZ when needed
|
||||
area = area.clone(true);
|
||||
outs = [ zs.length > 1 ? area.setZ(z) : area ];
|
||||
} else {
|
||||
// todo: move this out of the zs loop
|
||||
POLY.offset([ area ], tr_type === 'inside' ? [ -toolDiam / 2 ] : [ toolDiam / 2 ], {
|
||||
count: 1, outs, flat: true, z, minArea: 0
|
||||
});
|
||||
|
|
@ -183,14 +223,16 @@ class OpArea extends CamOp {
|
|||
// terminate z descent when no further output possible
|
||||
break;
|
||||
}
|
||||
// add dogbones when specified
|
||||
if (op.dogbones) outs.forEach(out => out.addDogbones(toolDiam / 5, op.revbones));
|
||||
// cut tabs when present
|
||||
if (tabs) outs = cutTabs(tabs, outs);
|
||||
slice.camLines = outs;
|
||||
slice.tool_shadow = POLY.offset(await shadowAt(z), [ toolDiam / 2 - 0.01 ], { count: 1, z });
|
||||
zroc += zinc;
|
||||
lzo = z;
|
||||
progress(proc + (pinc * zroc), 'trace');
|
||||
layers
|
||||
.setLayer("trace", { line: 0x88ff00 }, false)
|
||||
.setLayer(rename ?? "trace", { line: 0x88ff00 }, false)
|
||||
.addPolys(outs);
|
||||
}
|
||||
proc += pinc;
|
||||
|
|
@ -226,8 +268,12 @@ class OpArea extends CamOp {
|
|||
paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32());
|
||||
} else
|
||||
if (sr_type === 'offset') {
|
||||
// todo: progressive inset from perimeter
|
||||
console.log({ sr_offset: toolOver });
|
||||
// progressive inset from perimeter
|
||||
POLY.offset([ area ], [ -toolDiam / 2, -toolOver ], {
|
||||
count: 999, outs: paths, flat: true, z: 0, minArea: 0
|
||||
});
|
||||
paths.forEach(poly => poly.isClosed() && poly.push(poly.first()));
|
||||
paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32());
|
||||
}
|
||||
|
||||
// prepare tool mesh points
|
||||
|
|
@ -265,9 +311,10 @@ class OpArea extends CamOp {
|
|||
// convert terrain raster output back to open polylines
|
||||
for (let path of output.paths) {
|
||||
path = newPolygon().fromArray([1, ...path]);
|
||||
if (op.refine) path.refine(op.refine);
|
||||
surface.push(path);
|
||||
newLayer().output()
|
||||
.setLayer("linear", { line: 0x00ff00 }, false)
|
||||
.setLayer(rename ?? "linear", { line: 0x00ff00 }, false)
|
||||
.addPolys([ path ]);
|
||||
}
|
||||
|
||||
|
|
@ -281,29 +328,28 @@ class OpArea extends CamOp {
|
|||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, areas, surfaces } = this;
|
||||
let { getPrintPoint, newLayer, pocket, polyEmit, setTool, setSpindle, tip2tipEmit } = ops;
|
||||
let { newLayer, pocket, polyEmit, printPoint, tip2tipEmit } = ops;
|
||||
let { setContouring, setNextIsMove } = ops;
|
||||
let { process } = state.settings;
|
||||
|
||||
setTool(op.tool, op.rate);
|
||||
setSpindle(op.spindle);
|
||||
|
||||
let printPoint = getPrintPoint();
|
||||
|
||||
// process surface paths
|
||||
for (let surface of surfaces) {
|
||||
let array = surface.map(poly => { return {
|
||||
el: poly,
|
||||
first: poly.first(),
|
||||
last: poly.last()
|
||||
} });
|
||||
tip2tipEmit(array, printPoint, (next, first, count) => {
|
||||
printPoint = polyEmit(next.el, 0, 1, printPoint, {});
|
||||
newLayer();
|
||||
});
|
||||
}
|
||||
|
||||
// skip areas when processing surfaces
|
||||
if (surfaces.length) {
|
||||
setContouring(true);
|
||||
for (let surface of surfaces) {
|
||||
let array = surface.map(poly => { return {
|
||||
el: poly,
|
||||
first: poly.first(),
|
||||
last: poly.last()
|
||||
} });
|
||||
tip2tipEmit(array, printPoint, (next, point) => {
|
||||
setNextIsMove();
|
||||
if (next.last === point) next.el.reverse();
|
||||
printPoint = polyEmit(next.el);
|
||||
newLayer();
|
||||
});
|
||||
}
|
||||
setContouring(false);
|
||||
// skip areas when processing surfaces
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -313,7 +359,7 @@ class OpArea extends CamOp {
|
|||
dist: Infinity,
|
||||
area: undefined
|
||||
};
|
||||
for (let area of areas.filter(p => !p.used)) {
|
||||
for (let area of areas.filter(p => p.length && !p.used)) {
|
||||
// skip devel / debug only areas
|
||||
let topPolys = area[0].camLines;
|
||||
if (!topPolys) continue;
|
||||
|
|
|
|||
|
|
@ -83,10 +83,13 @@ class OpContour extends CamOp {
|
|||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { color, addSlices, settings, updateToolDiams } = state;
|
||||
let conTool = new Tool(settings, op.tool);
|
||||
let filter = createFilter(op, settings.origin, op.axis.toLowerCase());
|
||||
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
|
||||
let toolDiam = this.toolDiam = conTool.fluteDiameter();
|
||||
this.toolStep = conTool.getStepSize(op.step);
|
||||
|
||||
updateToolDiams(toolDiam);
|
||||
|
||||
// we need topo for safe travel moves when roughing and outlining
|
||||
// not generated when drilling-only. then all z moves use bounds max.
|
||||
// also generates x and y contouring when selected
|
||||
|
|
@ -114,21 +117,19 @@ class OpContour extends CamOp {
|
|||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, sliceOut } = this;
|
||||
let { op, state, sliceOut, toolStep } = this;
|
||||
let { settings } = state;
|
||||
let { process } = settings;
|
||||
|
||||
let { setTolerance, setTool, setSpindle } = ops;
|
||||
let { polyEmit, setContouring, setTolerance, setTool } = ops;
|
||||
let { widget, camOut, newLayer, zmax } = ops;
|
||||
|
||||
let bounds = widget.getBoundingBox();
|
||||
let toolDiam = this.toolDiam;
|
||||
let stepover = toolDiam * op.step * 2;
|
||||
let depthFirst = process.camDepthFirst;
|
||||
let depthData = [];
|
||||
|
||||
setTool(op.tool, op.rate, process.camFastFeedZ);
|
||||
setSpindle(op.spindle);
|
||||
setContouring(true, toolStep);
|
||||
setTolerance(this.tolerance);
|
||||
|
||||
let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax);
|
||||
|
|
@ -139,38 +140,21 @@ class OpContour extends CamOp {
|
|||
continue;
|
||||
}
|
||||
let polys = [], poly;
|
||||
slice.camLines.forEach(function (poly) {
|
||||
if (depthFirst) poly = poly.clone(true).annotate({ slice: slice.index + 1 });
|
||||
slice.camLines.forEach((poly) => {
|
||||
poly = poly.clone(true).annotate({ slice: slice.index + 1 });
|
||||
polys.push({ first: poly.first(), last: poly.last(), poly: poly });
|
||||
});
|
||||
if (depthFirst) {
|
||||
depthData.appendAll(polys);
|
||||
} else {
|
||||
printPoint = tip2tipEmit(polys, printPoint, function (el, point, count) {
|
||||
poly = el.poly;
|
||||
if (poly.last() === point) {
|
||||
poly.reverse();
|
||||
}
|
||||
poly.forEachPoint(function (point, pidx) {
|
||||
camOut(point.clone(), pidx > 0, { moveLen: stepover });
|
||||
}, false);
|
||||
});
|
||||
newLayer();
|
||||
}
|
||||
depthData.appendAll(polys);
|
||||
}
|
||||
|
||||
if (depthFirst) {
|
||||
tip2tipEmit(depthData, printPoint, function (el, point, count) {
|
||||
let poly = el.poly;
|
||||
if (poly.last() === point) {
|
||||
poly.reverse();
|
||||
}
|
||||
poly.forEachPoint(function (point, pidx) {
|
||||
camOut(printPoint = point.clone().annotate({ slice: poly.slice }), pidx > 0, { moveLen: stepover });
|
||||
}, false);
|
||||
newLayer();
|
||||
});
|
||||
}
|
||||
tip2tipEmit(depthData, printPoint, (el, point) => {
|
||||
let poly = el.poly;
|
||||
if (poly.last() === point) {
|
||||
poly.reverse();
|
||||
}
|
||||
polyEmit(poly);
|
||||
newLayer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ class OpDrill extends CamOp {
|
|||
drillToolDiam = drillTool.fluteDiameter(),
|
||||
sliceOut = this.sliceOut = [];
|
||||
|
||||
const allDrills = drills[widget.id] ?? []
|
||||
if (allDrills.length === 0) return;
|
||||
|
||||
updateToolDiams(drillToolDiam);
|
||||
|
||||
const allDrills = drills[widget.id] ?? []
|
||||
// drill points to use center (average of all points) of the polygon
|
||||
allDrills.forEach((drill) => {
|
||||
if (!drill.selected) {
|
||||
|
|
@ -61,9 +63,10 @@ class OpDrill extends CamOp {
|
|||
let { op, sliceOut } = this;
|
||||
let { setTool, setSpindle, setDrill, emitDrills } = ops;
|
||||
|
||||
if (sliceOut.length === 0) return;
|
||||
|
||||
setTool(op.tool, undefined, op.rate);
|
||||
setDrill(op.down, op.lift, op.dwell);
|
||||
setSpindle(op.spindle);
|
||||
emitDrills(sliceOut.map(slice => slice.camLines).flat());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,11 +240,10 @@ export class OpHelical extends CamOp {
|
|||
}
|
||||
|
||||
async prepare(ops, progress) {
|
||||
let { polyEmit, printPoint, setTool, setSpindle } = ops;
|
||||
let { polyEmit, printPoint, setTool } = ops;
|
||||
let { tool, spindle, rate, feed } = this.op;
|
||||
|
||||
setTool(tool, feed, rate);
|
||||
setSpindle(spindle);
|
||||
|
||||
let [ polys ] = this.sliceOut.map((s) => s.camLines);
|
||||
polys = polys.slice();
|
||||
|
|
@ -268,7 +267,7 @@ export class OpHelical extends CamOp {
|
|||
if (!closest) break;
|
||||
poly = polys[closestI]
|
||||
polys[closestI] = null;
|
||||
printPoint = polyEmit(poly, 0, 1, poly.points[0])
|
||||
printPoint = polyEmit(poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ export class OpIndex extends CamOp {
|
|||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
const { zmax, zsafe, camOut } = ops;
|
||||
let { zmax, zsafe, camOut, printPoint } = ops;
|
||||
// max point of stock corner radius when rotating (safe z when indexing)
|
||||
const ztop = Math.max(zsafe, zmax);
|
||||
let ztop = Math.max(zsafe, zmax);
|
||||
// move above rotating stock
|
||||
camOut(last = last.clone().setZ(ztop), 0);
|
||||
camOut(printPoint = printPoint.clone().setZ(ztop), 0);
|
||||
// issue rotation command
|
||||
camOut(last = last.clone().setZ(ztop).setA(this.degrees), 0);
|
||||
camOut(printPoint = printPoint.clone().setZ(ztop).setA(this.degrees), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,22 +54,13 @@ class OpLathe extends CamOp {
|
|||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, slices, topo } = this;
|
||||
let { settings } = state;
|
||||
let { op, slices, topo } = this;
|
||||
let { camOut, newLayer, zSafe } = ops;
|
||||
|
||||
let { setTool, setSpindle } = ops;
|
||||
let { camOut, newLayer, printPoint } = ops;
|
||||
let { zmax } = ops;
|
||||
|
||||
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
|
||||
let stepover = toolDiam * op.step * 2;
|
||||
let rez = topo.resolution;
|
||||
|
||||
setTool(op.tool, op.rate, op.plunge);
|
||||
setSpindle(op.spindle);
|
||||
|
||||
// start top center, X = 0, Y = 0 closest to 4th axis chuck
|
||||
printPoint = newPoint(0, 0, zmax);
|
||||
camOut(newPoint(0, 0, zSafe), 0);
|
||||
|
||||
for (let slice of slices) {
|
||||
// ignore debug slices
|
||||
|
|
@ -90,14 +81,14 @@ class OpLathe extends CamOp {
|
|||
return;
|
||||
}
|
||||
if (latent) {
|
||||
camOut(latent, true, stepover);
|
||||
camOut(latent, 1);
|
||||
latent = undefined;
|
||||
}
|
||||
}
|
||||
camOut(last = point.clone(), pidx > 0, stepover);
|
||||
camOut(last = point.clone(), pidx > 0 ? 1 : 0);
|
||||
}, false);
|
||||
if (latent) {
|
||||
camOut(latent, true, stepover);
|
||||
camOut(latent, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +101,7 @@ class OpLathe extends CamOp {
|
|||
// camOut(last = last.clone().setZ(zmax), 0);
|
||||
// camOut(last = last.clone().setA(amax), 0);
|
||||
newLayer();
|
||||
ops.addGCode([`G0 Z${zmax.round(2)}`, `G0 A${amax}`, "G92 A0"]);
|
||||
ops.addGCode([`G0 Z${zSafe.round(2)}`, `G0 A${amax}`, "G92 A0"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,22 +14,23 @@ class OpLevel extends CamOp {
|
|||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { addSlices, color, settings, tshadow, updateToolDiams, zMax, ztOff } = state;
|
||||
let { addSlices, color, settings, shadow, updateToolDiams, zMax, ztOff } = state;
|
||||
let { down, tool, step, stepz, inset } = op;
|
||||
let { stock } = settings;
|
||||
|
||||
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
|
||||
let stepOver = this.stepOver = toolDiam * op.step;
|
||||
let toolDiam = new Tool(settings, tool).fluteDiameter();
|
||||
let stepOver = this.stepOver = toolDiam * step;
|
||||
let wpos = state.widget.track.pos;
|
||||
let zTop = zMax + ztOff;
|
||||
let zBot = zTop - op.down;
|
||||
let zList = op.stepz ? util.lerp(zTop, zBot, op.stepz) : [ zBot ];
|
||||
let zBot = zTop - down;
|
||||
let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
|
||||
|
||||
updateToolDiams(toolDiam);
|
||||
|
||||
let points = [];
|
||||
let clear = op.stock ?
|
||||
[ newPolygon().centerRectangle({x:-wpos.x,y:-wpos.y,z:wpos.z}, stock.x + toolDiam/2, stock.y) ] :
|
||||
POLY.outer(POLY.offset(tshadow, toolDiam * (op.inset || 0)));
|
||||
POLY.outer(POLY.offset(shadow.base, toolDiam * (inset || 0)));
|
||||
|
||||
POLY.fillArea(clear, 1090, stepOver, points);
|
||||
|
||||
|
|
@ -49,12 +50,10 @@ class OpLevel extends CamOp {
|
|||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, layers, stepOver } = this;
|
||||
let { setTool, setSpindle, printPoint } = ops;
|
||||
let { layers, stepOver } = this;
|
||||
let { printPoint } = ops;
|
||||
let { newLayer, tip2tipEmit, camOut } = ops;
|
||||
|
||||
setTool(op.tool, op.rate);
|
||||
setSpindle(op.spindle);
|
||||
for (let lines of layers) {
|
||||
lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } });
|
||||
printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => {
|
||||
|
|
|
|||
|
|
@ -1,311 +1,46 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { OpArea } from './op-area.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { util as base_util } from '../../../geo/base.js';
|
||||
import { poly2polyEmit } from '../../../geo/paths.js';
|
||||
import { newPoint } from '../../../geo/point.js';
|
||||
import { addDogbones } from './slice.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
|
||||
class OpOutline extends CamOp {
|
||||
constructor(state, op) {
|
||||
super(state, op);
|
||||
}
|
||||
|
||||
// todo: wide cutout, dogbones
|
||||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { settings, slicer, addSlices, tshadow, thruHoles, unsafe, color, widget } = state;
|
||||
let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax, shadowAt } = state;
|
||||
let { process, stock } = settings;
|
||||
let { controller } = settings;
|
||||
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
|
||||
|
||||
if (op.down <= 0) {
|
||||
throw `invalid step down "${op.down}"`;
|
||||
}
|
||||
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter();
|
||||
updateToolDiams(toolDiam);
|
||||
|
||||
let shadow = [];
|
||||
let slices = [];
|
||||
let intopt = {
|
||||
off: 0.01,
|
||||
fit: true,
|
||||
down: true,
|
||||
min: Math.max(0, workarea.bottom_z),
|
||||
max: workarea.top_z
|
||||
let { shadow, tool, widget } = state;
|
||||
let areas = POLY.flatten(POLY.expand(shadow.base, tool.fluteDiameter() / 2 - 0.001));
|
||||
let cutout = {
|
||||
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
||||
dogbones: op.dogbones,
|
||||
down: op.down,
|
||||
expand: 0,
|
||||
mode: 'trace',
|
||||
outline: op.omitthru,
|
||||
ov_botz: op.ov_botz,
|
||||
ov_topz: op.ov_topz,
|
||||
plunge: op.plunge,
|
||||
rate: op.rate,
|
||||
rename: op.rename ?? "outline",
|
||||
revbones: op.revbones,
|
||||
smooth: 0,
|
||||
spindle: op.spindle,
|
||||
surfaces: {},
|
||||
tool: op.tool,
|
||||
tr_type: 'none',
|
||||
};
|
||||
let indices = slicer.interval(op.down, intopt);
|
||||
let trueShadow = process.camTrueShadow === true;
|
||||
let lastShadowZ;
|
||||
let stockClip;
|
||||
|
||||
// shift out first (top-most) slice
|
||||
indices.shift();
|
||||
// add flats to shadow
|
||||
const flats = Object.keys(slicer.zFlat)
|
||||
.map(v => (parseFloat(v) - 0.01).round(5))
|
||||
.filter(v => v > 0 && indices.indexOf(v) < 0);
|
||||
indices = indices.appendAll(flats).sort((a,b) => b-a);
|
||||
let cnt = 0;
|
||||
let tot = 0;
|
||||
if (op.outside && !op.inside) {
|
||||
// console.log({outline_bypass: indices, down: op.down});
|
||||
indices.forEach((ind,i) => {
|
||||
if (flats.indexOf(ind) >= 0) {
|
||||
// exclude flats
|
||||
return;
|
||||
}
|
||||
let slice = newSlice(ind);
|
||||
slice.shadow = shadow.clone(true);
|
||||
slices.push(slice);
|
||||
});
|
||||
} else
|
||||
await slicer.slice(indices, { each: data => {
|
||||
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
|
||||
if (flats.indexOf(data.z) >= 0) {
|
||||
// exclude flats injected to complete shadow
|
||||
return;
|
||||
}
|
||||
data.shadow = trueShadow ? shadowAt(data.z, lastShadowZ) : shadow.clone(true);
|
||||
data.slice.shadow = data.shadow;
|
||||
// data.slice.tops[0].inner = data.shadow;
|
||||
// data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z);
|
||||
slices.push(data.slice);
|
||||
// data.slice.xray();
|
||||
// onupdate(0.2 + (index/total) * 0.1, "outlines");
|
||||
progress(0.5 + 0.5 * (++cnt / tot));
|
||||
lastShadowZ = data.z;
|
||||
}, progress: (index, total) => {
|
||||
tot = total;
|
||||
progress((index / total) * 0.5);
|
||||
} });
|
||||
shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true);
|
||||
|
||||
// start slices at top of stock when `clear top` enabled
|
||||
if (op.top) {
|
||||
let first = slices[0];
|
||||
let zlist = slices.map(s => s.z);
|
||||
for (let z of indices.filter(v => v >= zMax)) {
|
||||
if (zlist.contains(z)) {
|
||||
continue;
|
||||
}
|
||||
let add = first.clone(true);
|
||||
add.tops.forEach(top => top.poly.setZ(add.z));
|
||||
add.shadow = first.shadow.clone(true);
|
||||
add.z = z;
|
||||
slices.splice(0,0,add);
|
||||
}
|
||||
}
|
||||
|
||||
// extend cut thru (only when z bottom is 0)
|
||||
if (workarea.bottom_z < 0) {
|
||||
let last = slices[slices.length-1];
|
||||
// when step down > full cut depth, clear slices and
|
||||
// leave only the cut-thru pass(es)
|
||||
if (op.down > workarea.top_stock - workarea.bottom_part) {
|
||||
slices.length = 0;
|
||||
}
|
||||
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
|
||||
if (!last) continue;
|
||||
let add = last.clone(true);
|
||||
add.tops.forEach(top => top.poly.setZ(add.z));
|
||||
add.shadow = last.shadow.clone(true);
|
||||
add.z -= zneg;
|
||||
slices.push(add);
|
||||
}
|
||||
}
|
||||
|
||||
slices.forEach(slice => {
|
||||
let tops = slice.shadow;
|
||||
|
||||
// outside only (use tshadow for entire cut)
|
||||
if (op.outside) {
|
||||
tops = tshadow;
|
||||
}
|
||||
|
||||
if (op.omitthru) {
|
||||
// eliminate thru holes from shadow
|
||||
for (let hole of thruHoles) {
|
||||
for (let top of tops) {
|
||||
if (!top.inner) continue;
|
||||
top.inner = top.inner.filter(innr => {
|
||||
return !innr.isEquivalent(hole, false, 0.1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (op.omitvoid) {
|
||||
for (let top of tops) {
|
||||
delete top.inner;
|
||||
}
|
||||
}
|
||||
|
||||
let offset = POLY.expand(tops, toolDiam / 2, slice.z);
|
||||
if (!(offset && offset.length)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// when pocket only, drop first outer poly
|
||||
// if it matches the shell and promote inner polys
|
||||
if (op.inside) {
|
||||
let shell = POLY.expand(tops.clone(), toolDiam / 2);
|
||||
offset = POLY.filter(offset, [], function(poly) {
|
||||
if (poly.area() < 1) {
|
||||
return null;
|
||||
}
|
||||
for (let sp=0; sp<shell.length; sp++) {
|
||||
// eliminate shell only polys
|
||||
if (poly.isEquivalent(shell[sp])) {
|
||||
if (poly.inner) return poly.inner;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return poly;
|
||||
});
|
||||
} else {
|
||||
if (op.wide) {
|
||||
let stepover = toolDiam * op.step;
|
||||
let wideCuts = [] //accumulator for wide cuts
|
||||
for (let c = (op.steps || 1); c > 0; c--){
|
||||
let wideCut = POLY.expand(offset.clone(true), stepover * c, slice.z, [], 1);
|
||||
wideCut.forEach(cut =>{ //set order of cuts when wide
|
||||
cut.order = c
|
||||
if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c })
|
||||
});
|
||||
wideCuts.push(...wideCut)
|
||||
}
|
||||
offset.appendAll(wideCuts);
|
||||
}
|
||||
}
|
||||
|
||||
if (op.dogbones && !op.wide) {
|
||||
addDogbones(offset, toolDiam / 5);
|
||||
}
|
||||
|
||||
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
|
||||
let { center } = stock;
|
||||
let x = center.x - center_off.x;
|
||||
let y = center.y - center_off.y;
|
||||
stockClip = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
|
||||
offset = cutPolys([stockClip], offset, slice.z, true);
|
||||
}
|
||||
|
||||
if (tabs) {
|
||||
tabs.forEach(tab => {
|
||||
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
|
||||
});
|
||||
offset = cutTabs(tabs, offset);
|
||||
}
|
||||
|
||||
// offset.xout(`slice ${slice.z}`);
|
||||
slice.camLines = offset;
|
||||
});
|
||||
|
||||
// when top expand fails above, it creates an empty slice
|
||||
slices = slices.filter(s => s.camLines);
|
||||
|
||||
// project empty up and render
|
||||
for (let slice of slices) {
|
||||
if (controller.devel) slice.output()
|
||||
.setLayer("slice", {line: 0xaaaa00}, false)
|
||||
.addPolys(slice.topPolys())
|
||||
if (controller.devel) slice.output()
|
||||
.setLayer("shadow", {line: 0x557799}, false)
|
||||
.addPolys(slice.shadow)
|
||||
if (controller.devel && stockClip) slice.output()
|
||||
.setLayer("stock clip", {line: 0x557799}, false)
|
||||
.addPolys([ stockClip ])
|
||||
slice.output()
|
||||
.setLayer(state.layername, {face: color, line: color})
|
||||
.addPolys(slice.camLines);
|
||||
}
|
||||
|
||||
addSlices(slices);
|
||||
this.sliceOut = slices;
|
||||
this.op_cutout = new OpArea(state, cutout);
|
||||
return this.op_cutout.slice(progress);
|
||||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, sliceOut } = this;
|
||||
let { settings } = state;
|
||||
let { process } = settings;
|
||||
let { depthOutlinePath, newLayer, polyEmit, printPoint, setTool, setSpindle } = ops;
|
||||
|
||||
let cutdir = op.ov_conv;
|
||||
let depthFirst = process.camDepthFirst;
|
||||
let depthData = [];
|
||||
let easeDown = process.camEaseDown;
|
||||
let toolDiam = this.toolDiam;
|
||||
|
||||
setTool(op.tool, op.rate, op.plunge);
|
||||
setSpindle(op.spindle);
|
||||
|
||||
// printPoint becomes NaN in engine mode
|
||||
if (Object.values(printPoint).some(v => Number.isNaN(v))) {
|
||||
printPoint = newPoint(0, 0, 0);
|
||||
}
|
||||
|
||||
for (let slice of sliceOut) {
|
||||
let polys = [], t = [], c = [];
|
||||
let lines =POLY.flatten(slice.camLines)
|
||||
// console.log(lines);
|
||||
lines.forEach((poly)=> {
|
||||
poly.order = poly.order ?? 0;
|
||||
let child = poly.parent;
|
||||
if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 }
|
||||
if (child) c.push(poly); else t.push(poly);
|
||||
poly.layer = depthData.layer;
|
||||
polys.push(poly);
|
||||
});
|
||||
|
||||
// set cut direction on outer polys
|
||||
POLY.setWinding(t, !cutdir);
|
||||
// set cut direction on inner polys
|
||||
POLY.setWinding(c, cutdir);
|
||||
|
||||
if (depthFirst) {
|
||||
depthData.push(polys);
|
||||
} else {
|
||||
let orderSplit = {}
|
||||
polys.forEach(poly => {
|
||||
if(poly.order in orderSplit) orderSplit[poly.order].push(poly);
|
||||
else orderSplit[poly.order] = [poly];
|
||||
})
|
||||
Object.entries(orderSplit) //split the polys by order
|
||||
.sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first)
|
||||
.forEach(([order, orderPolys]) => { // emit based on closest for each order
|
||||
let polyLast;
|
||||
// console.log({order, orderPolys});
|
||||
printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) {
|
||||
polyLast = polyEmit(poly, index, count, polyLast);
|
||||
}, {
|
||||
swapdir: false,
|
||||
weight: process.camInnerFirst
|
||||
});
|
||||
})
|
||||
newLayer();
|
||||
}
|
||||
}
|
||||
|
||||
if (depthFirst) {
|
||||
let flatLevels = depthData.map(level => {
|
||||
return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0));
|
||||
}).filter(l => l.length > 0);
|
||||
if (flatLevels.length && flatLevels[0].length) {
|
||||
// start with the smallest polygon on the top
|
||||
printPoint = flatLevels[0]
|
||||
.sort((a,b) => { return a.area() - b.area() })[0]
|
||||
.average();
|
||||
// experimental start of ease down
|
||||
let ease = op.down && easeDown ? 0.001 : 0;
|
||||
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease);
|
||||
printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease);
|
||||
}
|
||||
}
|
||||
async prepare(ops, progress) {
|
||||
return this.op_cutout.prepare(ops, progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { generate as Topo } from './topo3.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { util as base_util } from '../../../geo/base.js';
|
||||
import { calc_normal, calc_vertex } from '../../../geo/paths.js';
|
||||
import { CAM } from './driver-be.js';
|
||||
|
||||
const DEG2RAG = Math.PI / 180;
|
||||
import { OpArea } from './op-area.js';
|
||||
|
||||
class OpPocket extends CamOp {
|
||||
constructor(state, op) {
|
||||
|
|
@ -18,253 +9,37 @@ class OpPocket extends CamOp {
|
|||
}
|
||||
|
||||
async slice(progress) {
|
||||
const pocket = this;
|
||||
let { op, state } = this;
|
||||
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op;
|
||||
let { ov_botz, ov_conv } = op;
|
||||
let { settings, widget, addSlices, zBottom, tabs, color } = state;
|
||||
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state;
|
||||
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom;
|
||||
// generate tracing offsets from chosen features
|
||||
let sliceOut;
|
||||
let pockets = this.pockets = [];
|
||||
let camTool = new Tool(settings, tool);
|
||||
let toolDiam = camTool.fluteDiameter();
|
||||
let toolOver = toolDiam * op.step;
|
||||
let cutdir = ov_conv;
|
||||
let engrave = contour && op.engrave;
|
||||
let zTop = workarea.top_z;
|
||||
let devel = settings.controller.devel;
|
||||
let smoothVal = (smooth ?? 0) / 10;
|
||||
if (contour) {
|
||||
down = 0;
|
||||
this.contour = {
|
||||
axis: "-",
|
||||
inside: true,
|
||||
nogpu: true,
|
||||
step: toolOver,
|
||||
tolerance,
|
||||
tool,
|
||||
};
|
||||
}
|
||||
updateToolDiams(toolDiam);
|
||||
if (tabs) {
|
||||
tabs.forEach(tab => {
|
||||
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
|
||||
});
|
||||
}
|
||||
function newPocket() {
|
||||
pockets.push(sliceOut = []);
|
||||
}
|
||||
function newSliceOut(z) {
|
||||
let slice = newSlice(z);
|
||||
sliceOut.push(slice);
|
||||
return slice;
|
||||
}
|
||||
async function clearZ(polys, z, down) {
|
||||
if (down) {
|
||||
// adjust step down to a value <= down that
|
||||
// ends on the lowest z specified
|
||||
let diff = zTop - z;
|
||||
down = diff / Math.ceil(diff / down);
|
||||
}
|
||||
let zs = down ? base_util.lerp(zTop, z, down) : [ z ];
|
||||
if (engrave) {
|
||||
toolDiam = toolOver;
|
||||
}
|
||||
if (contour) {
|
||||
expand = engrave ? 0 : expand;
|
||||
} else if (expand) {
|
||||
polys = POLY.offset(polys, expand);
|
||||
}
|
||||
let zpro = 0, zinc = 1 / (polys.length * zs.length);
|
||||
for (let poly of polys) {
|
||||
newPocket();
|
||||
for (let z of zs) {
|
||||
let clip = [], shadow;
|
||||
if (contour) {
|
||||
if (smooth) {
|
||||
clip = POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal);
|
||||
} else {
|
||||
clip = [ poly ];
|
||||
}
|
||||
} else {
|
||||
shadow = shadowAt(z);
|
||||
if (smooth) {
|
||||
shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smoothVal), -smoothVal), z);
|
||||
}
|
||||
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
|
||||
if (op.outline) {
|
||||
POLY.clearInner(clip);
|
||||
}
|
||||
}
|
||||
if (clip.length === 0) {
|
||||
continue;
|
||||
}
|
||||
let slice = newSliceOut(z);
|
||||
let count = engrave ? 1 : 999;
|
||||
slice.camTrace = { tool, rate, plunge };
|
||||
if (toolDiam) {
|
||||
const offs = contour ?
|
||||
[ expand || (-0.02), -toolOver ] :
|
||||
[ -toolDiam / 2, -toolOver ];
|
||||
POLY.offset(clip, offs, {
|
||||
count, outs: slice.camLines = [], flat:true, z, minArea: 0
|
||||
});
|
||||
} else {
|
||||
// when engraving with a 0 width tip
|
||||
slice.camLines = clip;
|
||||
}
|
||||
if (tabs) {
|
||||
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
|
||||
} else {
|
||||
slice.camLines = POLY.flatten(slice.camLines, null, true);
|
||||
}
|
||||
POLY.setWinding(slice.camLines, cutdir, false);
|
||||
if (contour) {
|
||||
slice.camLines = await pocket.conform(slice.camLines, op.refine, engrave, pct => {
|
||||
progress(0.9 + (zpro + zinc * pct) * 0.1, "conform");
|
||||
});
|
||||
}
|
||||
slice.output()
|
||||
.setLayer(state.layername, {line: color}, false)
|
||||
.addPolys(slice.camLines)
|
||||
if (devel && shadow) slice.output()
|
||||
.setLayer("pocket shadow", {line: 0xff8811}, false)
|
||||
.addPolys(shadow);
|
||||
if (!contour) {
|
||||
progress(zpro, "pocket");
|
||||
}
|
||||
zpro += zinc;
|
||||
addSlices(slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
let surfaces = op.surfaces[widget.id] || [];
|
||||
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4));
|
||||
// let vert = widget.getVertices().array.map(v => v.round(4));
|
||||
let outline = [];
|
||||
let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG);
|
||||
let zmin = Infinity;
|
||||
let j=0, k=faces.length;
|
||||
for (let face of faces) {
|
||||
let i = face * 9;
|
||||
outline.push(newPolygon()
|
||||
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
|
||||
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
|
||||
.add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++]))
|
||||
);
|
||||
}
|
||||
zmin = Math.max(zBottom, zmin);
|
||||
outline = POLY.union(outline, 0.0001, true);
|
||||
outline = POLY.setWinding(outline, cutdir, false);
|
||||
outline = healPolys(outline);
|
||||
if (smooth) {
|
||||
outline = POLY.offset(POLY.offset(outline, smoothVal), -smoothVal);
|
||||
}
|
||||
if (outline.length) {
|
||||
// option to skip interior features (holes, pillars)
|
||||
if (op.outline) {
|
||||
POLY.clearInner(outline);
|
||||
}
|
||||
await clearZ(outline, zmin + 0.0001, down);
|
||||
if (devel && sliceOut?.length) sliceOut[0].output()
|
||||
.setLayer("pocket area", {line: 0x1188ff}, false)
|
||||
.addPolys(outline)
|
||||
progress(1, "pocket");
|
||||
}
|
||||
}
|
||||
|
||||
// mold cam output lines to the surface of the topo offset by tool geometry
|
||||
async conform(camLines, refine, engrave, progress) {
|
||||
if (!this.topo) {
|
||||
console.log('deferred topo');
|
||||
this.topo = await Topo({
|
||||
// onupdate: (update, msg) => {
|
||||
onupdate: (index, total, msg) => {
|
||||
progress((index / total) * 0.9, msg);
|
||||
},
|
||||
ondone: (slices) => {
|
||||
// console.log({ contour: slices });
|
||||
},
|
||||
contour: this.contour,
|
||||
state: this.state
|
||||
});
|
||||
}
|
||||
const topo = this.topo;
|
||||
// re-segment polygon to a higher resolution
|
||||
const hirez = camLines.map(p => p.segment(topo.tolerance * 2));
|
||||
// walk points and offset from surface taking into account tool geometry
|
||||
let steps = hirez.length;
|
||||
let iter = 0;
|
||||
for (let poly of hirez) {
|
||||
for (let point of poly.points) {
|
||||
point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y);
|
||||
}
|
||||
progress((iter++ / steps) * 0.8);
|
||||
}
|
||||
steps = steps * refine;
|
||||
iter = 0;
|
||||
// walk points noting z deltas and smoothing z sawtooth patterns
|
||||
for (let j=0; j<refine; j++) {
|
||||
for (let poly of hirez) {
|
||||
const points = poly.points, length = points.length;
|
||||
let sn = []; // segment normals
|
||||
for (let i=0; i<length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
sn.push(calc_normal(p1, p2));
|
||||
}
|
||||
let vn = []; // vertex normals
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
progress((iter++ / steps) * 0.2 + 0.8);
|
||||
}
|
||||
}
|
||||
// return hirez.map(p => p.midpoints(topo.tolerance * 8));
|
||||
return hirez;
|
||||
let { contour, down, expand, follow, outline, ov_botz, ov_topz } = op;
|
||||
let { plunge, rate, refine, smooth, spindle, surfaces, tolerance, tool } = op;
|
||||
let pocket = {
|
||||
areas: {},
|
||||
down,
|
||||
expand,
|
||||
follow,
|
||||
mode: contour ? 'surface' : 'clear',
|
||||
outline,
|
||||
ov_botz,
|
||||
ov_topz,
|
||||
over: op.step,
|
||||
plunge,
|
||||
rate,
|
||||
refine,
|
||||
rename: op.rename ?? "pocket",
|
||||
smooth,
|
||||
spindle,
|
||||
sr_type: 'offset',
|
||||
surfaces,
|
||||
tolerance,
|
||||
tool,
|
||||
tr_type: 'none',
|
||||
};
|
||||
this.op_pocket = new OpArea(state, pocket);
|
||||
return this.op_pocket.slice(progress);
|
||||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, pockets } = this;
|
||||
let { pocket, setTool, setSpindle, setTolerance } = ops;
|
||||
let { process } = state.settings;
|
||||
|
||||
setTool(op.tool, op.rate);
|
||||
setSpindle(op.spindle);
|
||||
|
||||
if (this.topo) {
|
||||
setTolerance(this.topo.tolerance);
|
||||
}
|
||||
|
||||
// eliminate empty pockets
|
||||
pockets = pockets.filter(p => p.length);
|
||||
|
||||
// naive output order
|
||||
for (let slices of pockets) {
|
||||
pocket({
|
||||
cutdir: op.ov_conv,
|
||||
depthFirst: process.camDepthFirst && !state.isIndexed,
|
||||
easeDown: op.down && process.easeDown ? op.down : 0,
|
||||
progress: (n,m) => progress(n/m, "pocket"),
|
||||
slices
|
||||
});
|
||||
}
|
||||
return this.op_pocket.prepare(ops, progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,18 +147,16 @@ class OpRegister extends CamOp {
|
|||
|
||||
prepare(ops, progress) {
|
||||
let { op } = this;
|
||||
let { emitDrills, setDrill, setSpindle, setTool } = ops;
|
||||
let { emitDrills, setDrill, setTool } = ops;
|
||||
|
||||
if (op.axis === '-' || op.axis === '=') {
|
||||
setTool(op.tool, op.feed, op.rate);
|
||||
setSpindle(op.spindle);
|
||||
for (let slice of this.sliceOut) {
|
||||
ops.emitTrace(slice);
|
||||
}
|
||||
} else {
|
||||
setTool(op.tool, undefined, op.rate);
|
||||
setDrill(op.down, op.lift, op.dwell);
|
||||
setSpindle(op.spindle);
|
||||
emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,341 +1,87 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { OpArea } from './op-area.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { util as base_util } from '../../../geo/base.js';
|
||||
import { poly2polyEmit } from '../../../geo/paths.js';
|
||||
|
||||
class OpRough extends CamOp {
|
||||
constructor(state, op) {
|
||||
super(state, op);
|
||||
}
|
||||
|
||||
// todo: cutThruBypass
|
||||
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { settings, slicer, addSlices, unsafe, color, widget } = state;
|
||||
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
|
||||
let { ztOff, zMax, shadowAt, isIndexed} = state;
|
||||
let { shadow, tool, widget } = state;
|
||||
let { workarea } = state;
|
||||
let { controller, process, stock } = settings;
|
||||
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0};
|
||||
|
||||
let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part;
|
||||
let cutOutside = !op.inside;
|
||||
let shadowBase = shadow.base;
|
||||
|
||||
if (op.down <= 0) {
|
||||
throw `invalid step down "${op.down}"`;
|
||||
}
|
||||
|
||||
let roughIn = op.inside;
|
||||
let roughDown = op.down;
|
||||
let roughLeave = op.leave || 0;
|
||||
let roughLeaveZ = op.leavez || 0;
|
||||
let roughStock = op.all && isIndexed;
|
||||
let toolDiam = new Tool(settings, op.tool).fluteDiameter();
|
||||
let trueShadow = process.camTrueShadow === true;
|
||||
let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part;
|
||||
|
||||
updateToolDiams(toolDiam);
|
||||
|
||||
if (tabs) {
|
||||
tabs.forEach(tab => {
|
||||
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
|
||||
});
|
||||
if (op.all) {
|
||||
shadowBase = [ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ];
|
||||
}
|
||||
|
||||
// clear the stock above the area to be roughed out
|
||||
if (workarea.top_z > workarea.top_part && !cutThruBypass) {
|
||||
let shadow = state.shadow.base.clone();
|
||||
let step = toolDiam * op.step;
|
||||
let inset = roughStock ?
|
||||
POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) :
|
||||
POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2);
|
||||
let facing = POLY.offset(inset, -step, { count: 999, flat: true });
|
||||
let zdiv = ztOff / roughDown;
|
||||
let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown;
|
||||
if (ztOff === 0) {
|
||||
// compensate for lack of z top offset in this scenario
|
||||
ztOff = zstep;
|
||||
}
|
||||
let zsteps = Math.round(ztOff / zstep);
|
||||
let camFaces = this.camFaces = [];
|
||||
let zstart = zMax + ztOff - zstep;
|
||||
for (let z = zstart; zsteps > 0; zsteps--) {
|
||||
let slice = newSlice();
|
||||
slice.z = z;
|
||||
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
|
||||
if (tabs) {
|
||||
polys = cutTabs(tabs, polys, slice.z);
|
||||
}
|
||||
slice.camLines = polys;
|
||||
slice.output()
|
||||
.setLayer(state.layername, {face: color, line: color})
|
||||
.addPolys(slice.camLines);
|
||||
addSlices(slice);
|
||||
camFaces.push(slice);
|
||||
z -= zstep;
|
||||
}
|
||||
let areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001));
|
||||
|
||||
let rough = {
|
||||
rename: op.rename ?? "rough",
|
||||
spindle: op.spindle,
|
||||
tool: op.tool,
|
||||
rate: op.rate,
|
||||
plunge: op.plunge,
|
||||
mode: 'clear',
|
||||
over: op.step,
|
||||
down: op.down,
|
||||
expand: 0,
|
||||
smooth: 0,
|
||||
outline: true,
|
||||
omitthru: op.omitthru,
|
||||
leave_xy: op.leave,
|
||||
leave_z: op.leavez,
|
||||
ov_botz: op.ov_botz,
|
||||
ov_topz: op.ov_topz,
|
||||
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
||||
surfaces: {}
|
||||
};
|
||||
|
||||
this.op_rough = new OpArea(state, rough);
|
||||
await this.op_rough.slice(progress);
|
||||
|
||||
if (cutOutside) {
|
||||
let cutout = {
|
||||
rename: op.rename ?? "rough",
|
||||
spindle: op.spindle,
|
||||
tool: op.tool,
|
||||
rate: op.rate,
|
||||
plunge: op.plunge,
|
||||
mode: 'trace',
|
||||
tr_type: 'none',
|
||||
down: op.down,
|
||||
expand: 0,
|
||||
smooth: 1,
|
||||
outline: !op.omitthru,
|
||||
ov_botz: op.ov_botz,
|
||||
ov_topz: op.ov_topz,
|
||||
areas: { [widget.id]: areas.map(p => p.toArray()) },
|
||||
surfaces: {}
|
||||
};
|
||||
this.op_cutout = new OpArea(state, cutout);
|
||||
await this.op_cutout.slice(progress);
|
||||
}
|
||||
|
||||
// create roughing slices
|
||||
let flats = [];
|
||||
let shadow = [];
|
||||
let slices = [];
|
||||
let indices = slicer.interval(roughDown, {
|
||||
down: true, min: 0, fit: true, off: 0.01
|
||||
});
|
||||
|
||||
// shift out first (top-most) slice
|
||||
indices.shift();
|
||||
|
||||
// find flats and add to indices for slicing
|
||||
if (op.flats) {
|
||||
let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2;
|
||||
let flats = Object.entries(slicer.zFlat)
|
||||
.filter(row => row[1] > flatArea)
|
||||
.map(row => row[0])
|
||||
.map(v => parseFloat(v).round(5))
|
||||
.filter(v => v >= workarea.bottom_z);
|
||||
flats.forEach(v => {
|
||||
if (!indices.contains(v)) {
|
||||
indices.push(v);
|
||||
}
|
||||
});
|
||||
indices = indices.sort((a,b) => { return b - a });
|
||||
// if layer is not on a flat and next one is,
|
||||
// then move this layer up to mid-point to previous layer
|
||||
// this is not perfect. the best method is to interpolate
|
||||
// between flats so that each step is < step down. on todo list
|
||||
for (let i=1; i<indices.length-1; i++) {
|
||||
const prev = indices[i-1];
|
||||
const curr = indices[i];
|
||||
const next = indices[i+1];
|
||||
if (!flats.contains(curr) && flats.contains(next)) {
|
||||
// console.log('move',curr,'up toward',prev,'b/c next',next,'is flat');
|
||||
indices[i] = next + ((prev - next) / 2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// add flats to shadow
|
||||
flats = Object.keys(slicer.zFlat)
|
||||
.map(v => (parseFloat(v) - 0.01).round(5))
|
||||
.filter(v => v > 0 && indices.indexOf(v) < 0);
|
||||
indices = indices.appendAll(flats).sort((a,b) => b-a);
|
||||
}
|
||||
|
||||
indices = indices.filter(v => v >= workarea.bottom_z);
|
||||
// console.log('indices', ...indices, {zBottom});
|
||||
|
||||
let lsz;
|
||||
let cnt = 0;
|
||||
let tot = 0;
|
||||
await slicer.slice(indices, { each: data => {
|
||||
shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true);
|
||||
if (flats.indexOf(data.z) >= 0) {
|
||||
// exclude flats injected to complete shadow
|
||||
return;
|
||||
}
|
||||
if (data.z > workarea.top_z) {
|
||||
return;
|
||||
}
|
||||
data.shadow = trueShadow ? shadowAt(data.z, lsz) : shadow.clone(true);
|
||||
data.slice.shadow = data.shadow;
|
||||
slices.push(data.slice);
|
||||
lsz = data.z;
|
||||
progress(0.25 + 0.25 * (++cnt / tot));
|
||||
}, progress: (index, total) => {
|
||||
tot = total;
|
||||
progress((index / total) * 0.25);
|
||||
} });
|
||||
|
||||
if (trueShadow) {
|
||||
shadow = state.shadow.base.clone(true);
|
||||
} else {
|
||||
shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true);
|
||||
}
|
||||
|
||||
// inset or eliminate thru holes from shadow
|
||||
shadow = POLY.flatten(shadow.clone(true), [], true);
|
||||
if (!op.omitthru)
|
||||
thruHoles.forEach(hole => {
|
||||
shadow = shadow.map(p => {
|
||||
if (p.isEquivalent(hole)) {
|
||||
let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05));
|
||||
return po ? po[0] : undefined;
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
}).filter(p => p);
|
||||
});
|
||||
shadow = POLY.nest(shadow);
|
||||
if (op.voids) {
|
||||
// eliminate voids from shadow when "clear voids" enables
|
||||
for (let s of shadow) s.inner = undefined;
|
||||
}
|
||||
|
||||
// shell = shadow expanded by half tool diameter + leave stock
|
||||
const sadd = roughIn ? toolDiam / 2 : toolDiam / 2;
|
||||
const shell = roughStock ?
|
||||
POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) :
|
||||
POLY.offset(shadow, sadd + roughLeave);
|
||||
|
||||
slices.forEach((slice, index) => {
|
||||
let offset = [shell.clone(true),slice.shadow.clone(true)].flat();
|
||||
let flat = POLY.flatten(offset, [], true);
|
||||
let nest = POLY.setZ(POLY.nest(flat), slice.z);
|
||||
|
||||
// inset offset array by 1/2 diameter then by tool overlap %
|
||||
offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], {
|
||||
minArea: Math.min(0.01, toolDiam * op.step / 4),
|
||||
z: slice.z,
|
||||
count: 999,
|
||||
flat: true,
|
||||
call: (polys, count, depth) => {
|
||||
// used in depth-first path creation
|
||||
polys.forEach(p => {
|
||||
p.depth = depth;
|
||||
if (p.inner) {
|
||||
p.inner.forEach(p => p.depth = depth);
|
||||
}
|
||||
});
|
||||
}
|
||||
}) || [];
|
||||
|
||||
// add outside pass if not inside only
|
||||
if (!roughIn && !roughStock) {
|
||||
const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z});
|
||||
if (outside) {
|
||||
outside.forEach(p => p.depth = -p.depth);
|
||||
offset.appendAll(outside);
|
||||
}
|
||||
}
|
||||
|
||||
// elimate double inset on inners
|
||||
offset.forEach(op => {
|
||||
if (op.inner) {
|
||||
let pv1 = op.perimeter();
|
||||
let newinner = [];
|
||||
op.inner.forEach(oi => {
|
||||
let pv2 = oi.perimeter();
|
||||
let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2;
|
||||
if (pct < 0.98) {
|
||||
newinner.push(oi);
|
||||
}
|
||||
});
|
||||
op.inner = newinner;
|
||||
}
|
||||
});
|
||||
|
||||
if (process.camStockClipTo && stock.x && stock.y && stock.center) {
|
||||
let { center } = stock;
|
||||
let x = center.x - center_off.x;
|
||||
let y = center.y - center_off.y;
|
||||
let rect = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001);
|
||||
offset = cutPolys([rect], offset, slice.z, true);
|
||||
}
|
||||
|
||||
if (!offset) return;
|
||||
|
||||
slice.camLines = offset;
|
||||
if (roughLeaveZ) {
|
||||
// offset roughing in Z as well to minimize tool marks on curved surfaces
|
||||
// const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2);
|
||||
for (let poly of slice.camLines) {
|
||||
for (let point of poly.points) point.z += roughLeaveZ;
|
||||
}
|
||||
}
|
||||
if (controller.devel) {
|
||||
if (tabs) slice.output()
|
||||
.setLayer("tabs clip", {line: 0xaa00aa}, true)
|
||||
.addPolys(tabs.map(tab => tab.off).flat());
|
||||
slice.output()
|
||||
.setLayer("slice", {line: 0xaaaa00}, true)
|
||||
.addPolys(slice.topPolys())
|
||||
// .setLayer("top shadow", {line: 0x0000aa})
|
||||
// .addPolys(tshadow)
|
||||
// .setLayer("rough shadow", {line: 0x00aa00})
|
||||
// .addPolys(shadow)
|
||||
.setLayer("shadow clip", {line: 0xaa0000})
|
||||
.addPolys(shell);
|
||||
}
|
||||
progress(0.5 + 0.5 * (index / slices.length));
|
||||
});
|
||||
|
||||
let last = slices[slices.length-1];
|
||||
|
||||
// when step down > full cut depth, clear slices and
|
||||
// leave only the cut-thru pass(es)
|
||||
if (cutThruBypass) {
|
||||
slices.length = 0;
|
||||
}
|
||||
// add cut thru passes
|
||||
if (workarea.bottom_z < 0)
|
||||
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
|
||||
if (!last) continue;
|
||||
let add = last.clone(true);
|
||||
add.z -= zneg;
|
||||
add.camLines = last.camLines.clone(true);
|
||||
add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ));
|
||||
// add.tops.forEach(top => top.poly.setZ(add.z));
|
||||
// add.shadow = last.shadow.clone(true);
|
||||
slices.push(add);
|
||||
}
|
||||
|
||||
slices.forEach(slice => {
|
||||
if (slice.camLines && tabs) {
|
||||
slice.camLines = cutTabs(tabs, slice.camLines);
|
||||
}
|
||||
slice.output()
|
||||
.setLayer(state.layername, {face: color, line: color})
|
||||
.addPolys(slice.camLines);
|
||||
});
|
||||
this.sliceOut = slices.filter(slice => slice.camLines);
|
||||
|
||||
addSlices(this.sliceOut);
|
||||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state, sliceOut, camFaces } = this;
|
||||
let { setTool, setSpindle, pocket, polyEmit, newLayer, printPoint } = ops;
|
||||
let { settings } = state;
|
||||
let { process } = settings;
|
||||
|
||||
let easeDown = process.camEaseDown;
|
||||
let cutdir = op.ov_conv;
|
||||
let depthFirst = process.camDepthFirst && !state.isIndexed;
|
||||
|
||||
setTool(op.tool, op.rate, op.plunge);
|
||||
setSpindle(op.spindle);
|
||||
|
||||
// output the clearing of stock above roughing
|
||||
for (let slice of (camFaces || [])) {
|
||||
const level = [];
|
||||
for (let poly of slice.camLines) {
|
||||
level.push(poly);
|
||||
if (poly.inner) {
|
||||
poly.inner.forEach(function(inner) {
|
||||
level.push(inner);
|
||||
});
|
||||
}
|
||||
}
|
||||
// set winding specified in output
|
||||
POLY.setWinding(level, cutdir, false);
|
||||
poly2polyEmit(level, printPoint, (poly, index, count) => {
|
||||
printPoint = polyEmit(poly, index, count, printPoint, {cutFromLast: true});
|
||||
}, {
|
||||
weight: process.camInnerFirst
|
||||
});
|
||||
newLayer();
|
||||
}
|
||||
|
||||
// output the roughing passes
|
||||
pocket({
|
||||
cutdir,
|
||||
depthFirst,
|
||||
easeDown: op.down && easeDown ? 0.001 : 0,
|
||||
progress: (n,m) => progress(n/m, "routing"),
|
||||
slices: sliceOut
|
||||
});
|
||||
async prepare(ops, progress) {
|
||||
await this.op_rough.prepare(ops, progress);
|
||||
if (this.op_cutout) await this.op_cutout.prepare(ops, progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
|
||||
/**
|
||||
* Computes the Part "shadow" and attaches relevant data to the "state" object
|
||||
|
|
@ -26,90 +27,63 @@ class OpShadow extends CamOp {
|
|||
|
||||
async slice(progress) {
|
||||
let state = this.state;
|
||||
let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state;
|
||||
let { addSlices, settings, shadowAt, unsafe, widget } = state;
|
||||
let { devel } = settings.controller;
|
||||
|
||||
let realOps = ops.map(rec => rec.op).filter(op => op);
|
||||
let trueShadow = state.settings.process.camTrueShadow === true;
|
||||
|
||||
let minStepDown = realOps
|
||||
.map(op => (op.down || 3) / (trueShadow ? 1 : 3))
|
||||
.reduce((a,v) => Math.min(a, v, 1));
|
||||
|
||||
let tslices = [];
|
||||
let tshadow = [];
|
||||
let tzindex = slicer.interval(minStepDown, {
|
||||
fit: true, off: 0.01, down: true, flats: true
|
||||
});
|
||||
let bounds = widget.getBoundingBox();
|
||||
let slices = [];
|
||||
let shadowBase;
|
||||
let skipTerrain = unsafe;
|
||||
let terrain = [];
|
||||
let tzindex = [];
|
||||
|
||||
let minZ = Math.floor(bounds.min.z);
|
||||
let maxZ = Math.floor(bounds.max.z);
|
||||
for (let z = maxZ; z >= minZ; z--) {
|
||||
tzindex.push(z);
|
||||
}
|
||||
|
||||
if (skipTerrain) {
|
||||
console.log("skipping terrain generation");
|
||||
tzindex = [ tzindex.pop() ];
|
||||
shadowBase = [ newPolygon() ];
|
||||
tzindex = [ ];
|
||||
}
|
||||
|
||||
let lsz; // only shadow up to bottom of last shadow for progressive union
|
||||
let cnt = 0;
|
||||
let tot = 0;
|
||||
|
||||
// terrain is the "shadow stack" where index 0 = top of part
|
||||
// thus array.length -1 = bottom of part
|
||||
let terrain = await slicer.slice(tzindex, { each: data => {
|
||||
let shadow = trueShadow ? shadowAt(data.z, lsz) : [];
|
||||
tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true);
|
||||
tslices.push(data.slice);
|
||||
// capture current shadow for this slice
|
||||
data.slice.shadow = tshadow;
|
||||
if (false) {
|
||||
const slice = data.slice;
|
||||
addSlices(slice);
|
||||
for (let i=0; i<tzindex.length; i++) {
|
||||
let z = tzindex[i];
|
||||
let shadow = shadowBase = await shadowAt(z);
|
||||
let slice = newSlice(z);
|
||||
slice.shadow = shadow;
|
||||
slice.addTops(shadow);
|
||||
slices.push(slice);
|
||||
terrain.push({ slice, tops: shadow });
|
||||
if (devel) {
|
||||
slice.output()
|
||||
.setLayer("shadow", {line: 0x888800, thin: true })
|
||||
.addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true });
|
||||
slice.output()
|
||||
.setLayer("slice", {line: 0x886622, thin: true })
|
||||
.addPolys(POLY.setZ(data.tops.clone(true), data.z), { thin: true });
|
||||
// let p1 = [], p2 = [], cp = p1;
|
||||
// for (let line of data.lines) {
|
||||
// cp.push(line.p1);
|
||||
// cp.push(line.p2);
|
||||
// cp = (cp === p1 ? p2 : p1);
|
||||
// }
|
||||
// slice.output()
|
||||
// .setLayer("lines1", {line: 0x884444, thin: true })
|
||||
// .addLines(p1, { thin: true });
|
||||
// slice.output()
|
||||
// .setLayer("lines2", {line: 0x444488, thin: true })
|
||||
// .addLines(p2, { thin: true });
|
||||
.addPolys(shadow, { thin: true });
|
||||
}
|
||||
lsz = data.z;
|
||||
progress(0.5 + 0.5 * (++cnt / tot));
|
||||
}, progress: (index, total) => {
|
||||
tot = total;
|
||||
progress((index / total) * 0.5);
|
||||
} });
|
||||
progress(i / tzindex.length);
|
||||
}
|
||||
|
||||
if (devel && slices.length) {
|
||||
addSlices(slices);
|
||||
}
|
||||
|
||||
if (terrain.length === 0) {
|
||||
throw `invalid widget shadow`;
|
||||
}
|
||||
|
||||
// TODO: deprecate use of separate shadow vars in state
|
||||
state.center = tshadow[0].bounds.center();
|
||||
state.tshadow = tshadow; // true shadow (base of part)
|
||||
state.terrain = terrain; // stack of shadow slices stored in tops
|
||||
state.tslices = tslices; // raw slicer 'data' layer outputs
|
||||
state.skipTerrain = skipTerrain;
|
||||
|
||||
// TODO: refactor ops to use a unified shadow object
|
||||
state.shadow = {
|
||||
base: tshadow, // computed shadow union at base of part
|
||||
base: shadowBase, // computed shadow union at base of part
|
||||
holes: shadowBase.map(p => p.inner || []).flat(),
|
||||
skip: skipTerrain,
|
||||
slices: slices, // legacy / transitional
|
||||
stack: terrain, // stack of shadow slices
|
||||
slices: tslices, // raw slicer 'data' objects
|
||||
skip: skipTerrain
|
||||
};
|
||||
|
||||
// identify through holes which are inner/child polygons
|
||||
// on the bottom-most layer of the shadow stack (tshadow, index == 0)
|
||||
state.thruHoles = tshadow.map(p => p.inner || []).flat();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,307 +1,47 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { CamOp } from './op.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { newPolygon } from '../../../geo/polygon.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { util as base_util } from '../../../geo/base.js';
|
||||
import { poly2polyEmit } from '../../../geo/paths.js';
|
||||
import { newPoint } from '../../../geo/point.js';
|
||||
import { OpArea } from './op-area.js';
|
||||
|
||||
class OpTrace extends CamOp {
|
||||
constructor(state, op) {
|
||||
super(state, op);
|
||||
}
|
||||
|
||||
async slice(progress) {
|
||||
const debug = false;
|
||||
let { op, state } = this;
|
||||
let { tool, rate, down, plunge, offset, offover, thru } = op;
|
||||
let { ov_conv } = op;
|
||||
let { settings, widget, addSlices, zThru, tabs, workarea } = state;
|
||||
let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state;
|
||||
let { process, stock } = settings;
|
||||
let { camStockClipTo } = process;
|
||||
if (state.isIndexed) {
|
||||
throw 'trace op not supported with indexed stock';
|
||||
}
|
||||
// generate tracing offsets from chosen features
|
||||
let zTop = workarea.top_z;
|
||||
let zBottom = workarea.bottom_z;
|
||||
let sliceOut = this.sliceOut = [];
|
||||
let areas = op.areas[widget.id] || [];
|
||||
let camTool = new Tool(settings, tool);
|
||||
let toolDiam = camTool.fluteDiameter();
|
||||
let toolOver = toolDiam * op.step;
|
||||
let traceOffset = camTool.traceOffset()
|
||||
let cutdir = ov_conv;
|
||||
let polys = [];
|
||||
let reContour = false;
|
||||
let canRecontour = offset !== 'none' && down < 0;
|
||||
let stockRect = stock.center && stock.x && stock.y ?
|
||||
newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined;
|
||||
updateToolDiams(toolDiam);
|
||||
// todo: cut thru, wide steps
|
||||
|
||||
if (tabs) {
|
||||
tabs.forEach(tab => {
|
||||
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
|
||||
});
|
||||
}
|
||||
for (let arr of areas) {
|
||||
let poly = newPolygon().fromArray(arr);
|
||||
POLY.setWinding([ poly ], cutdir, false);
|
||||
polys.push(poly);
|
||||
let zs = poly.points.map(p => p.z);
|
||||
let min = Math.min(...zs);
|
||||
let max = Math.max(...zs);
|
||||
if (max - min > 0.0001 && canRecontour) {
|
||||
reContour = true;
|
||||
down = 0;
|
||||
}
|
||||
}
|
||||
if (false) newSliceOut(0).output()
|
||||
.setLayer("polys", {line: 0xaaaa00}, false)
|
||||
.addPolys(polys);
|
||||
function newSliceOut(z) {
|
||||
let slice = newSlice(z);
|
||||
addSlices(slice);
|
||||
sliceOut.push(slice);
|
||||
return slice;
|
||||
}
|
||||
function minZ(z) {
|
||||
return zBottom ? Math.max(zBottom, z - thru) : z - thru;
|
||||
}
|
||||
function followZ(poly) {
|
||||
if (op.dogbone) {
|
||||
addDogbones(poly, toolDiam / 5, !op.revbone);
|
||||
}
|
||||
let z = poly.getZ();
|
||||
let slice = newSliceOut(z);
|
||||
slice.camTrace = { tool, rate, plunge };
|
||||
if (tabs) {
|
||||
slice.camLines = cutTabs(tabs, [poly], z);
|
||||
} else {
|
||||
slice.camLines = [ poly ];
|
||||
}
|
||||
if (camStockClipTo && stockRect) {
|
||||
slice.camLines = cutPolys([stockRect], slice.camLines, z, true);
|
||||
}
|
||||
if (reContour) {
|
||||
state.contourPolys(widget, slice.camLines);
|
||||
}
|
||||
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
|
||||
slice.output()
|
||||
.setLayer(state.layername, {line: color}, false)
|
||||
.addPolys(slice.camLines)
|
||||
}
|
||||
function clearZnew(polys, z, down) {
|
||||
if (down) {
|
||||
// adjust step down to a value <= down that
|
||||
// ends on the lowest z specified
|
||||
let diff = zTop - z;
|
||||
down = diff / Math.ceil(diff / down);
|
||||
}
|
||||
let zs = down ? base_util.lerp(zTop, z, down) : [ z ];
|
||||
let zpro = 0, zinc = 1 / (polys.length * zs.length);
|
||||
for (let poly of polys) {
|
||||
// newPocket();
|
||||
for (let z of zs) {
|
||||
let clip = [], shadow;
|
||||
shadow = shadowAt(z);
|
||||
// for cases where the shadow IS the poly like
|
||||
// with lettering without a bounding frame, clip
|
||||
// will fail and we need to restore the matching poly
|
||||
let subshadow = true;
|
||||
for (let spo of shadow) {
|
||||
if (poly.isInside(spo, 0.01)) {
|
||||
subshadow = false;
|
||||
clip = [ poly ];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (subshadow) {
|
||||
POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0);
|
||||
}
|
||||
if (op.outline) {
|
||||
POLY.clearInner(clip);
|
||||
}
|
||||
if (clip.length === 0) {
|
||||
continue;
|
||||
}
|
||||
let count = 999;
|
||||
let slice = newSliceOut(z);
|
||||
slice.camTrace = { tool, rate, plunge };
|
||||
if (toolDiam) {
|
||||
const offs = [ -toolDiam / 2, -toolOver ];
|
||||
POLY.offset(clip, offs, {
|
||||
count, outs: slice.camLines = [], flat:true, z, minArea: 0
|
||||
});
|
||||
} else {
|
||||
// when engraving with a 0 width tip
|
||||
slice.camLines = clip;
|
||||
}
|
||||
if (tabs) {
|
||||
slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z);
|
||||
} else {
|
||||
slice.camLines = POLY.flatten(slice.camLines, null, true);
|
||||
}
|
||||
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
|
||||
if (debug && shadow) slice.output()
|
||||
.setLayer("trace shadow", {line: 0xff8811}, false)
|
||||
.addPolys(shadow)
|
||||
if (debug) slice.output()
|
||||
.setLayer("trace poly", {line: 0x1188ff}, false)
|
||||
.addPolys([ poly ])
|
||||
slice.output()
|
||||
.setLayer(state.layername, {line: color}, false)
|
||||
.addPolys(slice.camLines)
|
||||
progress(zpro, "trace");
|
||||
zpro += zinc;
|
||||
addSlices(slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
function similar(v1, v2, epsilon = 0.01) {
|
||||
return Math.abs(v1-v2) <= epsilon;
|
||||
}
|
||||
function centerPoly(p1, p2) {
|
||||
// follow poly with most points
|
||||
if (p2.length > p1.length) {
|
||||
let t = p1;
|
||||
p1 = p2;
|
||||
p2 = t;
|
||||
}
|
||||
let np = newPolygon().setOpen(true);
|
||||
for (let p of p1.points) {
|
||||
let q = p2.findClosestPointTo(p);
|
||||
np.push(p.midPointTo3D(q.point));
|
||||
}
|
||||
return np;
|
||||
}
|
||||
function centerPolys(polys) {
|
||||
// select open polys and sort by length
|
||||
let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter());
|
||||
if (ptst.length < 2) {
|
||||
return polys;
|
||||
}
|
||||
let pt = newPoint(0,0,0);
|
||||
// ensure polys are ordered with start point closest to 0,0
|
||||
ptst.forEach(p => {
|
||||
if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) {
|
||||
p.reverse();
|
||||
}
|
||||
});
|
||||
let pout = polys.filter(p => p.isClosed());
|
||||
outer: for (let i=0,l=ptst.length; i<l-1; i++) {
|
||||
let p0 = ptst[i];
|
||||
if (!p0) continue;
|
||||
for (let j=i+1; j<l; j++) {
|
||||
let p1 = ptst[j];
|
||||
if (!p1) continue;
|
||||
if (
|
||||
similar(p0.perimeter(), p1.perimeter(), 0.1) &&
|
||||
similar(p0.first().distTo2D(p1.first()), toolDiam) &&
|
||||
similar(p0.last().distTo2D(p1.last()), toolDiam)
|
||||
) {
|
||||
pout.push(centerPoly(p0, p1));
|
||||
ptst[i] = undefined;
|
||||
ptst[j] = undefined;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
pout.appendAll(ptst.filter(p => p));
|
||||
return pout;
|
||||
}
|
||||
// connect selected segments if open and touching
|
||||
polys = healPolys(polys);
|
||||
// find center line for open polys spaced by tool diameter
|
||||
polys = centerPolys(polys);
|
||||
switch (op.mode) {
|
||||
case "follow":
|
||||
let routed = [];
|
||||
poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => {
|
||||
routed.push(poly);
|
||||
});
|
||||
let output = [];
|
||||
for (let poly of POLY.nest(routed)) {
|
||||
let offdist = offset !== 'none' ? offover : 0;
|
||||
if (!offdist)
|
||||
switch (offset) {
|
||||
case "outside": offdist = traceOffset; break;
|
||||
case "inside": offdist = -traceOffset; break;
|
||||
} else if (offset === "inside") {
|
||||
offdist = -offdist;
|
||||
}
|
||||
if (offdist) {
|
||||
let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true });
|
||||
if (pnew) {
|
||||
poly = POLY.setZ(pnew, poly.getZ());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
poly = [ poly ];
|
||||
}
|
||||
for (let pi of POLY.flatten(poly, [], true))
|
||||
if (down) {
|
||||
let zto = minZ(pi.getZ());
|
||||
if (zThru && similar(zto,0)) {
|
||||
zto -= zThru;
|
||||
}
|
||||
for (let z of base_util.lerp(zTop, zto, down)) {
|
||||
output.push(pi.clone().setZ(z));
|
||||
}
|
||||
} else {
|
||||
if (thru) {
|
||||
pi.setZ(pi.getZ() - thru);
|
||||
}
|
||||
output.push(pi);
|
||||
}
|
||||
if (!down && op.merge) {
|
||||
let nest = POLY.nest(output);
|
||||
let union = POLY.union(nest, 0, true);
|
||||
output = POLY.flatten(union, [], true);
|
||||
}
|
||||
}
|
||||
for (let poly of output) {
|
||||
followZ(poly);
|
||||
}
|
||||
break;
|
||||
case "clear":
|
||||
const zbo = widget.track.top - widget.track.box.d;
|
||||
let zmap = {};
|
||||
polys = POLY.nest(polys);
|
||||
for (let poly of polys) {
|
||||
let z = minZ(poly.minZ());
|
||||
if (offover) {
|
||||
let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true });
|
||||
if (pnew) {
|
||||
poly = POLY.setZ(pnew, poly.getZ());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
poly = [ poly ];
|
||||
}
|
||||
(zmap[z] = zmap[z] || []).appendAll(poly);
|
||||
}
|
||||
for (let [zv, polys] of Object.entries(zmap)) {
|
||||
clearZnew(polys, parseFloat(zv), down);
|
||||
}
|
||||
}
|
||||
async slice(progress) {
|
||||
let { op, state } = this;
|
||||
let { areas, down, expand, follow, offset, outline, mode, ov_botz, ov_topz } = op;
|
||||
let { plunge, rate, refine, smooth, spindle, step, steps, thru, tolerance, tool } = op;
|
||||
let trace = {
|
||||
areas,
|
||||
down,
|
||||
expand,
|
||||
follow,
|
||||
mode: mode === 'clear' ? 'clear' : 'trace',
|
||||
outline,
|
||||
ov_botz,
|
||||
ov_topz,
|
||||
over: op.step,
|
||||
plunge,
|
||||
rate,
|
||||
refine,
|
||||
rename: op.rename ?? "trace",
|
||||
smooth,
|
||||
spindle,
|
||||
step,
|
||||
surfaces: {},
|
||||
tolerance,
|
||||
tool,
|
||||
tr_type: offset
|
||||
};
|
||||
this.op_trace = new OpArea(state, trace);
|
||||
return this.op_trace.slice(progress);
|
||||
}
|
||||
|
||||
prepare(ops, progress) {
|
||||
let { op, state } = this;
|
||||
let { setTool, setSpindle } = ops;
|
||||
|
||||
setTool(op.tool, op.rate);
|
||||
setSpindle(op.spindle);
|
||||
for (let slice of this.sliceOut) {
|
||||
ops.emitTrace(slice);
|
||||
}
|
||||
async prepare(ops, progress) {
|
||||
return this.op_trace.prepare(ops, progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { CamOp } from './op.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { Slicer } from './slicer.js';
|
||||
import { Slicer } from './slicer_cam.js';
|
||||
|
||||
class OpXRay extends CamOp {
|
||||
constructor(state, op) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ import { polygons as POLY } from '../../../geo/polygons.js';
|
|||
import { setSliceTracker } from '../../core/slice.js';
|
||||
import { ops as OPS } from './ops.js';
|
||||
import { Tool } from './tool.js';
|
||||
import { Slicer as cam_slicer } from './slicer.js';
|
||||
import { Slicer as cam_slicer } from './slicer_cam.js';
|
||||
import { CAM } from './driver-be.js';
|
||||
|
||||
/**
|
||||
|
|
@ -25,19 +25,17 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
let tabW = widget.group.filter(w => w != widget);
|
||||
|
||||
let proc = settings.process,
|
||||
sliceAll = widget.slices = [],
|
||||
camOps = widget.camops = [],
|
||||
sliceAll = widget.slices = [],
|
||||
isIndexed = proc.camStockIndexed;
|
||||
|
||||
let stock, bounds, track,
|
||||
camZTop, camZBottom, camZThru, wztop, ztOff, zbOff,
|
||||
zBottom, zMin, zMax, zThru, zTop,
|
||||
minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units,
|
||||
axisRotation, axisIndex,
|
||||
part_size,
|
||||
let axisRotation, axisIndex,
|
||||
bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
|
||||
camZTop, camZBottom, camZThru, minToolDiam, maxToolDiam,
|
||||
bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut,
|
||||
top_stock, top_part, top_gap, top_z,
|
||||
workarea;
|
||||
zBottom, zMin, zMax, zThru, zTop,
|
||||
ztOff, zbOff, wztop;
|
||||
|
||||
axisRotation = axisIndex = undefined;
|
||||
dark = settings.controller.dark;
|
||||
|
|
@ -51,17 +49,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
// allow recomputing later if widget or settings changes
|
||||
const var_compute = () => {
|
||||
let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
|
||||
({ camZTop, camZBottom, camZThru } = proc);
|
||||
bounds = widget.getBoundingBox();
|
||||
let pos = widget.track.pos;
|
||||
stock = camStockOffset ? {
|
||||
x: bounds.dim.x + camStockX,
|
||||
y: bounds.dim.y + camStockY,
|
||||
z: bounds.dim.z + camStockZ,
|
||||
center: newPoint(pos.x, pos.y, pos.z)
|
||||
} : {
|
||||
x: camStockX,
|
||||
y: camStockY,
|
||||
z: camStockZ
|
||||
z: camStockZ,
|
||||
center: newPoint(pos.x, pos.y, pos.z)
|
||||
};
|
||||
({ camZTop, camZBottom, camZThru } = proc);
|
||||
track = widget.track;
|
||||
wztop = track.top;
|
||||
ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop);
|
||||
|
|
@ -71,7 +72,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
zMax = bounds.max.z;
|
||||
zThru = camZThru;
|
||||
zTop = zMax + ztOff;
|
||||
part_size = bounds.dim;
|
||||
bottom_gap = zbOff;
|
||||
bottom_part = 0;
|
||||
bottom_stock = -bottom_gap;
|
||||
|
|
@ -142,7 +142,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
let opList = [];
|
||||
let opSum = 0;
|
||||
let opTot = 0;
|
||||
let shadows = {};
|
||||
let slicer;
|
||||
let state = {
|
||||
addSlices,
|
||||
|
|
@ -159,9 +158,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
setAxisIndex,
|
||||
setToolDiam,
|
||||
settings,
|
||||
shadowAt,
|
||||
shadowAt(z) { return widget.shadowAt(z) },
|
||||
slicer,
|
||||
tabs,
|
||||
tool,
|
||||
unsafe,
|
||||
updateSlicer,
|
||||
updateToolDiams,
|
||||
|
|
@ -203,9 +203,9 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
}
|
||||
|
||||
async function computeShadows() {
|
||||
shadows = {};
|
||||
// console.log('(re)compute shadows');
|
||||
await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => {
|
||||
// console.log('reshadow', progress.round(3));
|
||||
// console.log('reshadowing', progress.round(3));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -223,21 +223,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
maxToolDiam = Math.max(maxToolDiam, toolDiam);
|
||||
}
|
||||
|
||||
function shadowAt(z) {
|
||||
let cached = shadows[z];
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
// find closest shadow above and use to speed up delta shadow gen
|
||||
let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z);
|
||||
let minZabove = Math.min(Infinity, ...zover);
|
||||
let shadow = computeShadowAt(widget, z, minZabove);
|
||||
if (minZabove < Infinity) {
|
||||
shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true);
|
||||
}
|
||||
return shadows[z] = POLY.setZ(shadow, z);
|
||||
}
|
||||
|
||||
async function setAxisIndex(degrees = 0, absolute = true) {
|
||||
axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
|
||||
axisRotation = (Math.PI / 180) * axisIndex;
|
||||
|
|
@ -284,14 +269,15 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
|
||||
let activeOps = proc.ops.filter(op => !op.disabled);
|
||||
|
||||
// silently preface op list with OpShadow
|
||||
if (isIndexed) {
|
||||
// preface op list with OpIndex
|
||||
if (activeOps.length === 0 || activeOps[0].type !== 'index') {
|
||||
opList.push(new OPS.index(state, { type: "index", index: 0 }));
|
||||
opTot += opList.peek().weight();
|
||||
|
||||
}
|
||||
} else {
|
||||
// preface op list with OpShadow
|
||||
opList.push(new OPS.shadow(state, { type: "shadow", silent: true }));
|
||||
opTot += opList.peek().weight();
|
||||
}
|
||||
|
|
@ -324,11 +310,15 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
|
||||
workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
|
||||
}
|
||||
if (valz.tool) {
|
||||
tool = new Tool(settings, valz.tool);
|
||||
}
|
||||
let { note, type } = op.op;
|
||||
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
|
||||
let layername = named.length ? named : (note ? `${type} (${note})` : type);
|
||||
Object.assign(state, {
|
||||
layername,
|
||||
tool,
|
||||
zBottom,
|
||||
zThru,
|
||||
ztOff,
|
||||
|
|
@ -336,9 +326,16 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
zTop,
|
||||
workarea: workover
|
||||
});
|
||||
let operr;
|
||||
await op.slice((progress, message) => {
|
||||
onupdate((opSum + (progress * weight)) / opTot, message || op.type());
|
||||
}).catch(e => {
|
||||
operr = e;
|
||||
console.trace(e);
|
||||
});
|
||||
if (operr) {
|
||||
return error(operr);
|
||||
}
|
||||
// update tracker rotation for next slice output() visualization
|
||||
tracker.rotation = isIndexed ? axisRotation : 0;
|
||||
camOps.push(op);
|
||||
|
|
@ -349,77 +346,12 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
|
|||
// reindex
|
||||
sliceAll.forEach((slice, index) => slice.index = index);
|
||||
|
||||
// used in printSetup()
|
||||
// used in CAM.prepare.getZClearPath()
|
||||
// add tabs to terrain tops so moves avoid them
|
||||
if (tabs) {
|
||||
state.terrain.forEach(slab => {
|
||||
tabs.forEach(tab => {
|
||||
if (tab.pos.z + tab.dim.z / 2 >= slab.z) {
|
||||
let all = [...slab.tops, tab.poly];
|
||||
slab.tops = POLY.union(all, 0, true);
|
||||
// slab.slice.output()
|
||||
// .setLayer("debug-tabs", {line: 0x880088, thin: true })
|
||||
// .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// add shadow perimeter to terrain to catch outside moves off part
|
||||
let tabpoly = tabs ? tabs.map(tab => tab.poly) : [];
|
||||
let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true);
|
||||
let shadowOff = maxToolDiam < 0 ? allpoly :
|
||||
POLY.offset(allpoly, [minToolDiam / 2, maxToolDiam / 2], { count: 2, flat: true, minArea: 0 });
|
||||
state.terrain.forEach(level => level.tops.appendAll(shadowOff));
|
||||
|
||||
widget.terrain = state.skipTerrain ? null : state.terrain;
|
||||
widget.minToolDiam = minToolDiam;
|
||||
widget.maxToolDiam = maxToolDiam;
|
||||
|
||||
ondone();
|
||||
};
|
||||
|
||||
export function addDogbones(poly, dist, reverse) {
|
||||
if (Array.isArray(poly)) {
|
||||
return poly.forEach(p => addDogbones(p, dist));
|
||||
}
|
||||
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) {
|
||||
addDogbones(poly.inner, dist, true);
|
||||
}
|
||||
};
|
||||
|
||||
export async function traces(settings, widget) {
|
||||
if (widget.traces) {
|
||||
return false;
|
||||
|
|
@ -703,7 +635,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
|
|||
for (let [i, slice] of slices.entries()) {
|
||||
for (let top of slice.tops) {
|
||||
// console.log("slicing",slice.z,top)
|
||||
slice.shadow = computeShadowAt(widget, slice.z, 0);
|
||||
slice.shadow = await widget.shadowAt(slice.z);
|
||||
let inner = top.inner;
|
||||
if (!inner) { //no holes
|
||||
continue;
|
||||
|
|
@ -903,75 +835,4 @@ function healPolys(noff, sameZ = true) {
|
|||
}
|
||||
}
|
||||
return noff;
|
||||
}
|
||||
|
||||
// union triangles > z (opt cap < ztop) into polygon(s)
|
||||
export function computeShadowAt(widget, z, ztop) {
|
||||
const geo = widget.cache.geo;
|
||||
const length = geo.length;
|
||||
// cache faces with normals up
|
||||
if (!widget.cache.shadow) {
|
||||
const faces = [];
|
||||
for (let i = 0, ip = 0; i < length; i += 3) {
|
||||
const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]);
|
||||
const n = THREE.computeFaceNormal(a, b, c);
|
||||
if (n.z > 0.001) {
|
||||
faces.push(a, b, c);
|
||||
// faces.push(newPoint(...a), newPoint(...b), newPoint(...c));
|
||||
}
|
||||
}
|
||||
widget.cache.shadow = faces;
|
||||
}
|
||||
const found = [];
|
||||
const faces = widget.cache.shadow;
|
||||
const { checkOverUnderOn, intersectPoints } = cam_slicer;
|
||||
for (let i = 0; i < faces.length;) {
|
||||
const a = faces[i++];
|
||||
const b = faces[i++];
|
||||
const c = faces[i++];
|
||||
let where = undefined;
|
||||
if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) {
|
||||
// skip faces over top threshold
|
||||
continue;
|
||||
}
|
||||
if (a.z < z && b.z < z && c.z < z) {
|
||||
// skip faces under threshold
|
||||
continue;
|
||||
} else if (a.z > z && b.z > z && c.z > z) {
|
||||
found.push([a, b, c]);
|
||||
} else {
|
||||
// check faces straddling threshold
|
||||
const where = { under: [], over: [], on: [] };
|
||||
checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where);
|
||||
checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where);
|
||||
checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where);
|
||||
if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length === 2) {
|
||||
if (where.over.length === 2) {
|
||||
found.push([where.over[1], line[0], line[1]]);
|
||||
found.push([where.over[0], where.over[1], line[0]]);
|
||||
} else {
|
||||
found.push([where.over[0], line[0], line[1]]);
|
||||
}
|
||||
} else {
|
||||
console.log({ msg: "invalid ips", line: line, where: where });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let polys = found.map(a => {
|
||||
return newPolygon()
|
||||
.add(a[0].x, a[0].y, a[0].z)
|
||||
.add(a[1].x, a[1].y, a[1].z)
|
||||
.add(a[2].x, a[2].y, a[2].z);
|
||||
});
|
||||
|
||||
polys = POLY.union(polys, 0, true);
|
||||
|
||||
return polys;
|
||||
}
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base, util } from '../../../geo/base.js';
|
||||
import { util } from '../../../geo/base.js';
|
||||
import { decode, decodePointArray } from '../../core/codec.js';
|
||||
import { newLine, newOrderedLine } from '../../../geo/line.js';
|
||||
import { newLine } from '../../../geo/line.js';
|
||||
import { newPoint } from '../../../geo/point.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { polygons as POLY } from '../../../geo/polygons.js';
|
||||
import { slicer } from '../../../geo/slicer.js';
|
||||
import {
|
||||
checkOverUnderOn,
|
||||
intersectPoints,
|
||||
makeZLine,
|
||||
removeDuplicateLines,
|
||||
sliceConnect,
|
||||
} from '../../../geo/slicer.js';
|
||||
|
||||
const { sliceConnect, sliceDedup } = slicer;
|
||||
const { config } = base;
|
||||
const timing = false;
|
||||
const zDecimal = 3;
|
||||
const epsilon = 10e-5;
|
||||
|
|
@ -232,15 +236,18 @@ export class Slicer {
|
|||
// console.log({ oneach: slice, ...track });
|
||||
}));
|
||||
}
|
||||
const data = (await Promise.all(promises)).flat();
|
||||
|
||||
data.sort((a, b) => b.z - a.z).forEach((rec, i) => {
|
||||
const data = (await Promise.all(promises)).flat();
|
||||
data.sort((a, b) => b.z - a.z);
|
||||
|
||||
for (let i=0; i<data.length; i++) {
|
||||
let rec = data[i];
|
||||
rec.tops = rec.polys;
|
||||
rec.slice = newSlice(rec.z).addTops(rec.tops);
|
||||
if (opt.each) {
|
||||
opt.each(rec, i, data.length);
|
||||
await opt.each(rec, i, data.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (threaded) minions.broadcast("cam_slice_cleanup");
|
||||
end("slicing");
|
||||
|
|
@ -339,7 +346,7 @@ export class Slicer {
|
|||
|
||||
if (dedup) {
|
||||
// console.log({ z, dedup: lines, points });
|
||||
lines = sliceDedup(lines, debug);
|
||||
lines = removeDuplicateLines(lines, debug);
|
||||
}
|
||||
|
||||
return lines.length ? {
|
||||
|
|
@ -403,78 +410,3 @@ export class Slicer {
|
|||
return array.map(v => parseFloat(v.toFixed(zDecimal)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 checkOverUnderOn(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.clone());
|
||||
p2 = getCachedPoint(phash, p2.clone());
|
||||
let line = newOrderedLine(p1, p2);
|
||||
line.coplanar = coplanar || false;
|
||||
line.edge = edge || false;
|
||||
return line;
|
||||
}
|
||||
|
||||
Slicer.checkOverUnderOn = checkOverUnderOn;
|
||||
Slicer.intersectPoints = intersectPoints;
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base } from '../../../geo/base.js';
|
||||
import { newPoint } from '../../../geo/point.js';
|
||||
import { newOrderedLine } from '../../../geo/line.js';
|
||||
|
||||
const { config } = base;
|
||||
import {
|
||||
checkOverUnderOn,
|
||||
makeZLine,
|
||||
removeDuplicateLines,
|
||||
intersectPoints
|
||||
} from '../../../geo/slicer.js';
|
||||
|
||||
/** Slicer used in Topo3 and Topo4 to find contour lines */
|
||||
export class Slicer {
|
||||
|
||||
static intersectPoints = intersectPoints;
|
||||
static checkOverUnderOn = checkOverUnderOn;
|
||||
static removeDuplicateLines = removeDuplicateLines;
|
||||
|
||||
constructor(index = 0) {
|
||||
this.min = Infinity;
|
||||
this.max = -Infinity;
|
||||
|
|
@ -143,177 +141,3 @@ export class Slicer {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 checkOverUnderOn(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, debug) {
|
||||
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;
|
||||
if (debug && (l1.del || l2.del)) {
|
||||
console.log('dup', l1, l2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// associate points with their lines, cull deleted
|
||||
lines.forEach(function (line) {
|
||||
if (!line.del) {
|
||||
tmplines.push(line);
|
||||
addLinesToPoint(line.p1, line);
|
||||
addLinesToPoint(line.p2, line);
|
||||
}
|
||||
});
|
||||
|
||||
// merge collinear lines
|
||||
points.forEach(function (point) {
|
||||
if (point.group.length != 2) return;
|
||||
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
|
||||
tmplines.forEach(function (line) {
|
||||
if (!line.del) {
|
||||
output.push(line);
|
||||
line.p1.group = null;
|
||||
line.p2.group = null;
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
const HPI = Math.PI / 2;
|
||||
const RAD2DEG = 180 / Math.PI;
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
class Tool {
|
||||
constructor(settings, id, number) {
|
||||
|
|
@ -15,6 +15,7 @@ class Tool {
|
|||
this.tool.number = number >= 0 ? number : this.tool.number;
|
||||
this.tool.id = id >= 0 ? id : this.tool.id;
|
||||
}
|
||||
this.work_units = settings.controller.units === 'mm' ? 1 : 25.4;
|
||||
}
|
||||
|
||||
getID() {
|
||||
|
|
@ -33,6 +34,12 @@ class Tool {
|
|||
return this.tool.number;
|
||||
}
|
||||
|
||||
// for taper tips, returns value in tool units
|
||||
// otherwise returns a fraction of the flute diameter in tool units
|
||||
getStepSize(frac) {
|
||||
return (this.hasTaper() ? frac : this.fluteDiameter() * (frac ?? 1)) * this.work_units;
|
||||
}
|
||||
|
||||
isMetric() {
|
||||
return this.tool.metric;
|
||||
}
|
||||
|
|
@ -66,15 +73,22 @@ class Tool {
|
|||
return diam ? diam * step : step;
|
||||
}
|
||||
|
||||
setTaperLengthFromAngle(angle) {
|
||||
const rad = (this.flute_diam - this.taper_tip) / 2;
|
||||
return this.flute_len = calcTaperLength(rad, angle);
|
||||
}
|
||||
// setTaperLengthFromAngle(angle) {
|
||||
// const rad = (this.flute_diam - this.taper_tip) / 2;
|
||||
// return this.flute_len = calcTaperLength(rad, angle);
|
||||
// }
|
||||
|
||||
getTaperAngle() {
|
||||
return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len);
|
||||
let { flute_diam, flute_len, taper_tip } = this.tool;
|
||||
return calcTaperAngle((flute_diam - taper_tip) / 2, flute_len);
|
||||
}
|
||||
|
||||
// getTaperBallExtent() {
|
||||
// let rad = this.tipDiameter() / 2;
|
||||
// let ang = this.getTaperAngle() * DEG2RAD;
|
||||
// return calcTaperBallExtent(rad, ang);
|
||||
// }
|
||||
|
||||
shaftLength() {
|
||||
return this.unitScale() * this.tool.shaft_len;
|
||||
}
|
||||
|
|
@ -102,12 +116,16 @@ class Tool {
|
|||
return this.tool.type === "tapermill";
|
||||
}
|
||||
|
||||
isTaperBall() {
|
||||
return this.tool.type === "taperball";
|
||||
}
|
||||
|
||||
isDrill() {
|
||||
return this.tool.type === "drill";
|
||||
}
|
||||
|
||||
hasTaper() {
|
||||
return this.isTaperMill();
|
||||
return this.isTaperMill() || this.isTaperBall();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -115,60 +133,77 @@ class Tool {
|
|||
* Float32Array stored in `this.profile` and `this.profileDim` containing
|
||||
* dimensions of the profile in pixels and shared array buffer size.
|
||||
*
|
||||
* @param {number} resolution - number of pixels for the tool profile
|
||||
* @param {number} resolution - pixel size in mm for the raster tool profile
|
||||
*
|
||||
* @return {Tool} this
|
||||
*/
|
||||
generateProfile(resolution) {
|
||||
// generate tool profile
|
||||
let ball = this.isBallMill(),
|
||||
taperball = this.isTaperBall(),
|
||||
taper = this.hasTaper(),
|
||||
drill = this.isDrill(),
|
||||
tip_diameter = this.tipDiameter(),
|
||||
drill_tip_length = this.drillTipLength(),
|
||||
shaft_offset = this.fluteLength(),
|
||||
flute_diameter = this.fluteDiameter(),
|
||||
shaft_diameter = Math.max(flute_diameter, this.shaftDiameter()),
|
||||
max_diameter = Math.max(flute_diameter, shaft_diameter),
|
||||
shaft_pix_float = max_diameter / resolution,
|
||||
shaft_pix_int = Math.round(shaft_pix_float),
|
||||
shaft_radius_pix_float = shaft_pix_float / 2,
|
||||
flute_radius = flute_diameter / 2,
|
||||
flute_pix_float = flute_diameter / resolution,
|
||||
flute_radius_pix_float = flute_pix_float / 2,
|
||||
tip_pix_float = tip_diameter / resolution,
|
||||
tip_radius_pix_float = tip_pix_float / 2,
|
||||
tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float,
|
||||
profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2),
|
||||
toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2,
|
||||
tip_dia = this.tipDiameter(),
|
||||
flute_dia = this.fluteDiameter(),
|
||||
flute_len = this.fluteLength(),
|
||||
flute_rad = flute_dia / 2,
|
||||
shaft_dia = Math.max(flute_dia, this.shaftDiameter()),
|
||||
max_dia = Math.max(flute_dia, shaft_dia),
|
||||
larger_shaft = shaft_dia - flute_dia > 0.001,
|
||||
pix_sh_dia_float = max_dia / resolution,
|
||||
pix_sh_dia_int = Math.round(pix_sh_dia_float),
|
||||
pix_sh_rad_float = pix_sh_dia_float / 2,
|
||||
pix_fl_dia_float = flute_dia / resolution,
|
||||
pix_fl_rad_float = pix_fl_dia_float / 2,
|
||||
pix_tip_dia_float = tip_dia / resolution,
|
||||
pix_tip_rad_float = pix_tip_dia_float / 2,
|
||||
tip_max_rad_offset = pix_fl_rad_float - pix_tip_rad_float,
|
||||
pix_profile_iter = pix_sh_dia_int + (1 - pix_sh_dia_int % 2),
|
||||
toolCenter = (pix_sh_dia_int - (pix_sh_dia_int % 2)) / 2,
|
||||
toolOffset = [],
|
||||
larger_shaft = shaft_diameter - flute_diameter > 0.001,
|
||||
rpixsq = flute_radius_pix_float * flute_radius_pix_float,
|
||||
maxo = -Infinity;
|
||||
maxo = -Infinity,
|
||||
// ball taper magic
|
||||
a = this.getTaperAngle() * DEG2RAD,
|
||||
r = (tip_dia/2) * (1 + Math.sin(a)) / Math.cos(a),
|
||||
b = r * Math.cos(a),
|
||||
pix_b = b / resolution,
|
||||
pix_r = r / resolution;
|
||||
|
||||
// for each point in tool profile, check inside radius
|
||||
for (let x = 0; x < profile_pix_iter; x++) {
|
||||
for (let y = 0; y < profile_pix_iter; y++) {
|
||||
for (let x = 0; x < pix_profile_iter; x++) {
|
||||
for (let y = 0; y < pix_profile_iter; y++) {
|
||||
let dx = x - toolCenter,
|
||||
dy = y - toolCenter,
|
||||
dist_from_center = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist_from_center <= flute_radius_pix_float) { // if xy point inside flute radius
|
||||
if (dist_from_center <= pix_fl_rad_float) { // if xy point inside flute radius
|
||||
maxo = Math.max(maxo, dx, dy);
|
||||
// flute offset points
|
||||
let z_offset = 0;
|
||||
if (ball) {
|
||||
let rd = dist_from_center * dist_from_center;
|
||||
z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius;
|
||||
// z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius;
|
||||
} else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius
|
||||
z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset;
|
||||
let ball_rad_sq = dist_from_center * dist_from_center;
|
||||
let ball_rpixsq = pix_fl_rad_float * pix_fl_rad_float;
|
||||
z_offset = Math.sqrt(ball_rpixsq - ball_rad_sq) * resolution - flute_rad;
|
||||
} else if (taperball) {
|
||||
// taperball: spherical tip, conical above
|
||||
if (dist_from_center <= pix_b) {
|
||||
// inside ball radius - spherical surface
|
||||
let ball_rad_sq = dist_from_center * dist_from_center;
|
||||
let ball_rpixsq = pix_r * pix_r;
|
||||
z_offset = Math.sqrt(ball_rpixsq - ball_rad_sq) * resolution - r;
|
||||
} else {
|
||||
// outside ball radius - conical taper
|
||||
z_offset = ((dist_from_center - pix_tip_rad_float) / tip_max_rad_offset) * -flute_len;
|
||||
}
|
||||
} else if (taper && dist_from_center >= pix_tip_rad_float) {
|
||||
// if tapered and not in the flat tip radius
|
||||
z_offset = ((dist_from_center - pix_tip_rad_float) / tip_max_rad_offset) * -flute_len;
|
||||
} else if (drill) {
|
||||
z_offset = -dist_from_center / 45;
|
||||
}
|
||||
toolOffset.push(dx, dy, z_offset);
|
||||
} else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) {
|
||||
} else if (flute_len && larger_shaft && dist_from_center <= pix_sh_rad_float) {
|
||||
// shaft offset points
|
||||
toolOffset.push(dx, dy, -shaft_offset);
|
||||
toolOffset.push(dx, dy, -flute_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -179,8 +214,8 @@ class Tool {
|
|||
|
||||
this.profile = profile;
|
||||
this.profileDim = {
|
||||
size: shaft_diameter,
|
||||
pix: profile_pix_iter + 2,
|
||||
size: shaft_dia,
|
||||
pix: pix_profile_iter + 2,
|
||||
maxo
|
||||
};
|
||||
|
||||
|
|
@ -190,19 +225,24 @@ class Tool {
|
|||
|
||||
function calcTaperAngle(rad, len) {
|
||||
return (Math.atan(rad / len) * RAD2DEG);
|
||||
};
|
||||
}
|
||||
|
||||
function calcTaperLength(rad, angle) {
|
||||
return (rad / Math.tan(angle));
|
||||
};
|
||||
}
|
||||
|
||||
function calcTaperBallExtent(rad, angle) {
|
||||
return rad * (1 - Math.sin(angle * 2));
|
||||
}
|
||||
|
||||
function getToolDiameter(settings, id) {
|
||||
return new Tool(settings, id).fluteDiameter();
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
Tool,
|
||||
calcTaperAngle,
|
||||
calcTaperBallExtent,
|
||||
calcTaperLength,
|
||||
getToolDiameter
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { $ } from '../../../moto/webui.js';
|
|||
import { api } from '../../core/api.js';
|
||||
import { consts } from '../../core/consts.js';
|
||||
import { settings as setconf } from '../../core/settings.js';
|
||||
import { calcTaperLength, calcTaperAngle } from './tool.js';
|
||||
import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js';
|
||||
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
|
|
@ -12,8 +12,7 @@ let { MODES } = consts,
|
|||
DOC = document,
|
||||
selectedTool = null,
|
||||
editTools = null,
|
||||
maxTool = 0,
|
||||
toolNames = ['endmill','ballmill','tapermill','drill'];
|
||||
maxTool = 0;
|
||||
|
||||
function settings() {
|
||||
return api.conf.get();
|
||||
|
|
@ -35,27 +34,110 @@ function renderTools() {
|
|||
function selectTool(tool) {
|
||||
const { ui } = api;
|
||||
selectedTool = tool;
|
||||
ui.toolName.value = tool.name;
|
||||
ui.toolNum.value = tool.number;
|
||||
ui.toolFluteDiam.value = tool.flute_diam;
|
||||
ui.toolFluteLen.value = tool.flute_len;
|
||||
ui.toolShaftDiam.value = tool.shaft_diam;
|
||||
ui.toolShaftLen.value = tool.shaft_len;
|
||||
ui.toolTaperTip.value = tool.taper_tip || 0;
|
||||
ui.toolMetric.checked = tool.metric;
|
||||
ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
|
||||
if (tool === 'tapermill') {
|
||||
ui.toolTaperAngle.value = calcTaperAngle(
|
||||
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
|
||||
).round(1);
|
||||
} else if(tool === 'drill'){
|
||||
ui.toolTaperAngle.value = 118;
|
||||
|
||||
api.util.rec2ui({
|
||||
toolName: selectedTool.name,
|
||||
toolType: selectedTool.type,
|
||||
toolNum: selectedTool.number,
|
||||
toolMetric: selectedTool.metric,
|
||||
toolShaftDiam: selectedTool.shaft_diam,
|
||||
toolShaftLen: selectedTool.shaft_len,
|
||||
toolFluteDiam: selectedTool.flute_diam,
|
||||
toolFluteLen: selectedTool.flute_len,
|
||||
toolTaperAngle: selectedTool.taper_angle,
|
||||
toolTaperTip: selectedTool.taper_tip,
|
||||
},{
|
||||
toolName: ui.toolName,
|
||||
toolType: ui.toolType,
|
||||
toolNum: ui.toolNum,
|
||||
toolMetric: ui.toolMetric,
|
||||
toolShaftDiam: ui.toolShaftDiam,
|
||||
toolShaftLen: ui.toolShaftLen,
|
||||
toolFluteDiam: ui.toolFluteDiam,
|
||||
toolFluteLen: ui.toolFluteLen,
|
||||
toolTaperAngle: ui.toolTaperAngle,
|
||||
toolTaperTip: ui.toolTaperTip,
|
||||
});
|
||||
|
||||
if (tool.type === 'tapermill' || tool.type === 'taperball') {
|
||||
const taperLen = tool.flute_len;
|
||||
ui.toolTaperAngle.value =
|
||||
selectedTool.taper_angle =
|
||||
calcTaperAngle( (tool.flute_diam - tool.taper_tip) / 2, taperLen ).round(1);
|
||||
} else if (tool === 'drill') {
|
||||
ui.toolTaperAngle.value = selectedTool.taper_angle = 90;
|
||||
} else {
|
||||
ui.toolTaperAngle.value = 0;
|
||||
ui.toolTaperAngle.value = selectedTool.taper_angle = 0;
|
||||
}
|
||||
|
||||
renderTool(tool);
|
||||
}
|
||||
|
||||
export function updateTool(ev) {
|
||||
const { ui } = api;
|
||||
|
||||
let changed = {
|
||||
toolName: selectedTool.name,
|
||||
toolType: selectedTool.type,
|
||||
toolNum: selectedTool.number,
|
||||
toolMetric: selectedTool.metric,
|
||||
toolShaftDiam: selectedTool.shaft_diam,
|
||||
toolShaftLen: selectedTool.shaft_len,
|
||||
toolFluteDiam: selectedTool.flute_diam,
|
||||
toolFluteLen: selectedTool.flute_len,
|
||||
toolTaperAngle: selectedTool.taper_angle,
|
||||
toolTaperTip: selectedTool.taper_tip,
|
||||
};
|
||||
|
||||
api.util.ui2rec(changed,{
|
||||
toolName: ui.toolName,
|
||||
toolType: ui.toolType,
|
||||
toolNum: ui.toolNum,
|
||||
toolMetric: ui.toolMetric,
|
||||
toolShaftDiam: ui.toolShaftDiam,
|
||||
toolShaftLen: ui.toolShaftLen,
|
||||
toolFluteDiam: ui.toolFluteDiam,
|
||||
toolFluteLen: ui.toolFluteLen,
|
||||
toolTaperAngle: ui.toolTaperAngle,
|
||||
toolTaperTip: ui.toolTaperTip,
|
||||
});
|
||||
|
||||
selectedTool.name = changed.toolName;
|
||||
selectedTool.type = changed.toolType;
|
||||
selectedTool.number = changed.toolNum;
|
||||
selectedTool.metric = changed.toolMetric;
|
||||
selectedTool.shaft_diam = changed.toolShaftDiam;
|
||||
selectedTool.shaft_len = changed.toolShaftLen;
|
||||
selectedTool.flute_diam = changed.toolFluteDiam;
|
||||
selectedTool.flute_len = changed.toolFluteLen;
|
||||
selectedTool.taper_angle = changed.toolTaperAngle;
|
||||
selectedTool.taper_tip = changed.toolTaperTip;
|
||||
|
||||
if (selectedTool.type === 'tapermill' || selectedTool.type === 'taperball') {
|
||||
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
|
||||
const ballRadius = selectedTool.type === 'taperball' ? selectedTool.taper_tip / 2 : 0;
|
||||
|
||||
if (ev && ev.target === ui.toolTaperAngle) {
|
||||
const angle = parseFloat(ev.target.value || 5);
|
||||
const len = calcTaperLength(rad, angle * DEG2RAD);
|
||||
selectedTool.flute_len = len + ballRadius;
|
||||
ui.toolTaperAngle.value = angle.round(1);
|
||||
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
|
||||
} else {
|
||||
const taperLen = selectedTool.flute_len - ballRadius;
|
||||
ui.toolTaperAngle.value =
|
||||
selectedTool.taper_angle =
|
||||
calcTaperAngle(rad, taperLen).round(1);
|
||||
}
|
||||
} else {
|
||||
ui.toolTaperAngle.value = selectedTool.taper_angle = 0;
|
||||
}
|
||||
|
||||
renderTools();
|
||||
setToolChanged(true);
|
||||
renderTool(selectedTool);
|
||||
}
|
||||
|
||||
function otag(o) {
|
||||
if (Array.isArray(o)) {
|
||||
let out = []
|
||||
|
|
@ -77,157 +159,89 @@ function otag(o) {
|
|||
|
||||
function renderTool(tool) {
|
||||
const { ui } = api;
|
||||
let type = selectedTool.type;
|
||||
let taper = type=== 'tapermill'
|
||||
let drill = type === 'drill'
|
||||
const drillAngleRad = 140 * Math.PI / 180
|
||||
ui.toolTaperAngle.disabled = taper ? undefined : 'true';
|
||||
ui.toolTaperTip.disabled = taper ? undefined : 'true';
|
||||
$('tool-view').innerHTML = '<svg id="tool-svg" width="100%" height="100%"></svg>';
|
||||
setTimeout(() => {
|
||||
let svg = $('tool-svg'),
|
||||
pad = 10,
|
||||
dim = { w: svg.clientWidth, h: svg.clientHeight },
|
||||
max = { w: dim.w - pad * 2, h: dim.h - pad * 2},
|
||||
off = { x: pad, y: pad },
|
||||
isBall = type === "ballmill",
|
||||
shaft_fill = "#cccccc",
|
||||
flute_fill = "#dddddd",
|
||||
stroke = "#777777",
|
||||
stroke_width = 3,
|
||||
stroke_thin = stroke_width / 2,
|
||||
shaft = tool.shaft_len || 1,
|
||||
flute = tool.flute_len || 1,
|
||||
drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0,
|
||||
total_len = shaft + flute+drillTip,
|
||||
units = dim.h / total_len,
|
||||
shaft_len = (shaft / total_len) * max.h,
|
||||
flute_len = (flute / total_len) * max.h,
|
||||
drill_tip_len = (drillTip / total_len) * max.h,
|
||||
shaft_diam = tool.shaft_diam * units,
|
||||
flute_diam = tool.flute_diam * units,
|
||||
// total_wid = Math.max(flute_diam, shaft_diam),
|
||||
shaft_off = (max.w - shaft_diam) / 2,
|
||||
flute_off = (max.w - flute_diam) / 2,
|
||||
taper_off = (max.w - (tool.taper_tip || 0) * units) / 2,
|
||||
parts = [
|
||||
// shaft rectangle
|
||||
{ rect: {
|
||||
x: off.x + shaft_off,
|
||||
y: off.y,
|
||||
width: max.w - shaft_off * 2,
|
||||
height: shaft_len,
|
||||
fill: shaft_fill,
|
||||
stroke_width,
|
||||
stroke
|
||||
} }
|
||||
];
|
||||
if (taper) {
|
||||
let yoff = off.y + shaft_len;
|
||||
// let mid = dim.w / 2;
|
||||
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
|
||||
`M ${off.x + flute_off} ${yoff}`,
|
||||
`L ${off.x + taper_off} ${yoff + flute_len}`,
|
||||
`L ${dim.w - off.x - taper_off} ${yoff + flute_len}`,
|
||||
`L ${dim.w - off.x - flute_off} ${yoff}`,
|
||||
`z`
|
||||
].join('\n')}});
|
||||
} else if(drill){
|
||||
const x1 = off.x + flute_off,
|
||||
y1 = off.y + shaft_len,
|
||||
x2 = dim.w - off.x - flute_off,
|
||||
y2 = y1 + flute_len,
|
||||
xMid = dim.w / 2
|
||||
let svg = $('tool-svg');
|
||||
let pad = 10;
|
||||
let dim = { w: svg.clientWidth, h: svg.clientHeight };
|
||||
let max = { w: dim.w - pad * 2, h: dim.h - pad * 2 };
|
||||
let off = { x: pad, y: pad };
|
||||
|
||||
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
|
||||
`M ${x1} ${y1}`, //move to top left
|
||||
`L ${x1} ${y2}`, //line to bottom left
|
||||
`L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point
|
||||
`L ${x2} ${y2}`, //line to bottom right
|
||||
`L ${x2} ${y1}`, //line to top right
|
||||
`z`
|
||||
].join('\n')}});
|
||||
//add drill flute lines
|
||||
parts.push({ line: {
|
||||
x1, y1, x2, y2: (y1 + y2) / 2,
|
||||
stroke, stroke_width: stroke_thin
|
||||
} });
|
||||
parts.push({ line: {
|
||||
x1, y1: (y1 + y2) / 2, x2, y2,
|
||||
stroke, stroke_width: stroke_thin
|
||||
} });
|
||||
} else {
|
||||
let fl = isBall ? flute_len - flute_diam/2 : flute_len;
|
||||
let x1 = off.x + flute_off;
|
||||
let y1 = off.y + shaft_len;
|
||||
let x2 = x1 + max.w - flute_off * 2;
|
||||
let y2 = y1 + fl;
|
||||
// flute rectangle
|
||||
parts.push({ rect: {
|
||||
x: off.x + flute_off,
|
||||
y: off.y + shaft_len,
|
||||
width: max.w - flute_off * 2,
|
||||
height: fl,
|
||||
fill: flute_fill,
|
||||
stroke_width,
|
||||
stroke,
|
||||
} });
|
||||
// hatch "fill" flute
|
||||
parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } });
|
||||
parts.push({ line: {
|
||||
x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2,
|
||||
stroke, stroke_width: stroke_thin
|
||||
} });
|
||||
parts.push({ line: {
|
||||
x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2,
|
||||
stroke, stroke_width: stroke_thin
|
||||
} });
|
||||
}
|
||||
if (isBall) {
|
||||
let rad = (max.w - flute_off * 2) / 2;
|
||||
let xend = dim.w - off.x - flute_off;
|
||||
let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2;
|
||||
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[
|
||||
`M ${off.x + flute_off} ${yoff}`,
|
||||
`A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`,
|
||||
// `L ${off.x + flute_off} ${yoff}`
|
||||
].join('\n')}})
|
||||
// Create Tool instance and generate profile
|
||||
const toolInst = new Tool(settings(), tool.id);
|
||||
const resolution = (toolInst.maxDiameter() / toolInst.unitScale()) / 100;
|
||||
toolInst.generateProfile(resolution);
|
||||
|
||||
const profile = toolInst.profile;
|
||||
const { pix } = toolInst.profileDim;
|
||||
|
||||
// Extract cross-section at y=0 (center line)
|
||||
// Profile is stored as [dx, dy, z_offset, dx, dy, z_offset, ...]
|
||||
let crossSection = [];
|
||||
for (let i = 0; i < profile.length; i += 3) {
|
||||
let dx = profile[i];
|
||||
let dy = profile[i + 1];
|
||||
let z = profile[i + 2];
|
||||
// Only take points where dy is approximately 0 (center line)
|
||||
if (Math.abs(dy) < 0.01) {
|
||||
crossSection.push({ x: dx * resolution, z });
|
||||
}
|
||||
}
|
||||
|
||||
// Section lengths
|
||||
let slen = toolInst.shaftLength();
|
||||
let flen = toolInst.fluteLength();
|
||||
|
||||
// Find bounds
|
||||
let minZ = Math.min(...crossSection.map(p => p.z), -(slen + flen));
|
||||
let maxZ = Math.max(...crossSection.map(p => p.z), 0);
|
||||
let minX = Math.min(...crossSection.map(p => p.x));
|
||||
let maxX = Math.max(...crossSection.map(p => p.x));
|
||||
let zRange = maxZ - minZ;
|
||||
let xRange = maxX - minX;
|
||||
|
||||
// Scale to fit
|
||||
let scale = Math.min(max.h / zRange, max.w / xRange);
|
||||
|
||||
// Center horizontally if tool is narrower than viewport
|
||||
let xOffset = off.x + (max.w - xRange * scale) / 2;
|
||||
|
||||
// Draw vertical lines for each point
|
||||
let parts = [];
|
||||
let stroke_width = 1;
|
||||
|
||||
crossSection.forEach(p => {
|
||||
let x = xOffset + (p.x - minX) * scale;
|
||||
let y1 = dim.h - off.y - (maxZ - p.z) * scale; // tip point
|
||||
let y2 = dim.h - off.y - (flen) * scale; // top flute
|
||||
let y3 = dim.h - off.y - (slen + flen) * scale; // top shaft
|
||||
|
||||
// flute
|
||||
parts.push({ line: {
|
||||
x1: x,
|
||||
x2: x,
|
||||
y1: y1,
|
||||
y2: y2,
|
||||
stroke: "#999999",
|
||||
stroke_width
|
||||
}});
|
||||
|
||||
// shaft
|
||||
parts.push({ line: {
|
||||
x1: x,
|
||||
x2: x,
|
||||
y1: y2,
|
||||
y2: y3,
|
||||
stroke: "#666666",
|
||||
stroke_width
|
||||
}});
|
||||
|
||||
});
|
||||
|
||||
svg.innerHTML = otag(parts);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
export function updateTool(ev) {
|
||||
const { ui } = api;
|
||||
selectedTool.name = ui.toolName.value;
|
||||
selectedTool.number = parseInt(ui.toolNum.value);
|
||||
selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value);
|
||||
selectedTool.flute_len = parseFloat(ui.toolFluteLen.value);
|
||||
selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value);
|
||||
selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value);
|
||||
selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value);
|
||||
selectedTool.metric = ui.toolMetric.checked;
|
||||
selectedTool.type = toolNames[ui.toolType.selectedIndex];
|
||||
if (selectedTool.type === 'tapermill') {
|
||||
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
|
||||
if (ev && ev.target === ui.toolTaperAngle) {
|
||||
const angle = parseFloat(ev.target.value);
|
||||
const len = calcTaperLength(rad, angle * DEG2RAD);
|
||||
selectedTool.flute_len = len;
|
||||
ui.toolTaperAngle.value = angle.round(1);
|
||||
ui.toolFluteLen.value = selectedTool.flute_len.round(4);
|
||||
} else {
|
||||
ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1);
|
||||
}
|
||||
} else {
|
||||
ui.toolTaperAngle.value = 0;
|
||||
}
|
||||
renderTools();
|
||||
ui.toolSelect.selectedIndex = selectedTool.order;
|
||||
setToolChanged(true);
|
||||
renderTool(selectedTool);
|
||||
}
|
||||
|
||||
function setToolChanged(changed) {
|
||||
editTools.changed = changed;
|
||||
api.ui.toolsSave.disabled = !changed;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { doTopShells } from '../mode/fdm/post.js';
|
|||
import { newPoint } from '../../geo/point.js';
|
||||
import { polygons as POLY } from '../../geo/polygons.js';
|
||||
import { sliceZ, sliceConnect } from '../../geo/slicer.js';
|
||||
import { Slicer as cam_slicer } from '../mode/cam/slicer.js';
|
||||
import { Slicer as cam_slicer } from '../mode/cam/slicer_cam.js';
|
||||
import { Slicer as topo_slicer } from '../mode/cam/slicer_topo.js';
|
||||
import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js';
|
||||
import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js';
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
}
|
||||
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
mode: map.mode,
|
||||
clear: map.clear,
|
||||
disable: map.disable,
|
||||
version: map.version || version
|
||||
|
|
|
|||
|
|
@ -142,9 +142,9 @@ async function _fetch(e) {
|
|||
}
|
||||
|
||||
if (url.pathname.endsWith("/")) {
|
||||
return e.respondWith(redirectOr404(appendURL(url, 'index.html').pathname));
|
||||
return e.respondWith(redirectToUrl(appendURL(url, 'index.html').pathname, request));
|
||||
} else if (url.pathname.indexOf(".") < 0 || url.pathname.endsWith("/boot")) {
|
||||
return e.respondWith(redirectOr404(appendURL(url, '/index.html').pathname));
|
||||
return e.respondWith(redirectToUrl(appendURL(url, '/index.html').pathname, request));
|
||||
} else {
|
||||
e.respondWith(fromCacheOrNetwork(request));
|
||||
}
|
||||
|
|
@ -155,16 +155,9 @@ function appendURL(url, append) {
|
|||
return new URL(url.origin + url.pathname + append + url.search);
|
||||
}
|
||||
|
||||
async function redirectOr404(path) {
|
||||
const cache = await cacheOpen;
|
||||
const hit = await cache.match(path, { ignoreSearch: true });
|
||||
if (hit) {
|
||||
if (debug) log('REDIRECT', path);
|
||||
return Response.redirect(path, 302);
|
||||
} else {
|
||||
if (debug) log('404', path);
|
||||
return new Response('Not Found', { status: 404, statusText: 'Not Found' });
|
||||
}
|
||||
async function redirectToUrl(path, request) {
|
||||
if (debug) log('REDIRECT', path);
|
||||
return Response.redirect(path, 302);
|
||||
}
|
||||
|
||||
async function fromCacheOrNetwork(req) {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,9 @@ th, tr, td, label {
|
|||
white-space: nowrap;
|
||||
}
|
||||
details summary {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
|
|
@ -438,6 +440,9 @@ details[open] summary::after {
|
|||
.dark .widopt {
|
||||
background-color: #222;
|
||||
}
|
||||
.dark #camops summary {
|
||||
background-color: var(--blue-0);
|
||||
}
|
||||
|
||||
/* top menu bar, drop menus */
|
||||
#top, #app-name {
|
||||
|
|
@ -1400,6 +1405,9 @@ details[open] summary::after {
|
|||
#camops .set-sep {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
#camops summary {
|
||||
background-color: var(--blue-4);
|
||||
}
|
||||
#oplist .clock:hover {
|
||||
background-color: var(--blue-2);
|
||||
}
|
||||
|
|
@ -2425,14 +2433,14 @@ details[open] summary::after {
|
|||
}
|
||||
.var-row input {
|
||||
/* flex: 1 1 auto; */
|
||||
min-width: 0;
|
||||
max-width: 7ch;
|
||||
min-width: 0;
|
||||
max-width: 7ch;
|
||||
margin-right: 0;
|
||||
margin-bottom: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.var-row #tool-name{
|
||||
.var-row #tool-name {
|
||||
max-width: 15ch;
|
||||
}
|
||||
.var-row button {
|
||||
|
|
|
|||
|
|
@ -452,16 +452,16 @@
|
|||
<div id="laser-off" title="turn off laser mode">laser off</div>
|
||||
<div id="cam-flip" title="flip part and load profile for other side for double-sided work">flip</div>
|
||||
<div id="cam-reg" title="drill registration holes along the X or Y axis for double-sided work">register</div>
|
||||
<div title="drill any holes matching selected tool diameter">drill</div>
|
||||
<div title="clear stock face or top of part when no stock">level</div>
|
||||
<div title="follow selected features with a specified tool. often used for lettering and engraving">trace</div>
|
||||
<div title="drill selected holes">drill</div>
|
||||
<div title="clear stock face">level</div>
|
||||
<div title="follow a downward helix using conical selections">helical</div>
|
||||
<div title="insert custom gcode">gcode</div>
|
||||
<div title="remove an area of material inside a defined boundary sometimes called pocketing">rough</div>
|
||||
<div title="clean up after roughing or perform a part cutout">outline</div>
|
||||
<div title="2.5D tracing along X or Y axis used for complex part features">contour</div>
|
||||
<div title="clear areas above selected surfaces">pocket</div>
|
||||
<div title="follow a helix around circular paths">helical</div>
|
||||
<div title="perform configurable operations on selected areas" id="op:area" class="hide">area</div>
|
||||
<div title="follow selected features. often used for lettering and engraving">trace</div>
|
||||
<div title="clear areas above selected surfaces">pocket</div>
|
||||
<div title="remove an area of material inside a defined boundary sometimes called pocketing">rough</div>
|
||||
<div title="cutout parts using their shadow outline">outline</div>
|
||||
<div title="perform operations on using outlines or surfaces many operations are simple wrappers around this">area</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -572,34 +572,13 @@
|
|||
<div class="f-row t-head gap3">
|
||||
<label class="t-33 set-header" lk="tool">tool</label>
|
||||
<label class="t-33 set-header" lk="detail">detail</label>
|
||||
<label class="t-33 set-header" lk="view">view</label>
|
||||
<label class="t-33 set-header" lk="preview">preview</label>
|
||||
</div>
|
||||
<div id="tool-cols" class="f-row gap3">
|
||||
<div class="f-col t-33">
|
||||
<select id="tool-select" size="10" class="grow"></select>
|
||||
</div>
|
||||
<div class="f-col t-33 t-body t-inset">
|
||||
<div class="f-row var-row"><label lk="name">name</label><input id="tool-name" ></input></div>
|
||||
<div class="f-row var-row"><label lk="type">type</label>
|
||||
<select id="tool-type">
|
||||
<option value="endmill" lk="td_tyem" selected>end</option>
|
||||
<option value="ballmill" lk="td_tybm">ball</option>
|
||||
<option value="tapermill" lk="td_tytm">taper</option>
|
||||
<option value="drill" lk="td_tydr">drill</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="f-row var-row" title="tool number to use
in gcode commands"><label lk="td_tonm">tool #</label><input id="tool-num" ></input></div>
|
||||
<div class="f-row var-row"><label lk="metric">metric</label><input id="tool-metric" type="checkbox"></input></div>
|
||||
<div class="set-header f-col" lk="td_shft">shaft</div>
|
||||
<div class="f-row var-row" title="shaft diameter in inches
unless metric is checked
then in millimeters"><label>diameter</label><input id="tool-sdiam" ></input></div>
|
||||
<div class="f-row var-row" title="shaft length in inches
unless metric is checked
then in millimeters"><label>length</label><input id="tool-slen" ></input></div>
|
||||
<div class="set-header f-col" lk="td_flut">flute</div>
|
||||
<div class="f-row var-row" title="flute diameter in inches
unless metric is checked
then in millimeters"><label>diameter</label><input id="tool-fdiam" ></input></div>
|
||||
<div class="f-row var-row" title="flute length in inches
unless metric is checked
then in millimeters"><label>length</label><input id="tool-flen" ></input></div>
|
||||
<div class="set-header f-col" ln="td_tapr">taper</div>
|
||||
<div class="f-row var-row" title="taper angle is measured from the center of the tool"><label>angle</label><input id="tool-tangle" size=7></input></div>
|
||||
<div class="f-row var-row" title="tip width in inches
unless metric is checked
then in millimeters"><label>tip</label><input id="tool-ttip" ></input></div>
|
||||
</div>
|
||||
<div id="tool-details" class="f-col t-33 t-body t-inset"></div>
|
||||
<div id="tool-view" class="f-col t-33 t-body"></div>
|
||||
</div>
|
||||
<div class="set-sep"></div>
|
||||
|
|
|
|||
|
|
@ -513,10 +513,10 @@ self.lang['en-us'] = {
|
|||
|
||||
// CNC COMMON
|
||||
cc_menu: "limits",
|
||||
cc_rapd_s: "xy feed",
|
||||
cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"],
|
||||
cc_rzpd_s: "z feed",
|
||||
cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"],
|
||||
cc_rapd_s: "feed rate",
|
||||
cc_rapd_l: ["max xy cutting feedrate","in workspace units / minute"],
|
||||
cc_rzpd_s: "plunge rate",
|
||||
cc_rzpd_l: ["max z cutting plunge rate","in workspace units / minute"],
|
||||
|
||||
// CNC LEVELING
|
||||
cc_loff_s: "z offset",
|
||||
|
|
@ -538,10 +538,6 @@ self.lang['en-us'] = {
|
|||
cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"],
|
||||
cr_clrt_s: "clear top",
|
||||
cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"],
|
||||
cr_clrp_s: "clear voids",
|
||||
cr_clrp_l: ["mill out through pockets","instead of just the outline"],
|
||||
cr_clrf_s: "clear faces",
|
||||
cr_clrf_l: ["interpolate step down to","clear any detected flat areas"],
|
||||
cr_olin_s: "inside only",
|
||||
cr_olin_l: ["limit cutting to","inside part boundaries"],
|
||||
|
||||
|
|
@ -553,18 +549,10 @@ self.lang['en-us'] = {
|
|||
co_dogb_l: ["insert dogbone cuts","into inside corners"],
|
||||
co_dogr_s: "reverse bones",
|
||||
co_dogr_l: ["reverse dogbone direction"],
|
||||
co_clrt_s: "clear top",
|
||||
co_clrt_l: ["cut starting at the top of stock","when stock is enabled"],
|
||||
co_wide_s: "wide cutout",
|
||||
co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"],
|
||||
co_olin_s: "inside only",
|
||||
co_olin_l: ["limit cutting to","inside part boundaries"],
|
||||
co_olot_s: "outside only",
|
||||
co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"],
|
||||
co_omit_s: "omit through",
|
||||
co_omit_l: "eliminate thru holes",
|
||||
co_omvd_s: "omit pocket",
|
||||
co_omvd_l: "eliminate interior pockets",
|
||||
co_olen_s: "enable",
|
||||
co_olen_l: "enabled outline cutting",
|
||||
|
||||
|
|
@ -601,8 +589,6 @@ self.lang['en-us'] = {
|
|||
cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."],
|
||||
cp_cont_s: "contour",
|
||||
cp_cont_l: ["ignore interior voids and features"],
|
||||
cp_engr_s: "engrave",
|
||||
cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"],
|
||||
cp_outl_s: "outline only",
|
||||
cp_outl_l: ["ignore interior voids and features"],
|
||||
|
||||
|
|
@ -828,6 +814,8 @@ self.lang['en-us'] = {
|
|||
ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.",
|
||||
ou_eang_s: "ease angle",
|
||||
ou_eang_l: "descent angle for ease down in degrees",
|
||||
ou_dire_s: "milling",
|
||||
ou_dire_l: ["milling direction","climb, conventional, alternating"],
|
||||
|
||||
// CAM STOCK
|
||||
cs_menu: "stock",
|
||||
|
|
|
|||
Loading…
Reference in a new issue