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. * Generate a list of points approximating a circular arc.
* @param {Point} start - the starting point of the arc. * @param {Point} start - the starting point of the arc.
* @param {Point} end - the ending point of the arc. * @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} [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 * @param {number} opts.radius - the radius of the arc. If undefined, will use the start and end points to infer the radius.
* start and end points to infer the radius. * @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. generating the points.
* @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. * @return {Array<Point>} an array of points representing the arc.
*/ */
export function arcToPath(start, end, arcdivs = 24, opts) { export function arcToPath(start, end, arcdivs = 24, opts) {
let { clockwise, center, radius } = opts; let { clockwise, center, radius } = opts;
// @type {Point}
if (end.x === undefined && end.x === undefined && center === undefined) { if (end.x === undefined && end.x === undefined && center === undefined) {
// bambu generates loop z or wipe loop arcs in place // bambu generates loop z or wipe loop arcs in place
// console.log({ skip_empty_arc: rec }); // console.log({ skip_empty_arc: rec });
return; return;
} }
if(arcdivs <= 2){ if (arcdivs <= 2) {
return [start.clone(), end.clone()]; 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 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 ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians
let samePoint = Math.abs(ad) < 0.001 let samePoint = Math.abs(ad) < 0.001
let ofFull = Math.abs(ad)/(2*Math.PI); let ofFull = Math.abs(ad) / (2 * Math.PI);
let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4);
let step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps; let step = (samePoint ? (Math.PI * 2) * (clockwise ? -1 : 1) : ad) / steps;
let zStart = start.z; let zStart = start.z;
let zStep = -dz / steps; let zStep = -dz / steps;
let rot = a1; let rot = a1;
@ -597,6 +592,7 @@ export function arcToPath(start, end, arcdivs = 24, opts) {
zStart += zStep; zStart += zStep;
rot += step; rot += step;
} }
// console.log(arr,start,end); // console.log(arr,start,end);
return arr return arr
} }

View file

@ -5,7 +5,7 @@
import { base, config, earcut, util } from './base.js'; import { base, config, earcut, util } from './base.js';
import { ClipperLib } from '../ext/clip2.esm.js'; import { ClipperLib } from '../ext/clip2.esm.js';
import { newBounds } from './bounds.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 { newPoint, pointFromClipper } from './point.js';
import { polygons as POLY } from './polygons.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 // generate a trace path around the inside of a polygon
// including inner polys. return the noodle and the remainder // including inner polys. return the noodle and the remainder
// of the polygon with the noodle removed (for the next pass) // of the polygon with the noodle removed (for the next pass)
// todo: relocate this code to post.js
noodle(width) { noodle(width) {
let clone = this.clone(true); let clone = this.clone(true);
let ins = clone.offset(width) ?? []; let ins = clone.offset(width) ?? [];
@ -278,6 +279,7 @@ export class Polygon {
// generate center crossing point cloud // generate center crossing point cloud
// only used for fdm thin-wall type 1 (fdm/post.js) // only used for fdm thin-wall type 1 (fdm/post.js)
// todo: relocate this code to post.js
centers(step, z, min, max, opt = {}) { centers(step, z, min, max, opt = {}) {
let cloud = [], let cloud = [],
bounds = this.bounds, bounds = this.bounds,
@ -736,6 +738,203 @@ export class Polygon {
return recs; 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 * add points forming a rectangle around a center point
* *
@ -1226,77 +1425,6 @@ export class Polygon {
return false; 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) { forEachPoint(fn, close, start) {
let index = start || 0, let index = start || 0,
points = this.points, 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) { intersections(lp1, lp2, deep) {
let list = []; let list = [];
@ -1893,9 +2022,10 @@ export class Polygon {
}); });
return { return {
point: closest,
distance: mindist, 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 // for turning a poly with an inner offset into a
// 3d mesh if and only if the inner has the same // 3d mesh if and only if the inner has the same
// circularity and <= num points // circularity and <= num points
// primarily used to make chamfers // primarily used to make chamfers in mesh:tool
// todo: relocate
ribbonMesh(swap) { ribbonMesh(swap) {
if (!(this.inner && this.inner.length === 1)) { if (!(this.inner && this.inner.length === 1)) {
return undefined; return undefined;
@ -2437,6 +2568,75 @@ export class Polygon {
return this; 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) { export function slopeDiff(s1, s2) {

View file

@ -41,36 +41,36 @@ const CleanPolygon = Clipper.CleanPolygon,
ClipIntersect = ClipType.ctIntersection; ClipIntersect = ClipType.ctIntersection;
const POLYS = { const POLYS = {
clearInner,
rayIntersect,
alignWindings, alignWindings,
setWinding, cleanClipperTree,
fillArea, clearInner,
subtract,
flatten,
offset,
trimTo,
expand,
points,
length,
renest,
route,
union,
inset,
outer,
inner,
nest,
diff, diff,
xor, expand,
setZ, fillArea,
filter, filter,
toClipper, fingerprint,
fingerprintCompare,
flatten,
fromClipperNode, fromClipperNode,
fromClipperTree, fromClipperTree,
fromClipperTreeUnion, fromClipperTreeUnion,
cleanClipperTree, inner,
fingerprintCompare, inset,
fingerprint length,
nest,
offset,
outer,
points,
rayIntersect,
renest,
route,
setWinding,
setZ,
subtract,
toClipper,
trimTo,
union,
xor,
}; };
export { POLYS }; export { POLYS };

View file

@ -246,7 +246,7 @@ export async function slice(points, options = {}) {
* @param {number} z offset * @param {number} z offset
* @param {Obejct} where * @param {Obejct} where
*/ */
function checkUnderOverOn(p, z, where) { export function checkOverUnderOn(p, z, where) {
let delta = p.z - z; let delta = p.z - z;
if (Math.abs(delta) < config.precision_slice_z) { // on if (Math.abs(delta) < config.precision_slice_z) { // on
where.on.push(p); where.on.push(p);
@ -266,7 +266,7 @@ function checkUnderOverOn(p, z, where) {
* @param {number} z offset * @param {number} z offset
* @returns {Point} intersection point * @returns {Point} intersection point
*/ */
function intersectPoints(over, under, z) { export function intersectPoints(over, under, z) {
let ip = []; let ip = [];
for (let i = 0; i < over.length; i++) { for (let i = 0; i < over.length; i++) {
for (let j = 0; j < under.length; j++) { 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 * Ensure points are unique with a cache/key algorithm
*/ */
function getCachedPoint(phash, p) { export function getCachedPoint(phash, p) {
let cached = phash[p.key]; let cached = phash[p.key];
if (!cached) { if (!cached) {
phash[p.key] = p; phash[p.key] = p;
@ -301,7 +301,7 @@ function getCachedPoint(phash, p) {
* @param {boolean} [edge] * @param {boolean} [edge]
* @returns {Line} * @returns {Line}
*/ */
function makeZLine(phash, p1, p2, coplanar, edge) { export function makeZLine(phash, p1, p2, coplanar, edge) {
p1 = getCachedPoint(phash, p1); p1 = getCachedPoint(phash, p1);
p2 = getCachedPoint(phash, p2); p2 = getCachedPoint(phash, p2);
let line = newOrderedLine(p1,p2); let line = newOrderedLine(p1,p2);
@ -336,9 +336,9 @@ export async function sliceZ(z, points, options = {}) {
p2 = points[i++]; p2 = points[i++];
p3 = points[i++]; p3 = points[i++];
let where = {under: [], over: [], on: []}; let where = {under: [], over: [], on: []};
checkUnderOverOn(p1, z, where); checkOverUnderOn(p1, z, where);
checkUnderOverOn(p2, z, where); checkOverUnderOn(p2, z, where);
checkUnderOverOn(p3, z, where); checkOverUnderOn(p3, z, where);
if (where.under.length === 3 || where.over.length === 3) { if (where.under.length === 3 || where.over.length === 3) {
// does not intersect (all 3 above or below) // does not intersect (all 3 above or below)
} else if (where.on.length === 2) { } else if (where.on.length === 2) {
@ -871,7 +871,5 @@ export const slicer = {
slice, slice,
sliceZ, sliceZ,
slicePost: {}, slicePost: {},
sliceDedup: removeDuplicateLines,
sliceConnect sliceConnect
} }

View file

@ -204,7 +204,6 @@ const renamed = {
roughingOn: "camRoughOn", roughingOn: "camRoughOn",
roughingOver: "camRoughOver", roughingOver: "camRoughOver",
roughingPlunge: "camRoughPlunge", roughingPlunge: "camRoughPlunge",
roughingPocket: "camRoughVoid",
roughingSpeed: "camRoughSpeed", roughingSpeed: "camRoughSpeed",
roughingSpindle: "camRoughSpindle", roughingSpindle: "camRoughSpindle",
roughingStock: "camRoughStock", roughingStock: "camRoughStock",
@ -428,14 +427,18 @@ export const conf = {
camAreaMode: "clear", camAreaMode: "clear",
camAreaTrace: "none", camAreaTrace: "none",
camAreaSurface: "linear", camAreaSurface: "linear",
camAreaDirection: "climb",
camAreaAngle: 0, camAreaAngle: 0,
camAreaOver: 0.4, camAreaOver: 0.4,
camAreaDown: 1, camAreaDown: 1,
camAreaSpeed: 1000, camAreaSpeed: 1000,
camAreaPlunge: 100, camAreaPlunge: 100,
camAreaFollow: 15,
camAreaExpand: 0, camAreaExpand: 0,
camAreaSmooth: 1,
camAreaRefine: 0, camAreaRefine: 0,
camAreaSmooth: 1,
camAreaDogbones: false,
camAreaRevbones: false,
camAreaOutline: false, camAreaOutline: false,
camContourAngle: 85, camContourAngle: 85,
camContourBottom: false, camContourBottom: false,
@ -528,23 +531,19 @@ export const conf = {
camOriginOffZ: 0, camOriginOffZ: 0,
camOriginTop: true, camOriginTop: true,
camOutlineDogbone: false, camOutlineDogbone: false,
camOutlineRevbone: false,
camOutlineDown: 3, camOutlineDown: 3,
camOutlineIn: false, camOutlineIn: false,
camOutlineOmitThru: false, camOutlineOmitThru: false,
camOutlineOmitVoid: false,
camOutlineOn: true,
camOutlineOut: true,
camOutlineOver: 0.4, camOutlineOver: 0.4,
camOutlineOverCount: 1, camOutlineOverCount: 1,
camOutlinePlunge: 250, camOutlinePlunge: 250,
camOutlineSpeed: 800, camOutlineSpeed: 800,
camOutlineSpindle: 1000, camOutlineSpindle: 1000,
camOutlineTool: 1000, camOutlineTool: 1000,
camOutlineTop: true,
camOutlineWide: false, camOutlineWide: false,
camPocketContour: false, camPocketContour: false,
camPocketDown: 1, camPocketDown: 1,
camPocketEngrave: false,
camPocketExpand: 0, camPocketExpand: 0,
camPocketFollow: 5, camPocketFollow: 5,
camPocketOutline: false, camPocketOutline: false,
@ -562,7 +561,6 @@ export const conf = {
camRegisterThru: 5, camRegisterThru: 5,
camRoughAll: true, camRoughAll: true,
camRoughDown: 2, camRoughDown: 2,
camRoughFlat: true,
camRoughIn: true, camRoughIn: true,
camRoughOmitThru: false, camRoughOmitThru: false,
camRoughOmitVoid: false, camRoughOmitVoid: false,
@ -575,7 +573,6 @@ export const conf = {
camRoughStockZ: 0, camRoughStockZ: 0,
camRoughTool: 1000, camRoughTool: 1000,
camRoughTop: true, camRoughTop: true,
camRoughVoid: false,
camStockClipTo: false, camStockClipTo: false,
camStockIndexed: false, camStockIndexed: false,
camStockIndexGrid: true, camStockIndexGrid: true,
@ -603,7 +600,6 @@ export const conf = {
camTraceType: "follow", camTraceType: "follow",
camTraceZBottom: 0, camTraceZBottom: 0,
camTraceZTop: 0, camTraceZTop: 0,
camTrueShadow: false,
camZAnchor: "middle", camZAnchor: "middle",
camZBottom: 0, camZBottom: 0,
camZClearance: 1, camZClearance: 1,

View file

@ -130,6 +130,18 @@ const LISTS = {
surftyp: [ surftyp: [
{ name: "linear" }, { name: "linear" },
{ name: "offset" }, { 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'), deviceExport: $('device-exp'),
deviceSave: $('device-save'), 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'), setMenu: $('set-menu'),
settings: $('settings'), settings: $('settings'),
settingsBody: $('settingsBody'), 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 // non-move in a new plane means burp out
// the old sequence and start a new one // the old sequence and start a new one
if (newlayer || (autolayer && seq.z != point.z)) { if (newlayer || (autolayer && seq.z != point.z)) {
@ -508,7 +508,7 @@ class Print {
} }
} }
// add point to current sequence // 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.lastPos = Object.assign({}, pos);
scope.lastPosE = pos.E; 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. * @param {number} index - The line number of the g-code file that contains the G2 or G3 command.
*/ */
function G2G3(g2, line, index) { function G2G3(g2, line, index) {
const axes = {}; let axes = {};
const {point, prevPoint, center} = processLine(line,axes); let { point, prevPoint, center } = processLine(line, axes);
let arcPoints = arcToPath(prevPoint, point, 64, { clockwise: g2, center }) ?? [];
// console.log(structuredClone({point,prevPoint,center})); for (let point of arcPoints) {
outputPoint(point, prevPoint, 1, {});
let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? [] prevPoint = point;
let emit = g2 ? 2 : 3; }
outputPoint(point, prevPoint, 1, {});
// 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});
} }
function G0G1(g0, line) { function G0G1(g0, line) {

View file

@ -47,11 +47,12 @@ async function path(levels, update, opts = {}) {
return []; return [];
} }
const isCAM = is_cam();
const dark = is_dark(); const dark = is_dark();
const tools = opts.tools || {}; const tools = opts.tools || {};
const flat = opts.flat; const flat = opts.flat;
const thin = opts.thin && !flat; const thin = opts.thin && !flat;
const ckspeed = opts.speed !== false; const ckspeed = isCAM || opts.speed !== false;
const headColor = 0x888888; const headColor = 0x888888;
const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa); const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa);
const printColor = opts.print >= 0 ? opts.print : 0x777700; const printColor = opts.print >= 0 ? opts.print : 0x777700;
@ -186,27 +187,7 @@ async function path(levels, update, opts = {}) {
if (arrowAll || lastOut.emit !== out.emit) { if (arrowAll || lastOut.emit !== out.emit) {
heads.push({p1: lastOutPoint, p2: outPoint}); heads.push({p1: lastOutPoint, p2: outPoint});
} }
const op = outPoint, lp = lastOutPoint; if (out.emit) {
// 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
// checks if a new poly should be started // checks if a new poly should be started
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
current = newPolygon().setOpen(); current = newPolygon().setOpen();

View file

@ -434,6 +434,7 @@ function newDiv(opt = {}) {
(opt.addto || addTo).appendChild(div); (opt.addto || addTo).appendChild(div);
if (opt.addto) lastDiv = addTo = div; if (opt.addto) lastDiv = addTo = div;
if (opt.class) div.setAttribute('class', opt.class); if (opt.class) div.setAttribute('class', opt.class);
if (opt.content) div.innerHTML = opt.content;
if (opt.group !== false) { if (opt.group !== false) {
lastGroup?.push(div); lastGroup?.push(div);
div._group = groupName; div._group = groupName;
@ -444,9 +445,10 @@ function newDiv(opt = {}) {
return div; return div;
} }
function newExpand(label, opt = {}, opteach = {}) { function newExpand(label, opt = {}) {
let div = DOC.createElement('details'); let div = DOC.createElement('details');
div.setAttribute('class', opt.class || 'f-col'); div.setAttribute('class', opt.class || 'f-col');
if (opt.open) div.setAttribute('open', true);
addModeControls(div, opt); addModeControls(div, opt);
let summary = DOC.createElement('summary'); let summary = DOC.createElement('summary');
@ -606,7 +608,7 @@ function newText(label, options) {
function newInput(label, opt = {}) { function newInput(label, opt = {}) {
let row = newDiv(opt), let row = newDiv(opt),
hide = opt.hide, hide = opt.hide,
size = opt.size || 5, size = opt.size ?? 5,
height = opt.height || 0, height = opt.height || 0,
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'), ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
action = opt.action || bindTo || inputAction; action = opt.action || bindTo || inputAction;
@ -630,6 +632,7 @@ function newInput(label, opt = {}) {
row.style.display = hide ? 'none' : ''; row.style.display = hide ? 'none' : '';
if (opt.disabled) ip.setAttribute("disabled", "true"); if (opt.disabled) ip.setAttribute("disabled", "true");
if (opt.title) row.setAttribute("title", opt.title); 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.convert) ip.convert = opt.convert.bind(ip);
if (opt.bound) ip.bound = opt.bound; if (opt.bound) ip.bound = opt.bound;
if (opt.action) action = opt.action; if (opt.action) action = opt.action;
@ -737,6 +740,7 @@ function newSelect(label, options = {}, source) {
} }
row.setAttribute("class", "var-row"); row.setAttribute("class", "var-row");
row.style.display = hide ? 'none' : ''; row.style.display = hide ? 'none' : '';
if (options.id) ip.setAttribute("id", options.id);
if (options.convert) ip.convert = options.convert.bind(ip); if (options.convert) ip.convert = options.convert.bind(ip);
if (options.disabled) ip.setAttribute("disabled", "true"); if (options.disabled) ip.setAttribute("disabled", "true");
if (options.title) row.setAttribute("title", options.title); if (options.title) row.setAttribute("title", options.title);

View file

@ -2,8 +2,12 @@
import { base } from '../../geo/base.js'; import { base } from '../../geo/base.js';
import { avgc } from './utils.js'; import { avgc } from './utils.js';
import { verticesToPoints } from '../../geo/points.js';
import { util as mesh_util } from '../../mesh/util.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 { inRange, time } = base.util;
const solid_opacity = 1.0; const solid_opacity = 1.0;
@ -707,7 +711,7 @@ class Widget {
} }
// used by CAM.shadowAt() // used by CAM.shadowAt()
this.cache.geo = pos; this.cache.geo = pos;
this.cache.shadow = undefined; delete this.cache.shadow;
return pos; return pos;
} }
@ -835,6 +839,115 @@ class Widget {
hide() { hide() {
this.mesh.visible = false; 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 // Widget Grouping API

View file

@ -9,14 +9,16 @@ const asLines = false;
let stock, center, grid, gridX, gridY, rez; let stock, center, grid, gridX, gridY, rez;
let path, pathIndex, tool, tools, last, toolID = 1; let path, pathIndex, tool, tools, last, toolID = 1;
let settings;
export function init(worker) { export function init(worker) {
const { dispatch } = worker; const { dispatch } = worker;
dispatch.animate_setup = function(data, send) { dispatch.animate_setup = function(data, send) {
const { settings } = data; settings = data.settings;
const { process } = settings; const { process } = settings;
const print = worker.current.print; const { print } = worker.current;
const density = parseInt(settings.controller.animesh) * 1000; const density = parseInt(settings.controller.animesh) * 1000;
pathIndex = 0; pathIndex = 0;
@ -287,7 +289,7 @@ function updateTool(toolobj, send) {
if (tool) { if (tool) {
send.data({ mesh_del: toolID }); send.data({ mesh_del: toolID });
} }
tool = new Tool({ tools }, toolobj.getID()); tool = new Tool(settings, toolobj.getID());
tool.generateProfile(rez); tool.generateProfile(rez);
const flen = tool.fluteLength() || 15; const flen = tool.fluteLength() || 15;
const slen = tool.shaftLength() || 15; const slen = tool.shaftLength() || 15;

View file

@ -12,6 +12,7 @@ let nextMeshID = 1,
tool, tool,
tools, tools,
last, last,
settings,
stockZ, stockZ,
stockIndexMsg = false, stockIndexMsg = false,
stockSlices, stockSlices,
@ -34,10 +35,10 @@ export function init(worker) {
const { dispatch } = worker; const { dispatch } = worker;
dispatch.animate_setup2 = function (data, send) { dispatch.animate_setup2 = function (data, send) {
const { settings } = data; settings = data.settings;
const { controller, process } = settings; const { controller, process } = settings;
const print = worker.current.print; const { print } = worker.current;
const density = parseInt(settings.controller.animesh) * 1000;
const isIndexed = process.camStockIndexed; const isIndexed = process.camStockIndexed;
pathIndex = 0; pathIndex = 0;
@ -141,7 +142,7 @@ function renderPath(send) {
} }
const id = toolID; const id = toolID;
const rezstep = Math.min(tool.maxDiameter(), 1) / 4; const rezstep = tool ? Math.min(tool.maxDiameter(), 1) / 4 : 1;
// console.log({ rezstep }); // console.log({ rezstep });
if (last) { if (last) {
@ -273,7 +274,7 @@ function toolUpdate(toolid, send) {
if (tool) { if (tool) {
send.data({ mesh_del: toolID }); send.data({ mesh_del: toolID });
} }
tool = new Tool({ tools }, toolid); tool = new Tool(settings, toolid);
const Instance = CSG.Instance(); const Instance = CSG.Instance();
const slen = tool.shaftLength() || 15; const slen = tool.shaftLength() || 15;
const srad = tool.shaftDiameter() / 2; const srad = tool.shaftDiameter() / 2;
@ -286,6 +287,16 @@ function toolUpdate(toolid, send) {
mesh = cylinder(tlen - frad * 2, frad, frad, 20, true) mesh = cylinder(tlen - frad * 2, frad, frad, 20, true)
.add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2)) .add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2))
.add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 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()) { } else if (tool.isTaperMill()) {
const trad = Math.max(tool.tipDiameter() / 2, 0.001); const trad = Math.max(tool.tipDiameter() / 2, 0.001);
mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2) 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) { export function clearHolesRec(widget) {
if (widget.adds) { if (widget.adds) {
widget.adds.length = 0 //clear adds array 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: () => { addNote: () => {
if (!op.note && type !== 'flip') { if (!op.note && type !== 'flip' && !op.rec.deprecated) {
const divid = `div-${++seed}`; const divid = `div-${++seed}`;
const noteid = `note-${++seed}`; const noteid = `note-${++seed}`;
const div = document.createElement('div'); const div = document.createElement('div');
@ -227,8 +227,6 @@ export function createPopOps() {
leave: 'camRoughStock', leave: 'camRoughStock',
leavez: 'camRoughStockZ', leavez: 'camRoughStockZ',
all: 'camRoughAll', all: 'camRoughAll',
voids: 'camRoughVoid',
flats: 'camRoughFlat',
inside: 'camRoughIn', inside: 'camRoughIn',
omitthru: 'camRoughOmitThru', omitthru: 'camRoughOmitThru',
ov_topz: 0, 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 }), 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" }), sep: UC.newBlank({ class: "pop-sep" }),
all: UC.newBoolean(LANG.cr_clst_s, undefined, { title: LANG.cr_clst_l, show: hasIndexing }), 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 }), 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 }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
@ -268,12 +264,9 @@ export function createPopOps() {
rate: 'camOutlineSpeed', rate: 'camOutlineSpeed',
plunge: 'camOutlinePlunge', plunge: 'camOutlinePlunge',
dogbones: 'camOutlineDogbone', dogbones: 'camOutlineDogbone',
omitvoid: 'camOutlineOmitVoid', revbones: 'camOutlineRevbone',
omitthru: 'camOutlineOmitThru', omitthru: 'camOutlineOmitThru',
outside: 'camOutlineOut',
inside: 'camOutlineIn',
wide: 'camOutlineWide', wide: 'camOutlineWide',
top: 'camOutlineTop',
ov_topz: 0, ov_topz: 0,
ov_botz: 0, ov_botz: 0,
ov_conv: '~camConventional', 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 }), 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 }), 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" }), sep: UC.newBlank({ class: "pop-sep" }),
top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }),
inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }), wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l }),
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 } }),
dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }), 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" }), sep: UC.newBlank({ class: "pop-sep" }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), 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', refine: 'camPocketRefine',
follow: 'camPocketFollow', follow: 'camPocketFollow',
contour: 'camPocketContour', contour: 'camPocketContour',
engrave: 'camPocketEngrave',
outline: 'camPocketOutline', outline: 'camPocketOutline',
ov_topz: 0, ov_topz: 0,
ov_botz: 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 }), follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }), 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 }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }),
exp: UC.newExpand("overrides"), exp: UC.newExpand("overrides"),
sep: UC.newBlank({ class: "pop-sep" }), 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}), 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}), 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)}), 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}), 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}), 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}), 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}), 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}), 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 }), 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}), 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'; return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear';
} }
const open = true;
createPopOp('area', { createPopOp('area', {
spindle: 'camAreaSpindle', spindle: 'camAreaSpindle',
tool: 'camAreaTool', tool: 'camAreaTool',
@ -683,14 +672,16 @@ export function createPopOps() {
plunge: 'camAreaPlunge', plunge: 'camAreaPlunge',
expand: 'camAreaExpand', expand: 'camAreaExpand',
smooth: 'camAreaSmooth', smooth: 'camAreaSmooth',
follow: 'camAreaFollow',
refine: 'camAreaRefine', refine: 'camAreaRefine',
outline: 'camAreaOutline', outline: 'camAreaOutline',
tolerance: 'camTolerance',
dogbones: 'camAreaDogbones',
revbones: 'camAreaRevbones',
ov_topz: 0, ov_topz: 0,
ov_botz: 0, ov_botz: 0,
ov_conv: '~camConventional', direction: 'camAreaDirection',
tolerance: 'camTolerance',
}).inputs = { }).inputs = {
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
mode: UC.newSelect(LANG.mo_menu, {}, "opmode"), mode: UC.newSelect(LANG.mo_menu, {}, "opmode"),
tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"), 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"), sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"),
@ -701,26 +692,29 @@ export function createPopOps() {
], { class: "ext-buttons f-row" }), ], { class: "ext-buttons f-row" }),
outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), 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 }), 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 }), 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() }), 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() }), 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 }), 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 }), dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }),
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 }), revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }),
sep: UC.newBlank({ class: "pop-sep" }), exp_end: UC.endExpand(),
exp: UC.newExpand("feeds & speeds"), exp: UC.newExpand("feeds & speeds", { open }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), 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 }), 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 }), plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }),
exp_end: UC.endExpand(), exp_end: UC.endExpand(),
exp: UC.newExpand("overrides"), exp: UC.newExpand("bounds", { open }),
sep: UC.newBlank({ class: "pop-sep" }), sep: UC.newBlank({ class: "pop-sep" }),
ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), 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_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(), exp_end: UC.endExpand(),
}; };

