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:
Stewart Allen 2025-12-06 00:09:17 -05:00
commit e0b1c4e31f
44 changed files with 1607 additions and 2894 deletions

View file

@ -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
}

View file

@ -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) {

View file

@ -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 };

View file

@ -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
}

View file

@ -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,

View file

@ -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" },
]
};

View file

@ -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'),

View file

@ -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) {

View file

@ -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();

View file

@ -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);

View file

@ -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

View file

@ -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;

View file

@ -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)

View file

@ -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]
}
}

View file

@ -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(),
};

View file

@ -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);

View file

@ -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,

View file

@ -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 }),
};
};

View file

@ -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;

View file

@ -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();
});
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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"]);
}
}

View file

@ -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) => {

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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
};

View file

@ -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;

View file

@ -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';

View file

@ -43,6 +43,7 @@
}
navigator.serviceWorker.controller.postMessage({
mode: map.mode,
clear: map.clear,
disable: map.disable,
version: map.version || version

View file

@ -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) {

View file

@ -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 {

View file

@ -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&#13;profile for other side&#13;for double-sided work">flip</div>
<div id="cam-reg" title="drill registration holes&#13;along the X or Y axis&#13;for double-sided work">register</div>
<div title="drill any holes matching&#13;selected tool diameter">drill</div>
<div title="clear stock face&#13;or top of part&#13;when no stock">level</div>
<div title="follow selected features with&#13;a specified tool. often used&#13;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&#13;using conical selections">helical</div>
<div title="insert custom gcode">gcode</div>
<div title="remove an area of material&#13;inside a defined boundary&#13;sometimes called pocketing">rough</div>
<div title="clean up after roughing&#13;or perform a part cutout">outline</div>
<div title="2.5D tracing along X or Y axis&#13;used for complex part features">contour</div>
<div title="clear areas above&#13;selected surfaces">pocket</div>
<div title="follow a helix&#13;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&#13;for lettering and engraving">trace</div>
<div title="clear areas above selected surfaces">pocket</div>
<div title="remove an area of material&#13;inside a defined boundary&#13;sometimes called pocketing">rough</div>
<div title="cutout parts using their shadow outline">outline</div>
<div title="perform operations on using outlines or surfaces&#13;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&#010;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&#010;unless metric is checked&#010;then in millimeters"><label>diameter</label><input id="tool-sdiam" ></input></div>
<div class="f-row var-row" title="shaft length in inches&#010;unless metric is checked&#010;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&#010;unless metric is checked&#010;then in millimeters"><label>diameter</label><input id="tool-fdiam" ></input></div>
<div class="f-row var-row" title="flute length in inches&#010;unless metric is checked&#010;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&#010;unless metric is checked&#010;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>

View file

@ -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",