View file

@ -291,7 +291,12 @@ export function opRender() {
let clazz = notime ? ["draggable", "notime"] : ["draggable"]; let clazz = notime ? ["draggable", "notime"] : ["draggable"];
let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; 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 (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([ html.appendAll([
`<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`, `<div id="${mark + i}" class="${clazz.join(' ')}"${title}>`,
`<label class="label">${label}</label>`, `<label class="label">${label}</label>`,
@ -526,10 +531,6 @@ export function opRender() {
export function init() { export function init() {
if (api.devel.enabled) {
$('op:area').classList.remove('hide');
}
api.event.on('tool.mesh.face-normal', normal => { api.event.on('tool.mesh.face-normal', normal => {
// console.log({ env.poppedRec }); // console.log({ env.poppedRec });
env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); 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; return;
} }
// skip arc points for display only
if (out.emit === -1) {
return;
}
let dx = opt.dx || newpos.x - pos.x, let dx = opt.dx || newpos.x - pos.x,
dy = opt.dy || newpos.y - pos.y, dy = opt.dy || newpos.y - pos.y,
dz = opt.dz || newpos.z - pos.z, dz = opt.dz || newpos.z - pos.z,

View file

@ -2,6 +2,7 @@
import { api } from '../../core/api.js'; import { api } from '../../core/api.js';
import { originReset, originSelect } from './cl-origin.js'; import { originReset, originSelect } from './cl-origin.js';
import { updateTool } from './tools.js';
let LANG = api.language.current; let LANG = api.language.current;
let { CAM } = api.const.MODES, let { CAM } = api.const.MODES,
@ -32,8 +33,37 @@ function zAnchorSave() {
export function menu() { export function menu() {
let anim = ui.anim = {}; let anim = ui.anim = {};
uc.setGroup($('tool-details'));
return { 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 */ /** Animation Bar */
_____: { _____: {
@ -92,8 +122,9 @@ export function menu() {
camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), 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 }), 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 }), 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}), 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" }), _____: 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}), camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}),
camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}),
@ -119,12 +150,10 @@ export function menu() {
newButton("reset", originReset), newButton("reset", originReset),
], { class: "ext-buttons f-row" }), ], { class: "ext-buttons f-row" }),
_____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), _____: 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 }), camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }),
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}),
camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}), 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}), 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 */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
// todo: surface offset pattern
// todo: trace dogbones, merge overlap
import { CamOp } from './op.js'; import { CamOp } from './op.js';
import { Tool } from './tool.js'; import { Tool } from './tool.js';
import { newSlice } from '../../core/slice.js'; import { newSlice } from '../../core/slice.js';
@ -22,16 +25,19 @@ class OpArea extends CamOp {
async slice(progress) { async slice(progress) {
let { op, state } = this; let { op, state } = this;
let { tool, mode, down, over, follow, expand, outline, refine, smooth } = op; let { tool, mode, down, over, follow, expand, outline, smooth } = op;
let { ov_topz, ov_botz, ov_conv } = op; let { ov_topz, ov_botz, direction, rename } = op;
let { settings, widget, tabs, color } = state; let { settings, widget, tabs, color } = state;
let { addSlices, setToolDiam, cutTabs, healPolys, shadowAt, workarea } = state; let { addSlices, setToolDiam, cutTabs, healPolys, shadowAt, workarea } = state;
let areaTool = new Tool(settings, tool); let areaTool = new Tool(settings, tool);
let smoothVal = (smooth ?? 0) / 10;
let toolDiam = areaTool.fluteDiameter(); let toolDiam = areaTool.fluteDiameter();
let toolOver = areaTool.hasTaper() ? over : toolDiam * over; let toolOver = areaTool.getStepSize(over);
let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_stock; let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_z;
let zBottom = ov_botz ? workarea.bottom_stock + ov_botz : workarea.bottom_part; 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 // also updates tab offsets
setToolDiam(toolDiam); setToolDiam(toolDiam);
@ -62,7 +68,7 @@ class OpArea extends CamOp {
// connect open poly edge segments into closed loops (when possible) // connect open poly edge segments into closed loops (when possible)
// surface and edge selections produce open polygons by default // surface and edge selections produce open polygons by default
polys = POLY.nest(healPolys(polys)); polys = POLY.nest(healPolys(polys, false));
// gather surface selections // gather surface selections
let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); 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 // add in unioned surface areas
polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), fminz)); 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) // expand selections (flattens z variable polys)
if (Math.abs(expand) > 0) { if (Math.abs(expand) > 0) {
@ -95,7 +103,9 @@ class OpArea extends CamOp {
nupolys.push(...expanded.flat()); 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 // process each area separately
@ -104,17 +114,17 @@ class OpArea extends CamOp {
for (let area of polys) { for (let area of polys) {
let bounds = area.getBounds3D(); let bounds = area.getBounds3D();
if (devel) newLayer().output()
.setLayer("area", { line: 0xff8800 }, false)
.addPolys([ area ]);
newArea();
if (outline) { if (outline) {
// remove inner voids when processing outline only // remove inner voids when processing outline only
area.inner = undefined; area.inner = undefined;
} }
newLayer().output()
.setLayer("area", { line: 0xff8800 }, false)
.addPolys([ area ]);
newArea();
if (mode === 'clear') { if (mode === 'clear') {
let zs = down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ]; let zs = down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ];
let zroc = 0; let zroc = 0;
@ -126,7 +136,22 @@ class OpArea extends CamOp {
let layers = slice.output(); let layers = slice.output();
let outs = []; let outs = [];
let clip = []; 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.subtract([ area ], shadow, clip, undefined, undefined, 0);
POLY.offset(clip, [ -toolDiam / 2, -toolOver ], { POLY.offset(clip, [ -toolDiam / 2, -toolOver ], {
count: 999, outs, flat: true, z, minArea: 0 count: 999, outs, flat: true, z, minArea: 0
@ -142,16 +167,30 @@ class OpArea extends CamOp {
break outer; break outer;
} }
// cut tabs when present // 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; slice.camLines = outs;
zroc += zinc; zroc += zinc;
lzo = z; lzo = z;
progress(proc + (pinc * zroc), 'clear'); progress(proc + (pinc * zroc), 'clear');
if (devel) layers if (devel) layers
.setLayer("shadow", { line: 0x0088ff }, false) .setLayer("base", { line: 0xff0000 }, false)
.addPolys(shadow); .addPolys(shadowBase)
.setLayer("shadow", { line: 0x00ff00 }, false)
.addPolys(shadow)
.setLayer("tool shadow", { line: 0x44ff88 }, false)
.addPolys(tool_shadow);
layers layers
.setLayer("clear", { line: 0x88ff00 }, false) .setLayer(rename ?? "clear", { line: 0x88ff00 }, false)
.addPolys(outs); .addPolys(outs);
// of the last output still cuts, we need an escape // of the last output still cuts, we need an escape
if (z === zs.peek()) { 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 zs = down ? base_util.lerp(zTop, bounds.min.z, down) : [ bounds.min.z ];
let zroc = 0; let zroc = 0;
let zinc = 1 / zs.length; let zinc = 1 / zs.length;
let lzo;
for (let z of zs) { for (let z of zs) {
let slice = newLayer(); let slice = newLayer();
let layers = slice.output(); let layers = slice.output();
let outs = []; let outs = [];
if (tr_type === 'none') { if (tr_type === 'none') {
// todo: move this out of the zs loop and only setZ when needed
area = area.clone(true); area = area.clone(true);
outs = [ zs.length > 1 ? area.setZ(z) : area ]; outs = [ zs.length > 1 ? area.setZ(z) : area ];
} else { } else {
// todo: move this out of the zs loop
POLY.offset([ area ], tr_type === 'inside' ? [ -toolDiam / 2 ] : [ toolDiam / 2 ], { POLY.offset([ area ], tr_type === 'inside' ? [ -toolDiam / 2 ] : [ toolDiam / 2 ], {
count: 1, outs, flat: true, z, minArea: 0 count: 1, outs, flat: true, z, minArea: 0
}); });
@ -183,14 +223,16 @@ class OpArea extends CamOp {
// terminate z descent when no further output possible // terminate z descent when no further output possible
break; break;
} }
// add dogbones when specified
if (op.dogbones) outs.forEach(out => out.addDogbones(toolDiam / 5, op.revbones));
// cut tabs when present // cut tabs when present
if (tabs) outs = cutTabs(tabs, outs); if (tabs) outs = cutTabs(tabs, outs);
slice.camLines = outs; slice.camLines = outs;
slice.tool_shadow = POLY.offset(await shadowAt(z), [ toolDiam / 2 - 0.01 ], { count: 1, z });
zroc += zinc; zroc += zinc;
lzo = z;
progress(proc + (pinc * zroc), 'trace'); progress(proc + (pinc * zroc), 'trace');
layers layers
.setLayer("trace", { line: 0x88ff00 }, false) .setLayer(rename ?? "trace", { line: 0x88ff00 }, false)
.addPolys(outs); .addPolys(outs);
} }
proc += pinc; proc += pinc;
@ -226,8 +268,12 @@ class OpArea extends CamOp {
paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32());
} else } else
if (sr_type === 'offset') { if (sr_type === 'offset') {
// todo: progressive inset from perimeter // progressive inset from perimeter
console.log({ sr_offset: toolOver }); 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 // prepare tool mesh points
@ -265,9 +311,10 @@ class OpArea extends CamOp {
// convert terrain raster output back to open polylines // convert terrain raster output back to open polylines
for (let path of output.paths) { for (let path of output.paths) {
path = newPolygon().fromArray([1, ...path]); path = newPolygon().fromArray([1, ...path]);
if (op.refine) path.refine(op.refine);
surface.push(path); surface.push(path);
newLayer().output() newLayer().output()
.setLayer("linear", { line: 0x00ff00 }, false) .setLayer(rename ?? "linear", { line: 0x00ff00 }, false)
.addPolys([ path ]); .addPolys([ path ]);
} }
@ -281,29 +328,28 @@ class OpArea extends CamOp {
prepare(ops, progress) { prepare(ops, progress) {
let { op, state, areas, surfaces } = this; 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; let { process } = state.settings;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
let printPoint = getPrintPoint();
// process surface paths // 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) { 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; return;
} }
@ -313,7 +359,7 @@ class OpArea extends CamOp {
dist: Infinity, dist: Infinity,
area: undefined 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 // skip devel / debug only areas
let topPolys = area[0].camLines; let topPolys = area[0].camLines;
if (!topPolys) continue; if (!topPolys) continue;

View file

@ -83,10 +83,13 @@ class OpContour extends CamOp {
async slice(progress) { async slice(progress) {
let { op, state } = this; let { op, state } = this;
let { color, addSlices, settings, updateToolDiams } = state; let { color, addSlices, settings, updateToolDiams } = state;
let conTool = new Tool(settings, op.tool);
let filter = createFilter(op, settings.origin, op.axis.toLowerCase()); 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); updateToolDiams(toolDiam);
// we need topo for safe travel moves when roughing and outlining // we need topo for safe travel moves when roughing and outlining
// not generated when drilling-only. then all z moves use bounds max. // not generated when drilling-only. then all z moves use bounds max.
// also generates x and y contouring when selected // also generates x and y contouring when selected
@ -114,21 +117,19 @@ class OpContour extends CamOp {
} }
prepare(ops, progress) { prepare(ops, progress) {
let { op, state, sliceOut } = this; let { op, state, sliceOut, toolStep } = this;
let { settings } = state; let { settings } = state;
let { process } = settings; let { process } = settings;
let { setTolerance, setTool, setSpindle } = ops; let { polyEmit, setContouring, setTolerance, setTool } = ops;
let { widget, camOut, newLayer, zmax } = ops; let { widget, camOut, newLayer, zmax } = ops;
let bounds = widget.getBoundingBox(); let bounds = widget.getBoundingBox();
let toolDiam = this.toolDiam; let toolDiam = this.toolDiam;
let stepover = toolDiam * op.step * 2;
let depthFirst = process.camDepthFirst;
let depthData = []; let depthData = [];
setTool(op.tool, op.rate, process.camFastFeedZ); setTool(op.tool, op.rate, process.camFastFeedZ);
setSpindle(op.spindle); setContouring(true, toolStep);
setTolerance(this.tolerance); setTolerance(this.tolerance);
let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax); let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax);
@ -139,38 +140,21 @@ class OpContour extends CamOp {
continue; continue;
} }
let polys = [], poly; let polys = [], poly;
slice.camLines.forEach(function (poly) { slice.camLines.forEach((poly) => {
if (depthFirst) poly = poly.clone(true).annotate({ slice: slice.index + 1 }); poly = poly.clone(true).annotate({ slice: slice.index + 1 });
polys.push({ first: poly.first(), last: poly.last(), poly: poly }); polys.push({ first: poly.first(), last: poly.last(), poly: poly });
}); });
if (depthFirst) { depthData.appendAll(polys);
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();
}
} }
if (depthFirst) { tip2tipEmit(depthData, printPoint, (el, point) => {
tip2tipEmit(depthData, printPoint, function (el, point, count) { let poly = el.poly;
let poly = el.poly; if (poly.last() === point) {
if (poly.last() === point) { poly.reverse();
poly.reverse(); }
} polyEmit(poly);
poly.forEachPoint(function (point, pidx) { newLayer();
camOut(printPoint = point.clone().annotate({ slice: poly.slice }), pidx > 0, { moveLen: stepover }); });
}, false);
newLayer();
});
}
} }
} }

View file

@ -21,9 +21,11 @@ class OpDrill extends CamOp {
drillToolDiam = drillTool.fluteDiameter(), drillToolDiam = drillTool.fluteDiameter(),
sliceOut = this.sliceOut = []; sliceOut = this.sliceOut = [];
const allDrills = drills[widget.id] ?? []
if (allDrills.length === 0) return;
updateToolDiams(drillToolDiam); updateToolDiams(drillToolDiam);
const allDrills = drills[widget.id] ?? []
// drill points to use center (average of all points) of the polygon // drill points to use center (average of all points) of the polygon
allDrills.forEach((drill) => { allDrills.forEach((drill) => {
if (!drill.selected) { if (!drill.selected) {
@ -61,9 +63,10 @@ class OpDrill extends CamOp {
let { op, sliceOut } = this; let { op, sliceOut } = this;
let { setTool, setSpindle, setDrill, emitDrills } = ops; let { setTool, setSpindle, setDrill, emitDrills } = ops;
if (sliceOut.length === 0) return;
setTool(op.tool, undefined, op.rate); setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell); setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(sliceOut.map(slice => slice.camLines).flat()); emitDrills(sliceOut.map(slice => slice.camLines).flat());
} }
} }

View file

@ -240,11 +240,10 @@ export class OpHelical extends CamOp {
} }
async prepare(ops, progress) { async prepare(ops, progress) {
let { polyEmit, printPoint, setTool, setSpindle } = ops; let { polyEmit, printPoint, setTool } = ops;
let { tool, spindle, rate, feed } = this.op; let { tool, spindle, rate, feed } = this.op;
setTool(tool, feed, rate); setTool(tool, feed, rate);
setSpindle(spindle);
let [ polys ] = this.sliceOut.map((s) => s.camLines); let [ polys ] = this.sliceOut.map((s) => s.camLines);
polys = polys.slice(); polys = polys.slice();
@ -268,7 +267,7 @@ export class OpHelical extends CamOp {
if (!closest) break; if (!closest) break;
poly = polys[closestI] poly = polys[closestI]
polys[closestI] = null; 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) { 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) // 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 // move above rotating stock
camOut(last = last.clone().setZ(ztop), 0); camOut(printPoint = printPoint.clone().setZ(ztop), 0);
// issue rotation command // 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) { prepare(ops, progress) {
let { op, state, slices, topo } = this; let { op, slices, topo } = this;
let { settings } = state; 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; 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 // 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) { for (let slice of slices) {
// ignore debug slices // ignore debug slices
@ -90,14 +81,14 @@ class OpLathe extends CamOp {
return; return;
} }
if (latent) { if (latent) {
camOut(latent, true, stepover); camOut(latent, 1);
latent = undefined; latent = undefined;
} }
} }
camOut(last = point.clone(), pidx > 0, stepover); camOut(last = point.clone(), pidx > 0 ? 1 : 0);
}, false); }, false);
if (latent) { 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().setZ(zmax), 0);
// camOut(last = last.clone().setA(amax), 0); // camOut(last = last.clone().setA(amax), 0);
newLayer(); 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) { async slice(progress) {
let { op, state } = this; 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 { stock } = settings;
let toolDiam = new Tool(settings, op.tool).fluteDiameter(); let toolDiam = new Tool(settings, tool).fluteDiameter();
let stepOver = this.stepOver = toolDiam * op.step; let stepOver = this.stepOver = toolDiam * step;
let wpos = state.widget.track.pos; let wpos = state.widget.track.pos;
let zTop = zMax + ztOff; let zTop = zMax + ztOff;
let zBot = zTop - op.down; let zBot = zTop - down;
let zList = op.stepz ? util.lerp(zTop, zBot, op.stepz) : [ zBot ]; let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ];
updateToolDiams(toolDiam); updateToolDiams(toolDiam);
let points = []; let points = [];
let clear = op.stock ? let clear = op.stock ?
[ newPolygon().centerRectangle({x:-wpos.x,y:-wpos.y,z:wpos.z}, stock.x + toolDiam/2, stock.y) ] : [ 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); POLY.fillArea(clear, 1090, stepOver, points);
@ -49,12 +50,10 @@ class OpLevel extends CamOp {
} }
prepare(ops, progress) { prepare(ops, progress) {
let { op, layers, stepOver } = this; let { layers, stepOver } = this;
let { setTool, setSpindle, printPoint } = ops; let { printPoint } = ops;
let { newLayer, tip2tipEmit, camOut } = ops; let { newLayer, tip2tipEmit, camOut } = ops;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
for (let lines of layers) { for (let lines of layers) {
lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } }); lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } });
printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => {

View file

@ -1,311 +1,46 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js'; 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 { polygons as POLY } from '../../../geo/polygons.js';
import { util as base_util } from '../../../geo/base.js'; import { newPolygon } from '../../../geo/polygon.js';
import { poly2polyEmit } from '../../../geo/paths.js';
import { newPoint } from '../../../geo/point.js';
import { addDogbones } from './slice.js';
class OpOutline extends CamOp { class OpOutline extends CamOp {
constructor(state, op) { constructor(state, op) {
super(state, op); super(state, op);
} }
// todo: wide cutout, dogbones
async slice(progress) { async slice(progress) {
let { op, state } = this; let { op, state } = this;
let { settings, slicer, addSlices, tshadow, thruHoles, unsafe, color, widget } = state; let { shadow, tool, widget } = state;
let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax, shadowAt } = state; let areas = POLY.flatten(POLY.expand(shadow.base, tool.fluteDiameter() / 2 - 0.001));
let { process, stock } = settings; let cutout = {
let { controller } = settings; areas: { [widget.id]: areas.map(p => p.toArray()) },
let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0}; dogbones: op.dogbones,
down: op.down,
if (op.down <= 0) { expand: 0,
throw `invalid step down "${op.down}"`; mode: 'trace',
} outline: op.omitthru,
let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); ov_botz: op.ov_botz,
updateToolDiams(toolDiam); ov_topz: op.ov_topz,
plunge: op.plunge,
let shadow = []; rate: op.rate,
let slices = []; rename: op.rename ?? "outline",
let intopt = { revbones: op.revbones,
off: 0.01, smooth: 0,
fit: true, spindle: op.spindle,
down: true, surfaces: {},
min: Math.max(0, workarea.bottom_z), tool: op.tool,
max: workarea.top_z tr_type: 'none',
}; };
let indices = slicer.interval(op.down, intopt); this.op_cutout = new OpArea(state, cutout);
let trueShadow = process.camTrueShadow === true; return this.op_cutout.slice(progress);
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;
} }
prepare(ops, progress) { async prepare(ops, progress) {
let { op, state, sliceOut } = this; return this.op_cutout.prepare(ops, progress);
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);
}
}
} }
} }

View file

@ -1,16 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js'; import { CamOp } from './op.js';
import { Tool } from './tool.js'; import { OpArea } from './op-area.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;
class OpPocket extends CamOp { class OpPocket extends CamOp {
constructor(state, op) { constructor(state, op) {
@ -18,253 +9,37 @@ class OpPocket extends CamOp {
} }
async slice(progress) { async slice(progress) {
const pocket = this;
let { op, state } = this; let { op, state } = this;
let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; let { contour, down, expand, follow, outline, ov_botz, ov_topz } = op;
let { ov_botz, ov_conv } = op; let { plunge, rate, refine, smooth, spindle, surfaces, tolerance, tool } = op;
let { settings, widget, addSlices, zBottom, tabs, color } = state; let pocket = {
let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; areas: {},
zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; down,
// generate tracing offsets from chosen features expand,
let sliceOut; follow,
let pockets = this.pockets = []; mode: contour ? 'surface' : 'clear',
let camTool = new Tool(settings, tool); outline,
let toolDiam = camTool.fluteDiameter(); ov_botz,
let toolOver = toolDiam * op.step; ov_topz,
let cutdir = ov_conv; over: op.step,
let engrave = contour && op.engrave; plunge,
let zTop = workarea.top_z; rate,
let devel = settings.controller.devel; refine,
let smoothVal = (smooth ?? 0) / 10; rename: op.rename ?? "pocket",
if (contour) { smooth,
down = 0; spindle,
this.contour = { sr_type: 'offset',
axis: "-", surfaces,
inside: true, tolerance,
nogpu: true, tool,
step: toolOver, tr_type: 'none',
tolerance, };
tool, this.op_pocket = new OpArea(state, pocket);
}; return this.op_pocket.slice(progress);
}
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;
} }
prepare(ops, progress) { prepare(ops, progress) {
let { op, state, pockets } = this; return this.op_pocket.prepare(ops, progress);
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
});
}
} }
} }

View file

@ -147,18 +147,16 @@ class OpRegister extends CamOp {
prepare(ops, progress) { prepare(ops, progress) {
let { op } = this; let { op } = this;
let { emitDrills, setDrill, setSpindle, setTool } = ops; let { emitDrills, setDrill, setTool } = ops;
if (op.axis === '-' || op.axis === '=') { if (op.axis === '-' || op.axis === '=') {
setTool(op.tool, op.feed, op.rate); setTool(op.tool, op.feed, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) { for (let slice of this.sliceOut) {
ops.emitTrace(slice); ops.emitTrace(slice);
} }
} else { } else {
setTool(op.tool, undefined, op.rate); setTool(op.tool, undefined, op.rate);
setDrill(op.down, op.lift, op.dwell); setDrill(op.down, op.lift, op.dwell);
setSpindle(op.spindle);
emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); emitDrills(this.sliceOut.map(slice => slice.camLines).flat());
} }
} }

View file

@ -1,341 +1,87 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js'; import { CamOp } from './op.js';
import { Tool } from './tool.js'; import { OpArea } from './op-area.js';
import { newPolygon } from '../../../geo/polygon.js'; import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.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 { class OpRough extends CamOp {
constructor(state, op) { constructor(state, op) {
super(state, op); super(state, op);
} }
// todo: cutThruBypass
async slice(progress) { async slice(progress) {
let { op, state } = this; let { op, state } = this;
let { settings, slicer, addSlices, unsafe, color, widget } = state; let { shadow, tool, widget } = state;
let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state;
let { ztOff, zMax, shadowAt, isIndexed} = state;
let { workarea } = 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) { if (op.down <= 0) {
throw `invalid step down "${op.down}"`; throw `invalid step down "${op.down}"`;
} }
let roughIn = op.inside; if (op.all) {
let roughDown = op.down; shadowBase = [ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ];
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();
});
} }
// clear the stock above the area to be roughed out let areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001));
if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone(); let rough = {
let step = toolDiam * op.step; rename: op.rename ?? "rough",
let inset = roughStock ? spindle: op.spindle,
POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : tool: op.tool,
POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); rate: op.rate,
let facing = POLY.offset(inset, -step, { count: 999, flat: true }); plunge: op.plunge,
let zdiv = ztOff / roughDown; mode: 'clear',
let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; over: op.step,
if (ztOff === 0) { down: op.down,
// compensate for lack of z top offset in this scenario expand: 0,
ztOff = zstep; smooth: 0,
} outline: true,
let zsteps = Math.round(ztOff / zstep); omitthru: op.omitthru,
let camFaces = this.camFaces = []; leave_xy: op.leave,
let zstart = zMax + ztOff - zstep; leave_z: op.leavez,
for (let z = zstart; zsteps > 0; zsteps--) { ov_botz: op.ov_botz,
let slice = newSlice(); ov_topz: op.ov_topz,
slice.z = z; areas: { [widget.id]: areas.map(p => p.toArray()) },
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); surfaces: {}
if (tabs) { };
polys = cutTabs(tabs, polys, slice.z);
} this.op_rough = new OpArea(state, rough);
slice.camLines = polys; await this.op_rough.slice(progress);
slice.output()
.setLayer(state.layername, {face: color, line: color}) if (cutOutside) {
.addPolys(slice.camLines); let cutout = {
addSlices(slice); rename: op.rename ?? "rough",
camFaces.push(slice); spindle: op.spindle,
z -= zstep; 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) { async prepare(ops, progress) {
let { op, state, sliceOut, camFaces } = this; await this.op_rough.prepare(ops, progress);
let { setTool, setSpindle, pocket, polyEmit, newLayer, printPoint } = ops; if (this.op_cutout) await this.op_cutout.prepare(ops, progress);
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
});
} }
} }

View file

@ -1,7 +1,8 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js'; 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 * Computes the Part "shadow" and attaches relevant data to the "state" object
@ -26,90 +27,63 @@ class OpShadow extends CamOp {
async slice(progress) { async slice(progress) {
let state = this.state; 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 bounds = widget.getBoundingBox();
let trueShadow = state.settings.process.camTrueShadow === true; let slices = [];
let shadowBase;
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 skipTerrain = unsafe; 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) { if (skipTerrain) {
console.log("skipping terrain generation"); 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 // terrain is the "shadow stack" where index 0 = top of part
// thus array.length -1 = bottom of part // thus array.length -1 = bottom of part
let terrain = await slicer.slice(tzindex, { each: data => { for (let i=0; i<tzindex.length; i++) {
let shadow = trueShadow ? shadowAt(data.z, lsz) : []; let z = tzindex[i];
tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); let shadow = shadowBase = await shadowAt(z);
tslices.push(data.slice); let slice = newSlice(z);
// capture current shadow for this slice slice.shadow = shadow;
data.slice.shadow = tshadow; slice.addTops(shadow);
if (false) { slices.push(slice);
const slice = data.slice; terrain.push({ slice, tops: shadow });
addSlices(slice); if (devel) {
slice.output() slice.output()
.setLayer("shadow", {line: 0x888800, thin: true }) .setLayer("shadow", {line: 0x888800, thin: true })
.addPolys(POLY.setZ(tshadow.clone(true), data.z), { thin: true }); .addPolys(shadow, { 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 });
} }
lsz = data.z; progress(i / tzindex.length);
progress(0.5 + 0.5 * (++cnt / tot)); }
}, progress: (index, total) => {
tot = total; if (devel && slices.length) {
progress((index / total) * 0.5); addSlices(slices);
} }); }
if (terrain.length === 0) { if (terrain.length === 0) {
throw `invalid widget shadow`; 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 // TODO: refactor ops to use a unified shadow object
state.shadow = { 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 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 */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { CamOp } from './op.js'; 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';
import { newPoint } from '../../../geo/point.js';
class OpTrace extends CamOp { class OpTrace extends CamOp {
constructor(state, op) { constructor(state, op) {
super(state, op); super(state, op);
} }
async slice(progress) { // todo: cut thru, wide steps
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);
if (tabs) { async slice(progress) {
tabs.forEach(tab => { let { op, state } = this;
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); 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 = {
for (let arr of areas) { areas,
let poly = newPolygon().fromArray(arr); down,
POLY.setWinding([ poly ], cutdir, false); expand,
polys.push(poly); follow,
let zs = poly.points.map(p => p.z); mode: mode === 'clear' ? 'clear' : 'trace',
let min = Math.min(...zs); outline,
let max = Math.max(...zs); ov_botz,
if (max - min > 0.0001 && canRecontour) { ov_topz,
reContour = true; over: op.step,
down = 0; plunge,
} rate,
} refine,
if (false) newSliceOut(0).output() rename: op.rename ?? "trace",
.setLayer("polys", {line: 0xaaaa00}, false) smooth,
.addPolys(polys); spindle,
function newSliceOut(z) { step,
let slice = newSlice(z); surfaces: {},
addSlices(slice); tolerance,
sliceOut.push(slice); tool,
return slice; tr_type: offset
} };
function minZ(z) { this.op_trace = new OpArea(state, trace);
return zBottom ? Math.max(zBottom, z - thru) : z - thru; return this.op_trace.slice(progress);
}
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);
}
}
} }
prepare(ops, progress) { async prepare(ops, progress) {
let { op, state } = this; return this.op_trace.prepare(ops, progress);
let { setTool, setSpindle } = ops;
setTool(op.tool, op.rate);
setSpindle(op.spindle);
for (let slice of this.sliceOut) {
ops.emitTrace(slice);
}
} }
} }

View file

@ -2,7 +2,7 @@
import { CamOp } from './op.js'; import { CamOp } from './op.js';
import { newSlice } from '../../core/slice.js'; import { newSlice } from '../../core/slice.js';
import { Slicer } from './slicer.js'; import { Slicer } from './slicer_cam.js';
class OpXRay extends CamOp { class OpXRay extends CamOp {
constructor(state, op) { 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 { setSliceTracker } from '../../core/slice.js';
import { ops as OPS } from './ops.js'; import { ops as OPS } from './ops.js';
import { Tool } from './tool.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'; 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 tabW = widget.group.filter(w => w != widget);
let proc = settings.process, let proc = settings.process,
sliceAll = widget.slices = [],
camOps = widget.camops = [], camOps = widget.camops = [],
sliceAll = widget.slices = [],
isIndexed = proc.camStockIndexed; isIndexed = proc.camStockIndexed;
let stock, bounds, track, let axisRotation, axisIndex,
camZTop, camZBottom, camZThru, wztop, ztOff, zbOff, bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea,
zBottom, zMin, zMax, zThru, zTop, camZTop, camZBottom, camZThru, minToolDiam, maxToolDiam,
minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units,
axisRotation, axisIndex,
part_size,
bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut, bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut,
top_stock, top_part, top_gap, top_z, top_stock, top_part, top_gap, top_z,
workarea; zBottom, zMin, zMax, zThru, zTop,
ztOff, zbOff, wztop;
axisRotation = axisIndex = undefined; axisRotation = axisIndex = undefined;
dark = settings.controller.dark; 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 // allow recomputing later if widget or settings changes
const var_compute = () => { const var_compute = () => {
let { camStockX, camStockY, camStockZ, camStockOffset } = proc; let { camStockX, camStockY, camStockZ, camStockOffset } = proc;
({ camZTop, camZBottom, camZThru } = proc);
bounds = widget.getBoundingBox(); bounds = widget.getBoundingBox();
let pos = widget.track.pos;
stock = camStockOffset ? { stock = camStockOffset ? {
x: bounds.dim.x + camStockX, x: bounds.dim.x + camStockX,
y: bounds.dim.y + camStockY, y: bounds.dim.y + camStockY,
z: bounds.dim.z + camStockZ, z: bounds.dim.z + camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
} : { } : {
x: camStockX, x: camStockX,
y: camStockY, y: camStockY,
z: camStockZ z: camStockZ,
center: newPoint(pos.x, pos.y, pos.z)
}; };
({ camZTop, camZBottom, camZThru } = proc);
track = widget.track; track = widget.track;
wztop = track.top; wztop = track.top;
ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop); 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; zMax = bounds.max.z;
zThru = camZThru; zThru = camZThru;
zTop = zMax + ztOff; zTop = zMax + ztOff;
part_size = bounds.dim;
bottom_gap = zbOff; bottom_gap = zbOff;
bottom_part = 0; bottom_part = 0;
bottom_stock = -bottom_gap; bottom_stock = -bottom_gap;
@ -142,7 +142,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
let opList = []; let opList = [];
let opSum = 0; let opSum = 0;
let opTot = 0; let opTot = 0;
let shadows = {};
let slicer; let slicer;
let state = { let state = {
addSlices, addSlices,
@ -159,9 +158,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
setAxisIndex, setAxisIndex,
setToolDiam, setToolDiam,
settings, settings,
shadowAt, shadowAt(z) { return widget.shadowAt(z) },
slicer, slicer,
tabs, tabs,
tool,
unsafe, unsafe,
updateSlicer, updateSlicer,
updateToolDiams, updateToolDiams,
@ -203,9 +203,9 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
} }
async function computeShadows() { async function computeShadows() {
shadows = {}; // console.log('(re)compute shadows');
await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { 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); 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) { async function setAxisIndex(degrees = 0, absolute = true) {
axisIndex = absolute ? degrees : (axisIndex || 0) + degrees; axisIndex = absolute ? degrees : (axisIndex || 0) + degrees;
axisRotation = (Math.PI / 180) * axisIndex; 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); let activeOps = proc.ops.filter(op => !op.disabled);
// silently preface op list with OpShadow
if (isIndexed) { if (isIndexed) {
// preface op list with OpIndex
if (activeOps.length === 0 || activeOps[0].type !== 'index') { if (activeOps.length === 0 || activeOps[0].type !== 'index') {
opList.push(new OPS.index(state, { type: "index", index: 0 })); opList.push(new OPS.index(state, { type: "index", index: 0 }));
opTot += opList.peek().weight(); opTot += opList.peek().weight();
} }
} else { } else {
// preface op list with OpShadow
opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); opList.push(new OPS.shadow(state, { type: "shadow", silent: true }));
opTot += opList.peek().weight(); 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_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz;
workover.bottom_cut = Math.max(workover.bottom_z, -zThru); workover.bottom_cut = Math.max(workover.bottom_z, -zThru);
} }
if (valz.tool) {
tool = new Tool(settings, valz.tool);
}
let { note, type } = op.op; let { note, type } = op.op;
let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : []; let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : [];
let layername = named.length ? named : (note ? `${type} (${note})` : type); let layername = named.length ? named : (note ? `${type} (${note})` : type);
Object.assign(state, { Object.assign(state, {
layername, layername,
tool,
zBottom, zBottom,
zThru, zThru,
ztOff, ztOff,
@ -336,9 +326,16 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
zTop, zTop,
workarea: workover workarea: workover
}); });
let operr;
await op.slice((progress, message) => { await op.slice((progress, message) => {
onupdate((opSum + (progress * weight)) / opTot, message || op.type()); 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 // update tracker rotation for next slice output() visualization
tracker.rotation = isIndexed ? axisRotation : 0; tracker.rotation = isIndexed ? axisRotation : 0;
camOps.push(op); camOps.push(op);
@ -349,77 +346,12 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
// reindex // reindex
sliceAll.forEach((slice, index) => slice.index = index); 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.minToolDiam = minToolDiam;
widget.maxToolDiam = maxToolDiam; widget.maxToolDiam = maxToolDiam;
ondone(); 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) { export async function traces(settings, widget) {
if (widget.traces) { if (widget.traces) {
return false; return false;
@ -703,7 +635,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
for (let [i, slice] of slices.entries()) { for (let [i, slice] of slices.entries()) {
for (let top of slice.tops) { for (let top of slice.tops) {
// console.log("slicing",slice.z,top) // console.log("slicing",slice.z,top)
slice.shadow = computeShadowAt(widget, slice.z, 0); slice.shadow = await widget.shadowAt(slice.z);
let inner = top.inner; let inner = top.inner;
if (!inner) { //no holes if (!inner) { //no holes
continue; continue;
@ -903,75 +835,4 @@ function healPolys(noff, sameZ = true) {
} }
} }
return noff; 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 */ /** 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 { 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 { newPoint } from '../../../geo/point.js';
import { newSlice } from '../../core/slice.js'; import { newSlice } from '../../core/slice.js';
import { polygons as POLY } from '../../../geo/polygons.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 timing = false;
const zDecimal = 3; const zDecimal = 3;
const epsilon = 10e-5; const epsilon = 10e-5;
@ -232,15 +236,18 @@ export class Slicer {
// console.log({ oneach: slice, ...track }); // 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.tops = rec.polys;
rec.slice = newSlice(rec.z).addTops(rec.tops); rec.slice = newSlice(rec.z).addTops(rec.tops);
if (opt.each) { if (opt.each) {
opt.each(rec, i, data.length); await opt.each(rec, i, data.length);
} }
}); }
if (threaded) minions.broadcast("cam_slice_cleanup"); if (threaded) minions.broadcast("cam_slice_cleanup");
end("slicing"); end("slicing");
@ -339,7 +346,7 @@ export class Slicer {
if (dedup) { if (dedup) {
// console.log({ z, dedup: lines, points }); // console.log({ z, dedup: lines, points });
lines = sliceDedup(lines, debug); lines = removeDuplicateLines(lines, debug);
} }
return lines.length ? { return lines.length ? {
@ -403,78 +410,3 @@ export class Slicer {
return array.map(v => parseFloat(v.toFixed(zDecimal))); 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 */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { base } from '../../../geo/base.js';
import { newPoint } from '../../../geo/point.js'; import { newPoint } from '../../../geo/point.js';
import { newOrderedLine } from '../../../geo/line.js'; import {
checkOverUnderOn,
const { config } = base; makeZLine,
removeDuplicateLines,
intersectPoints
} from '../../../geo/slicer.js';
/** Slicer used in Topo3 and Topo4 to find contour lines */ /** Slicer used in Topo3 and Topo4 to find contour lines */
export class Slicer { export class Slicer {
static intersectPoints = intersectPoints;
static checkOverUnderOn = checkOverUnderOn;
static removeDuplicateLines = removeDuplicateLines;
constructor(index = 0) { constructor(index = 0) {
this.min = Infinity; this.min = Infinity;
this.max = -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 */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
const HPI = Math.PI / 2;
const RAD2DEG = 180 / Math.PI; const RAD2DEG = 180 / Math.PI;
const DEG2RAD = Math.PI / 180;
class Tool { class Tool {
constructor(settings, id, number) { constructor(settings, id, number) {
@ -15,6 +15,7 @@ class Tool {
this.tool.number = number >= 0 ? number : this.tool.number; this.tool.number = number >= 0 ? number : this.tool.number;
this.tool.id = id >= 0 ? id : this.tool.id; this.tool.id = id >= 0 ? id : this.tool.id;
} }
this.work_units = settings.controller.units === 'mm' ? 1 : 25.4;
} }
getID() { getID() {
@ -33,6 +34,12 @@ class Tool {
return this.tool.number; 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() { isMetric() {
return this.tool.metric; return this.tool.metric;
} }
@ -66,15 +73,22 @@ class Tool {
return diam ? diam * step : step; return diam ? diam * step : step;
} }
setTaperLengthFromAngle(angle) { // setTaperLengthFromAngle(angle) {
const rad = (this.flute_diam - this.taper_tip) / 2; // const rad = (this.flute_diam - this.taper_tip) / 2;
return this.flute_len = calcTaperLength(rad, angle); // return this.flute_len = calcTaperLength(rad, angle);
} // }
getTaperAngle() { 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() { shaftLength() {
return this.unitScale() * this.tool.shaft_len; return this.unitScale() * this.tool.shaft_len;
} }
@ -102,12 +116,16 @@ class Tool {
return this.tool.type === "tapermill"; return this.tool.type === "tapermill";
} }
isTaperBall() {
return this.tool.type === "taperball";
}
isDrill() { isDrill() {
return this.tool.type === "drill"; return this.tool.type === "drill";
} }
hasTaper() { hasTaper() {
return this.isTaperMill(); return this.isTaperMill() || this.isTaperBall();
} }
/** /**
@ -115,60 +133,77 @@ class Tool {
* Float32Array stored in `this.profile` and `this.profileDim` containing * Float32Array stored in `this.profile` and `this.profileDim` containing
* dimensions of the profile in pixels and shared array buffer size. * 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 * @return {Tool} this
*/ */
generateProfile(resolution) { generateProfile(resolution) {
// generate tool profile // generate tool profile
let ball = this.isBallMill(), let ball = this.isBallMill(),
taperball = this.isTaperBall(),
taper = this.hasTaper(), taper = this.hasTaper(),
drill = this.isDrill(), drill = this.isDrill(),
tip_diameter = this.tipDiameter(), tip_dia = this.tipDiameter(),
drill_tip_length = this.drillTipLength(), flute_dia = this.fluteDiameter(),
shaft_offset = this.fluteLength(), flute_len = this.fluteLength(),
flute_diameter = this.fluteDiameter(), flute_rad = flute_dia / 2,
shaft_diameter = Math.max(flute_diameter, this.shaftDiameter()), shaft_dia = Math.max(flute_dia, this.shaftDiameter()),
max_diameter = Math.max(flute_diameter, shaft_diameter), max_dia = Math.max(flute_dia, shaft_dia),
shaft_pix_float = max_diameter / resolution, larger_shaft = shaft_dia - flute_dia > 0.001,
shaft_pix_int = Math.round(shaft_pix_float), pix_sh_dia_float = max_dia / resolution,
shaft_radius_pix_float = shaft_pix_float / 2, pix_sh_dia_int = Math.round(pix_sh_dia_float),
flute_radius = flute_diameter / 2, pix_sh_rad_float = pix_sh_dia_float / 2,
flute_pix_float = flute_diameter / resolution, pix_fl_dia_float = flute_dia / resolution,
flute_radius_pix_float = flute_pix_float / 2, pix_fl_rad_float = pix_fl_dia_float / 2,
tip_pix_float = tip_diameter / resolution, pix_tip_dia_float = tip_dia / resolution,
tip_radius_pix_float = tip_pix_float / 2, pix_tip_rad_float = pix_tip_dia_float / 2,
tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float, tip_max_rad_offset = pix_fl_rad_float - pix_tip_rad_float,
profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2), pix_profile_iter = pix_sh_dia_int + (1 - pix_sh_dia_int % 2),
toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2, toolCenter = (pix_sh_dia_int - (pix_sh_dia_int % 2)) / 2,
toolOffset = [], toolOffset = [],
larger_shaft = shaft_diameter - flute_diameter > 0.001, maxo = -Infinity,
rpixsq = flute_radius_pix_float * flute_radius_pix_float, // ball taper magic
maxo = -Infinity; 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 each point in tool profile, check inside radius
for (let x = 0; x < profile_pix_iter; x++) { for (let x = 0; x < pix_profile_iter; x++) {
for (let y = 0; y < profile_pix_iter; y++) { for (let y = 0; y < pix_profile_iter; y++) {
let dx = x - toolCenter, let dx = x - toolCenter,
dy = y - toolCenter, dy = y - toolCenter,
dist_from_center = Math.sqrt(dx * dx + dy * dy); 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); maxo = Math.max(maxo, dx, dy);
// flute offset points // flute offset points
let z_offset = 0; let z_offset = 0;
if (ball) { if (ball) {
let rd = dist_from_center * dist_from_center; let ball_rad_sq = dist_from_center * dist_from_center;
z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius; let ball_rpixsq = pix_fl_rad_float * pix_fl_rad_float;
// z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; z_offset = Math.sqrt(ball_rpixsq - ball_rad_sq) * resolution - flute_rad;
} else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius } else if (taperball) {
z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; // 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) { } else if (drill) {
z_offset = -dist_from_center / 45; z_offset = -dist_from_center / 45;
} }
toolOffset.push(dx, dy, z_offset); 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 // 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.profile = profile;
this.profileDim = { this.profileDim = {
size: shaft_diameter, size: shaft_dia,
pix: profile_pix_iter + 2, pix: pix_profile_iter + 2,
maxo maxo
}; };
@ -190,19 +225,24 @@ class Tool {
function calcTaperAngle(rad, len) { function calcTaperAngle(rad, len) {
return (Math.atan(rad / len) * RAD2DEG); return (Math.atan(rad / len) * RAD2DEG);
}; }
function calcTaperLength(rad, angle) { function calcTaperLength(rad, angle) {
return (rad / Math.tan(angle)); return (rad / Math.tan(angle));
}; }
function calcTaperBallExtent(rad, angle) {
return rad * (1 - Math.sin(angle * 2));
}
function getToolDiameter(settings, id) { function getToolDiameter(settings, id) {
return new Tool(settings, id).fluteDiameter(); return new Tool(settings, id).fluteDiameter();
}; }
export { export {
Tool, Tool,
calcTaperAngle, calcTaperAngle,
calcTaperBallExtent,
calcTaperLength, calcTaperLength,
getToolDiameter getToolDiameter
}; };

View file

@ -4,7 +4,7 @@ import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js'; import { api } from '../../core/api.js';
import { consts } from '../../core/consts.js'; import { consts } from '../../core/consts.js';
import { settings as setconf } from '../../core/settings.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; const DEG2RAD = Math.PI / 180;
@ -12,8 +12,7 @@ let { MODES } = consts,
DOC = document, DOC = document,
selectedTool = null, selectedTool = null,
editTools = null, editTools = null,
maxTool = 0, maxTool = 0;
toolNames = ['endmill','ballmill','tapermill','drill'];
function settings() { function settings() {
return api.conf.get(); return api.conf.get();
@ -35,27 +34,110 @@ function renderTools() {
function selectTool(tool) { function selectTool(tool) {
const { ui } = api; const { ui } = api;
selectedTool = tool; selectedTool = tool;
ui.toolName.value = tool.name;
ui.toolNum.value = tool.number; api.util.rec2ui({
ui.toolFluteDiam.value = tool.flute_diam; toolName: selectedTool.name,
ui.toolFluteLen.value = tool.flute_len; toolType: selectedTool.type,
ui.toolShaftDiam.value = tool.shaft_diam; toolNum: selectedTool.number,
ui.toolShaftLen.value = tool.shaft_len; toolMetric: selectedTool.metric,
ui.toolTaperTip.value = tool.taper_tip || 0; toolShaftDiam: selectedTool.shaft_diam,
ui.toolMetric.checked = tool.metric; toolShaftLen: selectedTool.shaft_len,
ui.toolType.selectedIndex = toolNames.indexOf(tool.type); toolFluteDiam: selectedTool.flute_diam,
if (tool === 'tapermill') { toolFluteLen: selectedTool.flute_len,
ui.toolTaperAngle.value = calcTaperAngle( toolTaperAngle: selectedTool.taper_angle,
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len toolTaperTip: selectedTool.taper_tip,
).round(1); },{
} else if(tool === 'drill'){ toolName: ui.toolName,
ui.toolTaperAngle.value = 118; 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 { } else {
ui.toolTaperAngle.value = 0; ui.toolTaperAngle.value = selectedTool.taper_angle = 0;
} }
renderTool(tool); 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) { function otag(o) {
if (Array.isArray(o)) { if (Array.isArray(o)) {
let out = [] let out = []
@ -77,157 +159,89 @@ function otag(o) {
function renderTool(tool) { function renderTool(tool) {
const { ui } = api; 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>'; $('tool-view').innerHTML = '<svg id="tool-svg" width="100%" height="100%"></svg>';
setTimeout(() => { setTimeout(() => {
let svg = $('tool-svg'), let svg = $('tool-svg');
pad = 10, let pad = 10;
dim = { w: svg.clientWidth, h: svg.clientHeight }, let dim = { w: svg.clientWidth, h: svg.clientHeight };
max = { w: dim.w - pad * 2, h: dim.h - pad * 2}, let max = { w: dim.w - pad * 2, h: dim.h - pad * 2 };
off = { x: pad, y: pad }, let 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
parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ // Create Tool instance and generate profile
`M ${x1} ${y1}`, //move to top left const toolInst = new Tool(settings(), tool.id);
`L ${x1} ${y2}`, //line to bottom left const resolution = (toolInst.maxDiameter() / toolInst.unitScale()) / 100;
`L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point toolInst.generateProfile(resolution);
`L ${x2} ${y2}`, //line to bottom right
`L ${x2} ${y1}`, //line to top right const profile = toolInst.profile;
`z` const { pix } = toolInst.profileDim;
].join('\n')}});
//add drill flute lines // Extract cross-section at y=0 (center line)
parts.push({ line: { // Profile is stored as [dx, dy, z_offset, dx, dy, z_offset, ...]
x1, y1, x2, y2: (y1 + y2) / 2, let crossSection = [];
stroke, stroke_width: stroke_thin for (let i = 0; i < profile.length; i += 3) {
} }); let dx = profile[i];
parts.push({ line: { let dy = profile[i + 1];
x1, y1: (y1 + y2) / 2, x2, y2, let z = profile[i + 2];
stroke, stroke_width: stroke_thin // Only take points where dy is approximately 0 (center line)
} }); if (Math.abs(dy) < 0.01) {
} else { crossSection.push({ x: dx * resolution, z });
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')}})
} }
// 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); svg.innerHTML = otag(parts);
}, 10); }, 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) { function setToolChanged(changed) {
editTools.changed = changed; editTools.changed = changed;
api.ui.toolsSave.disabled = !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 { newPoint } from '../../geo/point.js';
import { polygons as POLY } from '../../geo/polygons.js'; import { polygons as POLY } from '../../geo/polygons.js';
import { sliceZ, sliceConnect } from '../../geo/slicer.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 { Slicer as topo_slicer } from '../mode/cam/slicer_topo.js';
import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js'; import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js';
import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js'; import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js';

View file

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

View file

@ -142,9 +142,9 @@ async function _fetch(e) {
} }
if (url.pathname.endsWith("/")) { 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")) { } 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 { } else {
e.respondWith(fromCacheOrNetwork(request)); e.respondWith(fromCacheOrNetwork(request));
} }
@ -155,16 +155,9 @@ function appendURL(url, append) {
return new URL(url.origin + url.pathname + append + url.search); return new URL(url.origin + url.pathname + append + url.search);
} }
async function redirectOr404(path) { async function redirectToUrl(path, request) {
const cache = await cacheOpen; if (debug) log('REDIRECT', path);
const hit = await cache.match(path, { ignoreSearch: true }); return Response.redirect(path, 302);
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 fromCacheOrNetwork(req) { async function fromCacheOrNetwork(req) {

View file

@ -189,7 +189,9 @@ th, tr, td, label {
white-space: nowrap; white-space: nowrap;
} }
details summary { details summary {
display: flex;
list-style: none; list-style: none;
padding-left: 0 !important;
} }
details summary::-webkit-details-marker { details summary::-webkit-details-marker {
display: none; display: none;
@ -438,6 +440,9 @@ details[open] summary::after {
.dark .widopt { .dark .widopt {
background-color: #222; background-color: #222;
} }
.dark #camops summary {
background-color: var(--blue-0);
}
/* top menu bar, drop menus */ /* top menu bar, drop menus */
#top, #app-name { #top, #app-name {
@ -1400,6 +1405,9 @@ details[open] summary::after {
#camops .set-sep { #camops .set-sep {
margin-bottom: 4px; margin-bottom: 4px;
} }
#camops summary {
background-color: var(--blue-4);
}
#oplist .clock:hover { #oplist .clock:hover {
background-color: var(--blue-2); background-color: var(--blue-2);
} }
@ -2425,14 +2433,14 @@ details[open] summary::after {
} }
.var-row input { .var-row input {
/* flex: 1 1 auto; */ /* flex: 1 1 auto; */
min-width: 0; min-width: 0;
max-width: 7ch; max-width: 7ch;
margin-right: 0; margin-right: 0;
margin-bottom: 1px; margin-bottom: 1px;
padding-bottom: 1px; padding-bottom: 1px;
padding-top: 1px; padding-top: 1px;
} }
.var-row #tool-name{ .var-row #tool-name {
max-width: 15ch; max-width: 15ch;
} }
.var-row button { .var-row button {

View file

@ -452,16 +452,16 @@
<div id="laser-off" title="turn off laser mode">laser off</div> <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-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 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="drill selected holes">drill</div>
<div title="clear stock face&#13;or top of part&#13;when no stock">level</div> <div title="clear stock face">level</div>
<div title="follow selected features with&#13;a specified tool. often used&#13;for lettering and engraving">trace</div> <div title="follow a downward helix&#13;using conical selections">helical</div>
<div title="insert custom gcode">gcode</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="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 selected features. often used&#13;for lettering and engraving">trace</div>
<div title="follow a helix&#13;around circular paths">helical</div> <div title="clear areas above selected surfaces">pocket</div>
<div title="perform configurable operations on selected areas" id="op:area" class="hide">area</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> </div>
</div> </div>
@ -572,34 +572,13 @@
<div class="f-row t-head gap3"> <div class="f-row t-head gap3">
<label class="t-33 set-header" lk="tool">tool</label> <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="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>
<div id="tool-cols" class="f-row gap3"> <div id="tool-cols" class="f-row gap3">
<div class="f-col t-33"> <div class="f-col t-33">
<select id="tool-select" size="10" class="grow"></select> <select id="tool-select" size="10" class="grow"></select>
</div> </div>
<div class="f-col t-33 t-body t-inset"> <div id="tool-details" class="f-col t-33 t-body t-inset"></div>
<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-view" class="f-col t-33 t-body"></div> <div id="tool-view" class="f-col t-33 t-body"></div>
</div> </div>
<div class="set-sep"></div> <div class="set-sep"></div>

View file

@ -513,10 +513,10 @@ self.lang['en-us'] = {
// CNC COMMON // CNC COMMON
cc_menu: "limits", cc_menu: "limits",
cc_rapd_s: "xy feed", cc_rapd_s: "feed rate",
cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], cc_rapd_l: ["max xy cutting feedrate","in workspace units / minute"],
cc_rzpd_s: "z feed", cc_rzpd_s: "plunge rate",
cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], cc_rzpd_l: ["max z cutting plunge rate","in workspace units / minute"],
// CNC LEVELING // CNC LEVELING
cc_loff_s: "z offset", 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_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"],
cr_clrt_s: "clear top", cr_clrt_s: "clear top",
cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], 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_s: "inside only",
cr_olin_l: ["limit cutting to","inside part boundaries"], 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_dogb_l: ["insert dogbone cuts","into inside corners"],
co_dogr_s: "reverse bones", co_dogr_s: "reverse bones",
co_dogr_l: ["reverse dogbone direction"], 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_s: "wide cutout",
co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], 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_s: "omit through",
co_omit_l: "eliminate thru holes", co_omit_l: "eliminate thru holes",
co_omvd_s: "omit pocket",
co_omvd_l: "eliminate interior pockets",
co_olen_s: "enable", co_olen_s: "enable",
co_olen_l: "enabled outline cutting", 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_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_s: "contour",
cp_cont_l: ["ignore interior voids and features"], 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_s: "outline only",
cp_outl_l: ["ignore interior voids and features"], 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_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_s: "ease angle",
ou_eang_l: "descent angle for ease down in degrees", ou_eang_l: "descent angle for ease down in degrees",
ou_dire_s: "milling",
ou_dire_l: ["milling direction","climb, conventional, alternating"],
// CAM STOCK // CAM STOCK
cs_menu: "stock", cs_menu: "stock",