From e0b1c4e31f0cd7c0ebca08d60b05854e68f9d203 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Sat, 6 Dec 2025 00:09:17 -0500 Subject: [PATCH] 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 --- src/geo/paths.js | 22 +- src/geo/polygon.js | 352 ++++-- src/geo/polygons.js | 48 +- src/geo/slicer.js | 16 +- src/kiri/core/conf.js | 16 +- src/kiri/core/consts.js | 12 + src/kiri/core/init.js | 19 - src/kiri/core/print.js | 25 +- src/kiri/core/render.js | 25 +- src/kiri/core/ui.js | 8 +- src/kiri/core/widget.js | 117 +- src/kiri/mode/cam/anim-2d-be.js | 8 +- src/kiri/mode/cam/anim-3d-be.js | 21 +- src/kiri/mode/cam/cl-hole.js | 2 +- src/kiri/mode/cam/cl-ops.js | 56 +- src/kiri/mode/cam/client.js | 11 +- src/kiri/mode/cam/export.js | 5 + src/kiri/mode/cam/init-menu.js | 39 +- src/kiri/mode/cam/op-area.js | 136 ++- src/kiri/mode/cam/op-contour.js | 52 +- src/kiri/mode/cam/op-drill.js | 7 +- src/kiri/mode/cam/op-helical.js | 5 +- src/kiri/mode/cam/op-index.js | 8 +- src/kiri/mode/cam/op-lathe.js | 23 +- src/kiri/mode/cam/op-level.js | 19 +- src/kiri/mode/cam/op-outline.js | 321 +----- src/kiri/mode/cam/op-pocket.js | 281 +---- src/kiri/mode/cam/op-register.js | 4 +- src/kiri/mode/cam/op-rough.js | 372 +----- src/kiri/mode/cam/op-shadow.js | 100 +- src/kiri/mode/cam/op-trace.js | 324 +----- src/kiri/mode/cam/op-xray.js | 2 +- src/kiri/mode/cam/prepare.js | 1001 +++++++---------- src/kiri/mode/cam/slice.js | 199 +--- .../mode/cam/{slicer.js => slicer_cam.js} | 102 +- src/kiri/mode/cam/slicer_topo.js | 188 +--- src/kiri/mode/cam/tool.js | 126 ++- src/kiri/mode/cam/tools.js | 338 +++--- src/kiri/run/minion.js | 2 +- web/boot/index.html | 1 + web/boot/service.js | 17 +- web/kiri/index.css | 14 +- web/kiri/index.html | 41 +- web/kiri/lang/en.js | 24 +- 44 files changed, 1611 insertions(+), 2898 deletions(-) rename src/kiri/mode/cam/{slicer.js => slicer_cam.js} (86%) diff --git a/src/geo/paths.js b/src/geo/paths.js index 4e638505..b4698257 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -518,27 +518,22 @@ export function shapeToPath(shape, points, closed) { /** * Generate a list of points approximating a circular arc. * @param {Point} start - the starting point of the arc. - * @param {Point} end - the ending point of the arc. - * @param {number} [arcdivs= 24] - the number of lines to use to represent PI radians - * @param {number} opts.radius - the radius of the arc. If undefined, will use the - * start and end points to infer the radius. - * @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. - * generating the points. - * + * @param {Point} end - the ending point of the arc (not included). + * @param {number} [arcdivs = 24] - the number of lines to use to represent PI radians + * @param {number} opts.radius - the radius of the arc. If undefined, will use the start and end points to infer the radius. + * @param {boolean} opts.clockwise - whether the arc is clockwise or counter-clockwise. generating the points. * @return {Array} an array of points representing the arc. */ export function arcToPath(start, end, arcdivs = 24, opts) { - let { clockwise, center, radius } = opts; - // @type {Point} if (end.x === undefined && end.x === undefined && center === undefined) { // bambu generates loop z or wipe loop arcs in place // console.log({ skip_empty_arc: rec }); return; } - if(arcdivs <= 2){ + if (arcdivs <= 2) { return [start.clone(), end.clone()]; } @@ -573,9 +568,9 @@ export function arcToPath(start, end, arcdivs = 24, opts) { let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI; let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians let samePoint = Math.abs(ad) < 0.001 - let ofFull = Math.abs(ad)/(2*Math.PI); - let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); - let step = (samePoint? (Math.PI*2)*(clockwise? -1 : 1) : ad) / steps; + let ofFull = Math.abs(ad) / (2 * Math.PI); + let steps = samePoint ? arcdivs : Math.max(Math.floor(arcdivs * ofFull), 4); + let step = (samePoint ? (Math.PI * 2) * (clockwise ? -1 : 1) : ad) / steps; let zStart = start.z; let zStep = -dz / steps; let rot = a1; @@ -597,6 +592,7 @@ export function arcToPath(start, end, arcdivs = 24, opts) { zStart += zStep; rot += step; } + // console.log(arr,start,end); return arr } diff --git a/src/geo/polygon.js b/src/geo/polygon.js index f834776a..b8b085de 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -5,7 +5,7 @@ import { base, config, earcut, util } from './base.js'; import { ClipperLib } from '../ext/clip2.esm.js'; import { newBounds } from './bounds.js'; -import { paths } from './paths.js'; +import { calc_normal, calc_vertex, paths } from './paths.js'; import { newPoint, pointFromClipper } from './point.js'; import { polygons as POLY } from './polygons.js'; @@ -268,6 +268,7 @@ export class Polygon { // generate a trace path around the inside of a polygon // including inner polys. return the noodle and the remainder // of the polygon with the noodle removed (for the next pass) + // todo: relocate this code to post.js noodle(width) { let clone = this.clone(true); let ins = clone.offset(width) ?? []; @@ -278,6 +279,7 @@ export class Polygon { // generate center crossing point cloud // only used for fdm thin-wall type 1 (fdm/post.js) + // todo: relocate this code to post.js centers(step, z, min, max, opt = {}) { let cloud = [], bounds = this.bounds, @@ -736,6 +738,203 @@ export class Polygon { return recs; } + /** + * Detect and annotate arc sequences in polygon points. + * When an arc is detected, the first point is annotated with arc metadata + * and intermediate points remain in the array but should be skipped during emission. + * + * @param {Object} opts - detection options + * @param {number} opts.tolerance - arc detection tolerance (default 0.1) + * @param {number} opts.arcRes - arc resolution in radians (default 0.1) + * @param {number} opts.minPoints - minimum points to consider an arc (default 4) + * @returns {Polygon} this polygon with arc annotations + */ + detectArcs(opts = {}) { + const { + tolerance = 0.1, + arcRes = 0.1, + minPoints = 4 + } = opts; + + const points = this.points; + const length = points.length; + + if (length < minPoints) { + return this; + } + + // Clear any existing arc annotations + for (let p of points) { + delete p.arc; + } + + let i = 0; + // Process all points except we can't start an arc at the last point + // because we need at least minPoints to form an arc + while (i < length - minPoints + 1) { + // Try to detect arc starting at position i + const arcData = this._detectArcAt(i, points, length, tolerance, arcRes, minPoints); + + if (arcData) { + // Annotate the starting point + points[i].arc = arcData; + // Skip past the arc points + i += arcData.skip + 1; // +1 to move to point after arc end + } else { + i++; + } + } + + return this; + } + + /** + * Try to detect an arc starting at the given index + * Uses a greedy approach: grow the arc as long as possible, then validate + * @private + * @returns {Object|null} arc data or null if no arc detected + */ + _detectArcAt(startIdx, points, length, tolerance, arcRes, minPoints) { + // Greedy approach: collect as many points as possible that might form an arc + let candidates = []; + + for (let idx = startIdx; idx < length; idx++) { + const p1 = points[idx]; + + // Last point - add and done + if (idx >= length - 1) { + candidates.push(p1); + break; + } + + const p2 = points[idx + 1]; + + // Skip duplicate points + if (p1.distTo2D(p2) < 0.001) { + break; + } + + candidates.push(p1); + + // Stop if we've collected enough to test + if (candidates.length >= minPoints) { + // Try to validate the arc with current candidates + const arcData = this._validateArc(candidates, tolerance, arcRes, minPoints); + + if (!arcData) { + // Current set doesn't form valid arc, back up one point + candidates.pop(); + break; + } + } + } + + // Final validation with all collected candidates + return this._validateArc(candidates, tolerance, arcRes, minPoints); + } + + /** + * Validate if a set of points forms a valid arc + * @private + */ + _validateArc(arcPoints, tolerance, arcRes, minPoints) { + if (arcPoints.length < minPoints) { + return null; + } + + // Calculate center using well-distributed points + const center = this._findBestCenter(arcPoints, tolerance); + if (!center) { + return null; + } + + // Check if all points lie on the circle within tolerance + let maxRadiusError = 0; + let sumRadiusError = 0; + + for (let i = 0; i < arcPoints.length; i++) { + const p = arcPoints[i]; + const radius = Math.hypot(p.x - center.x, p.y - center.y); + const radiusError = Math.abs(radius - center.r); + + maxRadiusError = Math.max(maxRadiusError, radiusError); + sumRadiusError += radiusError; + + // Hard limit on any single point + if (radiusError > tolerance * 2) { + return null; + } + } + + // Check average error is reasonable + const avgRadiusError = sumRadiusError / arcPoints.length; + if (avgRadiusError > tolerance) { + return null; + } + + // Check angular resolution (don't want points too far apart) + for (let i = 0; i < arcPoints.length - 1; i++) { + const curr = arcPoints[i]; + const next = arcPoints[i + 1]; + const dist = curr.distTo2D(next); + const radius = Math.hypot(curr.x - center.x, curr.y - center.y); + + if (radius > 0) { + const angle = 2 * Math.asin(Math.min(1, dist / (2 * radius))); + if (Math.abs(angle) > arcRes) { + return null; + } + } + } + + // Determine arc direction + const p0 = arcPoints[0]; + const p1 = arcPoints[Math.min(1, arcPoints.length - 1)]; + const vec1 = { x: p1.x - p0.x, y: p1.y - p0.y }; + const vec2 = { x: center.x - p0.x, y: center.y - p0.y }; + const cross = vec1.x * vec2.y - vec1.y * vec2.x; + const clockwise = cross < 0; + + return { + center: newPoint(center.x, center.y, p0.z), + clockwise, + skip: arcPoints.length - 1 + }; + } + + /** + * Find the best-fit center for a set of arc points + * @private + */ + _findBestCenter(arcPoints, tolerance) { + const len = arcPoints.length; + + // Use 3 well-spaced points for initial center calculation + let idx1 = 0; + let idx2 = Math.floor(len / 2); + let idx3 = len - 1; + + // If first and last are very close (near-circle), use different points + if (arcPoints[idx1].distTo2D(arcPoints[idx3]) < tolerance * 2) { + idx1 = Math.floor(len * 0.25); + idx2 = Math.floor(len * 0.5); + idx3 = Math.floor(len * 0.75); + } + + const center = util.center2d( + arcPoints[idx1], + arcPoints[idx2], + arcPoints[idx3], + 1 + ); + + if (!center || center.hasNaN?.() || !isFinite(center.r)) { + return null; + } + + return center; + } + /** * add points forming a rectangle around a center point * @@ -1226,77 +1425,6 @@ export class Polygon { return false; } - /** - * Ease down along the polygonal path. - * - * 1. Travel from fromPoint to closest point on polygon, to rampZ above that that point, - * 2. ease-down starts, following the polygonal path, decreasing Z at a fixed slope until target Z is hit, - * 3. then the rest of the path is completed and repeated at target Z until touchdown point is reached. - * 4. this function should probably move to CAM prepare since it's only called from there - * - * possibly no longer used anywhere - */ - forEachPointEaseDown(fn, fromPoint, degrees = 45) { - let index = this.findClosestPointTo(fromPoint).index, - fromZ = fromPoint.z, - offset = 0, - points = this.points, - length = points.length, - touch = -1, // first point to touch target z - targetZ = points[0].z, - dist2next, - last, - next, - done; - - // Slope for computations. - const slope = Math.tan((degrees * Math.PI) / 180); - // Z height above polygon Z from which to start the ease-down. - // Machine will travel from "fromPoint" to "nearest point x, y, z' => with z' = point z + rampZ", - // then start the ease down along path. - const rampZ = 2.0; - while (true) { - next = points[index % length]; - if (last && next.z < fromZ) { - // When "in Ease-Down" (ie. while target Z not yet reached) - follow path while slowly decreasing Z. - let deltaZ = fromZ - next.z; - dist2next = last.distTo2D(next); - let deltaZFullMove = dist2next * slope; - - if (deltaZFullMove > deltaZ) { - // Too long: easing along full path would overshoot depth, synth intermediate point at target Z. - // - // XXX: please check my super basic trig - this should follow from `last` to `next` up until the - // intersect at the target Z distance. - fn(last.followTo(next, dist2next * deltaZ / deltaZFullMove).setZ(next.z), offset++); - } else { - // Ok: execute full move at desired slope. - next = next.clone().setZ(fromZ - deltaZFullMove); - } - - fromZ = next.z; - } else if (offset === 0 && next.z < fromZ) { - // First point, move to rampZ height above next. - let deltaZ = fromZ - next.z; - fromZ = next.z + Math.min(deltaZ, rampZ) - next = next.clone().setZ(fromZ); - } - last = next; - fn(next, offset++); - if ((index % length) === touch) { - break; - } - if (touch < 0 && next.z <= targetZ) { - // Save touch-down index so as to be able to "complete" the full cut at target Z, - // i.e. keep following the path loop until the touch down point is reached again. - touch = ((index + length) % length); - } - index++; - } - - return last; - } - forEachPoint(fn, close, start) { let index = start || 0, points = this.points, @@ -1328,7 +1456,8 @@ export class Polygon { } /** - * returns intersections sorted by closest to lp1 + * given two endpoints of a line + * find all intersections sorted by closest to lp1 */ intersections(lp1, lp2, deep) { let list = []; @@ -1893,9 +2022,10 @@ export class Polygon { }); return { - point: closest, distance: mindist, - index: index + point: closest, + index: index, + poly: this }; } @@ -2195,7 +2325,8 @@ export class Polygon { // for turning a poly with an inner offset into a // 3d mesh if and only if the inner has the same // circularity and <= num points - // primarily used to make chamfers + // primarily used to make chamfers in mesh:tool + // todo: relocate ribbonMesh(swap) { if (!(this.inner && this.inner.length === 1)) { return undefined; @@ -2437,6 +2568,75 @@ export class Polygon { return this; } + // walk points noting z deltas and smoothing z sawtooth patterns + // used to smooth low-rez surface contouring + refine(passes = 0) { + for (let j = 0; j < passes; j++) { + let points = this.points, + length = points.length, + sn = [], // segment normals + vn = []; // vertex normals + for (let i = 0; i < length; i++) { + let p1 = points[i]; + let p2 = points[(i + 1) % length]; + sn.push(calc_normal(p1, p2)); + } + for (let i = 0; i < length; i++) { + let n1 = sn[(i + length - 1) % length]; + let n2 = sn[i]; + let vi = calc_vertex(n1, n2, 1); + vn.push(vi); + let vl = Math.abs(1 - vi.vl).round(2); + // vl should be close to zero on smooth / continuous curves + // factoring out hard turns, we smooth the z using the weighted + // z values of the points before and after the current point + if (vl === 0) { + let p0 = points[(i + length - 1) % length]; + let p1 = points[i]; + let p2 = points[(i + 1) % length]; + p1.z = (p0.z + p2.z + p1.z) / 3; + } + } + } + } + + addDogbones(dist, reverse) { + let poly = this; + let open = poly.open; + let isCW = poly.isClockwise(); + if (reverse || poly.parent) isCW = !isCW; + let oldpts = poly.points.slice(); + let lastpt = oldpts[oldpts.length - 1]; + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let length = oldpts.length + (open ? 0 : 1); + let newpts = []; + for (let i = 0; i < length; i++) { + let nextpt = oldpts[i % oldpts.length]; + let nextsl = lastpt.slopeTo(nextpt).toUnit(); + let adiff = lastsl.angleDiff(nextsl, true); + let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180; + if (!open || (i > 1 && i < length)) { + if (isCW && adiff > 45) { + let newa = newSlopeFromAngle(lastsl.angle + bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } else if (!isCW && adiff < -45) { + let newa = newSlopeFromAngle(lastsl.angle - bdiff); + newpts.push(lastpt.projectOnSlope(newa, dist)); + newpts.push(lastpt.clone()); + } + } + lastsl = nextsl; + lastpt = nextpt; + if (i < oldpts.length) { + newpts.push(nextpt); + } + } + poly.points = newpts; + if (poly.inner) { + poly.inner.forEach(inner => inner.addDogbones(dist, true)); + } + } } export function slopeDiff(s1, s2) { diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 1d5c5212..8606e40a 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -41,36 +41,36 @@ const CleanPolygon = Clipper.CleanPolygon, ClipIntersect = ClipType.ctIntersection; const POLYS = { - clearInner, - rayIntersect, alignWindings, - setWinding, - fillArea, - subtract, - flatten, - offset, - trimTo, - expand, - points, - length, - renest, - route, - union, - inset, - outer, - inner, - nest, + cleanClipperTree, + clearInner, diff, - xor, - setZ, + expand, + fillArea, filter, - toClipper, + fingerprint, + fingerprintCompare, + flatten, fromClipperNode, fromClipperTree, fromClipperTreeUnion, - cleanClipperTree, - fingerprintCompare, - fingerprint + inner, + inset, + length, + nest, + offset, + outer, + points, + rayIntersect, + renest, + route, + setWinding, + setZ, + subtract, + toClipper, + trimTo, + union, + xor, }; export { POLYS }; diff --git a/src/geo/slicer.js b/src/geo/slicer.js index 16b4b174..3ce82017 100644 --- a/src/geo/slicer.js +++ b/src/geo/slicer.js @@ -246,7 +246,7 @@ export async function slice(points, options = {}) { * @param {number} z offset * @param {Obejct} where */ -function checkUnderOverOn(p, z, where) { +export function checkOverUnderOn(p, z, where) { let delta = p.z - z; if (Math.abs(delta) < config.precision_slice_z) { // on where.on.push(p); @@ -266,7 +266,7 @@ function checkUnderOverOn(p, z, where) { * @param {number} z offset * @returns {Point} intersection point */ -function intersectPoints(over, under, z) { +export function intersectPoints(over, under, z) { let ip = []; for (let i = 0; i < over.length; i++) { for (let j = 0; j < under.length; j++) { @@ -279,7 +279,7 @@ function intersectPoints(over, under, z) { /** * Ensure points are unique with a cache/key algorithm */ -function getCachedPoint(phash, p) { +export function getCachedPoint(phash, p) { let cached = phash[p.key]; if (!cached) { phash[p.key] = p; @@ -301,7 +301,7 @@ function getCachedPoint(phash, p) { * @param {boolean} [edge] * @returns {Line} */ -function makeZLine(phash, p1, p2, coplanar, edge) { +export function makeZLine(phash, p1, p2, coplanar, edge) { p1 = getCachedPoint(phash, p1); p2 = getCachedPoint(phash, p2); let line = newOrderedLine(p1,p2); @@ -336,9 +336,9 @@ export async function sliceZ(z, points, options = {}) { p2 = points[i++]; p3 = points[i++]; let where = {under: [], over: [], on: []}; - checkUnderOverOn(p1, z, where); - checkUnderOverOn(p2, z, where); - checkUnderOverOn(p3, z, where); + checkOverUnderOn(p1, z, where); + checkOverUnderOn(p2, z, where); + checkOverUnderOn(p3, z, where); if (where.under.length === 3 || where.over.length === 3) { // does not intersect (all 3 above or below) } else if (where.on.length === 2) { @@ -871,7 +871,5 @@ export const slicer = { slice, sliceZ, slicePost: {}, - sliceDedup: removeDuplicateLines, sliceConnect } - diff --git a/src/kiri/core/conf.js b/src/kiri/core/conf.js index 86f10e4e..84650664 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -204,7 +204,6 @@ const renamed = { roughingOn: "camRoughOn", roughingOver: "camRoughOver", roughingPlunge: "camRoughPlunge", - roughingPocket: "camRoughVoid", roughingSpeed: "camRoughSpeed", roughingSpindle: "camRoughSpindle", roughingStock: "camRoughStock", @@ -428,14 +427,18 @@ export const conf = { camAreaMode: "clear", camAreaTrace: "none", camAreaSurface: "linear", + camAreaDirection: "climb", camAreaAngle: 0, camAreaOver: 0.4, camAreaDown: 1, camAreaSpeed: 1000, camAreaPlunge: 100, + camAreaFollow: 15, camAreaExpand: 0, - camAreaSmooth: 1, camAreaRefine: 0, + camAreaSmooth: 1, + camAreaDogbones: false, + camAreaRevbones: false, camAreaOutline: false, camContourAngle: 85, camContourBottom: false, @@ -528,23 +531,19 @@ export const conf = { camOriginOffZ: 0, camOriginTop: true, camOutlineDogbone: false, + camOutlineRevbone: false, camOutlineDown: 3, camOutlineIn: false, camOutlineOmitThru: false, - camOutlineOmitVoid: false, - camOutlineOn: true, - camOutlineOut: true, camOutlineOver: 0.4, camOutlineOverCount: 1, camOutlinePlunge: 250, camOutlineSpeed: 800, camOutlineSpindle: 1000, camOutlineTool: 1000, - camOutlineTop: true, camOutlineWide: false, camPocketContour: false, camPocketDown: 1, - camPocketEngrave: false, camPocketExpand: 0, camPocketFollow: 5, camPocketOutline: false, @@ -562,7 +561,6 @@ export const conf = { camRegisterThru: 5, camRoughAll: true, camRoughDown: 2, - camRoughFlat: true, camRoughIn: true, camRoughOmitThru: false, camRoughOmitVoid: false, @@ -575,7 +573,6 @@ export const conf = { camRoughStockZ: 0, camRoughTool: 1000, camRoughTop: true, - camRoughVoid: false, camStockClipTo: false, camStockIndexed: false, camStockIndexGrid: true, @@ -603,7 +600,6 @@ export const conf = { camTraceType: "follow", camTraceZBottom: 0, camTraceZTop: 0, - camTrueShadow: false, camZAnchor: "middle", camZBottom: 0, camZClearance: 1, diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index ca4ec075..e1e78b26 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -130,6 +130,18 @@ const LISTS = { surftyp: [ { name: "linear" }, { name: "offset" }, + ], + direction: [ + { name: "climb" }, + { name: "conventional" }, + { name: "alternating" }, + ], + camtool: [ + { name: "flat end", id: "endmill" }, + { name: "ball end", id: "ballmill" }, + { name: "taper tip", id: "tapermill" }, + { name: "taper ball", id: "taperball" }, + { name: "drill bit", id: "drill" }, ] }; diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index bf47547a..b44c96f2 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -918,25 +918,6 @@ function init_one() { deviceExport: $('device-exp'), deviceSave: $('device-save'), - toolsSave: $('tools-save'), - toolsClose: $('tools-close'), - toolsImport: $('tools-import'), - toolsExport: $('tools-export'), - toolSelect: $('tool-select'), - toolAdd: $('tool-add'), - toolCopy: $('tool-dup'), - toolDelete: $('tool-del'), - toolType: $('tool-type'), - toolName: $('tool-name'), - toolNum: $('tool-num'), - toolFluteDiam: $('tool-fdiam'), - toolFluteLen: $('tool-flen'), - toolShaftDiam: $('tool-sdiam'), - toolShaftLen: $('tool-slen'), - toolTaperAngle: $('tool-tangle'), - toolTaperTip: $('tool-ttip'), - toolMetric: $('tool-metric'), - setMenu: $('set-menu'), settings: $('settings'), settingsBody: $('settingsBody'), diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 8a86606a..2e99c9df 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -473,7 +473,7 @@ class Print { }; } - function outputPoint(point,lastP,emit,{center,arcPoints,retract}) { + function outputPoint(point,lastP,emit,{retract}) { // non-move in a new plane means burp out // the old sequence and start a new one if (newlayer || (autolayer && seq.z != point.z)) { @@ -508,7 +508,7 @@ class Print { } } // add point to current sequence - scope.addOutput(seq, point, emit, pos.F, tool,{retract,arcPoints}); + scope.addOutput(seq, point, emit, pos.F, tool, {retract}); scope.lastPos = Object.assign({}, pos); scope.lastPosE = pos.E; } @@ -520,19 +520,14 @@ class Print { * @param {number} index - The line number of the g-code file that contains the G2 or G3 command. */ function G2G3(g2, line, index) { - const axes = {}; - const {point, prevPoint, center} = processLine(line,axes); - - // console.log(structuredClone({point,prevPoint,center})); - - let arcPoints = arcToPath( prevPoint, point, 64,{ clockwise:g2,center}) ?? [] - let emit = g2 ? 2 : 3; - - // console.log("clone point",structuredClone({point,prevPoint,center,arcPoints,emit})); - // console.log("pointer point",{point,prevPoint,center,arcPoints,emit}); - - outputPoint(point,prevPoint,emit,{center,arcPoints}); - // scope.addOutput(seq, point, emit, pos.F, tool,{center,arcPoints}); + let axes = {}; + let { point, prevPoint, center } = processLine(line, axes); + let arcPoints = arcToPath(prevPoint, point, 64, { clockwise: g2, center }) ?? []; + for (let point of arcPoints) { + outputPoint(point, prevPoint, 1, {}); + prevPoint = point; + } + outputPoint(point, prevPoint, 1, {}); } function G0G1(g0, line) { diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 1c92f077..cd5ac95e 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -47,11 +47,12 @@ async function path(levels, update, opts = {}) { return []; } + const isCAM = is_cam(); const dark = is_dark(); const tools = opts.tools || {}; const flat = opts.flat; const thin = opts.thin && !flat; - const ckspeed = opts.speed !== false; + const ckspeed = isCAM || opts.speed !== false; const headColor = 0x888888; const moveColor = opts.move >= 0 ? opts.move : (dark ? 0x666666 : 0xaaaaaa); const printColor = opts.print >= 0 ? opts.print : 0x777700; @@ -186,27 +187,7 @@ async function path(levels, update, opts = {}) { if (arrowAll || lastOut.emit !== out.emit) { heads.push({p1: lastOutPoint, p2: outPoint}); } - const op = outPoint, lp = lastOutPoint; - // const moved = Math.max( - // Math.abs(op.x - lp.x), - // Math.abs(op.y - lp.y), - // Math.abs(op.z - lp.z)); - // if (moved < 0.0001) return; - if (is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit - // checks if a new poly should be started - if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { - current = newPolygon().setOpen(); - current.push(lastOutPoint); - current.color = color(out); - pushPrint(out.tool, current); - } - out.arcPoints.forEach(p => { - current.push(p); - }) - current.push(outPoint); - } else if (out.emit) { - // explicity G1 in CAM mode - // just a non-move in other modes + if (out.emit) { // checks if a new poly should be started if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) { current = newPolygon().setOpen(); diff --git a/src/kiri/core/ui.js b/src/kiri/core/ui.js index b0fcee75..1078967d 100644 --- a/src/kiri/core/ui.js +++ b/src/kiri/core/ui.js @@ -434,6 +434,7 @@ function newDiv(opt = {}) { (opt.addto || addTo).appendChild(div); if (opt.addto) lastDiv = addTo = div; if (opt.class) div.setAttribute('class', opt.class); + if (opt.content) div.innerHTML = opt.content; if (opt.group !== false) { lastGroup?.push(div); div._group = groupName; @@ -444,9 +445,10 @@ function newDiv(opt = {}) { return div; } -function newExpand(label, opt = {}, opteach = {}) { +function newExpand(label, opt = {}) { let div = DOC.createElement('details'); div.setAttribute('class', opt.class || 'f-col'); + if (opt.open) div.setAttribute('open', true); addModeControls(div, opt); let summary = DOC.createElement('summary'); @@ -606,7 +608,7 @@ function newText(label, options) { function newInput(label, opt = {}) { let row = newDiv(opt), hide = opt.hide, - size = opt.size || 5, + size = opt.size ?? 5, height = opt.height || 0, ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'), action = opt.action || bindTo || inputAction; @@ -630,6 +632,7 @@ function newInput(label, opt = {}) { row.style.display = hide ? 'none' : ''; if (opt.disabled) ip.setAttribute("disabled", "true"); if (opt.title) row.setAttribute("title", opt.title); + if (opt.id) ip.setAttribute("id", opt.id); if (opt.convert) ip.convert = opt.convert.bind(ip); if (opt.bound) ip.bound = opt.bound; if (opt.action) action = opt.action; @@ -737,6 +740,7 @@ function newSelect(label, options = {}, source) { } row.setAttribute("class", "var-row"); row.style.display = hide ? 'none' : ''; + if (options.id) ip.setAttribute("id", options.id); if (options.convert) ip.convert = options.convert.bind(ip); if (options.disabled) ip.setAttribute("disabled", "true"); if (options.title) row.setAttribute("title", options.title); diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index f72115da..c01f7308 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -2,8 +2,12 @@ import { base } from '../../geo/base.js'; import { avgc } from './utils.js'; -import { verticesToPoints } from '../../geo/points.js'; import { util as mesh_util } from '../../mesh/util.js'; +import { verticesToPoints } from '../../geo/points.js'; +import { newPoint } from '../../geo/point.js'; +import { newPolygon } from '../../geo/polygon.js'; +import { polygons as POLY } from '../../geo/polygons.js'; +import { checkOverUnderOn, intersectPoints } from '../../geo/slicer.js'; const { inRange, time } = base.util; const solid_opacity = 1.0; @@ -707,7 +711,7 @@ class Widget { } // used by CAM.shadowAt() this.cache.geo = pos; - this.cache.shadow = undefined; + delete this.cache.shadow; return pos; } @@ -835,6 +839,115 @@ class Widget { hide() { this.mesh.visible = false; } + + async shadowAt(z) { + let shadows = this.cache.shadows; + if (!shadows) { + shadows = this.cache.shadows = {}; + } + let cached = shadows[z]; + if (cached) { + return cached; + } + // find closest shadow above and use to speed up delta shadow gen + let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z); + let minZabove = Math.min(Infinity, ...zover); + let shadow = this.#computeShadowAt(z, minZabove); + if (minZabove < Infinity) { + shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true); + } + return shadows[z] = POLY.setZ(shadow, z); + } + + #ensureShadowCache() { + // cache faces with normals up + if (this.cache.shadow) { + return; + } + const geo = this.cache.geo; + const length = geo.length; + const bounds = this.getBoundingBox(); + const stack = {}; + // const faces = []; + for (let i = Math.floor(bounds.min.z); i <= Math.ceil(bounds.max.z); i++) { + stack[i] = []; + } + for (let i = 0, ip = 0; i < length; i += 3) { + const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); + const n = THREE.computeFaceNormal(a, b, c); + if (n.z < 0.001) { + continue; + // faces.push(a, b, c); + } + const minZ = Math.floor(Math.min(a.z, b.z, c.z)); + const maxZ = Math.ceil(Math.max(a.z, b.z, c.z)); + for (let z = minZ; z <= maxZ; z++) { + stack[z].push(a, b, c); + } + } + this.cache.shadow = stack; + } + + // union triangles > z (opt cap < ztop) into polygon(s) + #computeShadowAt(z, ztop) { + this.#ensureShadowCache(); + const found = []; + const stack = this.cache.shadow; + let minZ = Math.floor(z); + let maxZ = Math.ceil(z); + let slices = []; + for (let sz = minZ; sz <= maxZ; sz++) { + slices.push(stack[sz] ?? []); + } + for (let faces of slices) + for (let i = 0; i < faces.length; ) { + const a = faces[i++]; + const b = faces[i++]; + const c = faces[i++]; + if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) { + // skip faces over top threshold + continue; + } + if (a.z < z && b.z < z && c.z < z) { + // skip faces under threshold + continue; + } else if (a.z >= z && b.z >= z && c.z >= z) { + found.push([a, b, c]); + } else { + // check faces straddling threshold + const where = { under: [], over: [], on: [] }; + checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); + checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); + checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); + if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { + // compute two point intersections and construct line + let line = intersectPoints(where.over, where.under, z); + if (line.length === 2) { + if (where.over.length === 2) { + found.push([where.over[1], line[0], line[1]]); + found.push([where.over[0], where.over[1], line[0]]); + } else { + found.push([where.over[0], line[0], line[1]]); + } + } else { + console.log({ msg: "invalid ips", line: line, where: where }); + } + } + } + } + + let polys = found.map(a => { + return newPolygon() + .add(a[0].x, a[0].y, a[0].z) + .add(a[1].x, a[1].y, a[1].z) + .add(a[2].x, a[2].y, a[2].z); + }); + polys = POLY.union(polys, 0, true); + + return polys; + } } // Widget Grouping API diff --git a/src/kiri/mode/cam/anim-2d-be.js b/src/kiri/mode/cam/anim-2d-be.js index 720c74b0..92c0f6e0 100644 --- a/src/kiri/mode/cam/anim-2d-be.js +++ b/src/kiri/mode/cam/anim-2d-be.js @@ -9,14 +9,16 @@ const asLines = false; let stock, center, grid, gridX, gridY, rez; let path, pathIndex, tool, tools, last, toolID = 1; +let settings; export function init(worker) { const { dispatch } = worker; dispatch.animate_setup = function(data, send) { - const { settings } = data; + settings = data.settings; + const { process } = settings; - const print = worker.current.print; + const { print } = worker.current; const density = parseInt(settings.controller.animesh) * 1000; pathIndex = 0; @@ -287,7 +289,7 @@ function updateTool(toolobj, send) { if (tool) { send.data({ mesh_del: toolID }); } - tool = new Tool({ tools }, toolobj.getID()); + tool = new Tool(settings, toolobj.getID()); tool.generateProfile(rez); const flen = tool.fluteLength() || 15; const slen = tool.shaftLength() || 15; diff --git a/src/kiri/mode/cam/anim-3d-be.js b/src/kiri/mode/cam/anim-3d-be.js index 1288c65d..6f94fa2d 100644 --- a/src/kiri/mode/cam/anim-3d-be.js +++ b/src/kiri/mode/cam/anim-3d-be.js @@ -12,6 +12,7 @@ let nextMeshID = 1, tool, tools, last, + settings, stockZ, stockIndexMsg = false, stockSlices, @@ -34,10 +35,10 @@ export function init(worker) { const { dispatch } = worker; dispatch.animate_setup2 = function (data, send) { - const { settings } = data; + settings = data.settings; + const { controller, process } = settings; - const print = worker.current.print; - const density = parseInt(settings.controller.animesh) * 1000; + const { print } = worker.current; const isIndexed = process.camStockIndexed; pathIndex = 0; @@ -141,7 +142,7 @@ function renderPath(send) { } const id = toolID; - const rezstep = Math.min(tool.maxDiameter(), 1) / 4; + const rezstep = tool ? Math.min(tool.maxDiameter(), 1) / 4 : 1; // console.log({ rezstep }); if (last) { @@ -273,7 +274,7 @@ function toolUpdate(toolid, send) { if (tool) { send.data({ mesh_del: toolID }); } - tool = new Tool({ tools }, toolid); + tool = new Tool(settings, toolid); const Instance = CSG.Instance(); const slen = tool.shaftLength() || 15; const srad = tool.shaftDiameter() / 2; @@ -286,6 +287,16 @@ function toolUpdate(toolid, send) { mesh = cylinder(tlen - frad * 2, frad, frad, 20, true) .add(sphere(frad, 20).translate(0, 0, -(tlen - frad * 2) / 2)) .add(cylinder(slen, srad, srad, 20, true).translate(0, 0, flen / 2)); + } else if (tool.isTaperBall()) { + const trad = Math.max(tool.tipDiameter() / 2, 0.001); + const brad = trad; // ball radius equals tip radius + const taperLen = flen - brad; // taper length excludes ball + // shaft at top + mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2) + // taper cone in middle + .add(cylinder(taperLen, trad, frad, 20, true).translate(0, 0, -taperLen / 2)) + // ball at bottom + .add(sphere(brad, 20).translate(0, 0, -(taperLen + brad))); } else if (tool.isTaperMill()) { const trad = Math.max(tool.tipDiameter() / 2, 0.001); mesh = cylinder(slen, srad, srad, 20, true).translate(0, 0, slen / 2) diff --git a/src/kiri/mode/cam/cl-hole.js b/src/kiri/mode/cam/cl-hole.js index 77d703ab..49c2b593 100644 --- a/src/kiri/mode/cam/cl-hole.js +++ b/src/kiri/mode/cam/cl-hole.js @@ -157,7 +157,7 @@ export function selectHoleToggle(id,mesh) { export function clearHolesRec(widget) { if (widget.adds) { widget.adds.length = 0 //clear adds array - delete env.poppedRec.drills[widget.id] + if (env.poppedRec?.drills) delete env.poppedRec.drills[widget.id] } } diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index 80555902..948d42d6 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -140,7 +140,7 @@ export function createPopOp(type, map) { } }, addNote: () => { - if (!op.note && type !== 'flip') { + if (!op.note && type !== 'flip' && !op.rec.deprecated) { const divid = `div-${++seed}`; const noteid = `note-${++seed}`; const div = document.createElement('div'); @@ -227,8 +227,6 @@ export function createPopOps() { leave: 'camRoughStock', leavez: 'camRoughStockZ', all: 'camRoughAll', - voids: 'camRoughVoid', - flats: 'camRoughFlat', inside: 'camRoughIn', omitthru: 'camRoughOmitThru', ov_topz: 0, @@ -247,8 +245,6 @@ export function createPopOps() { leavez: UC.newInput(LANG.cr_lstz_s, { title: LANG.cr_lstz_l, convert: toFloat, bound: UC.bound(0, 10), units }), sep: UC.newBlank({ class: "pop-sep" }), all: UC.newBoolean(LANG.cr_clst_s, undefined, { title: LANG.cr_clst_l, show: hasIndexing }), - voids: UC.newBoolean(LANG.cr_clrp_s, undefined, { title: LANG.cr_clrp_l }), - flats: UC.newBoolean(LANG.cr_clrf_s, undefined, { title: LANG.cr_clrf_l }), inside: UC.newBoolean(LANG.cr_olin_s, undefined, { title: LANG.cr_olin_l }), omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), sep: UC.newBlank({ class: "pop-sep" }), @@ -268,12 +264,9 @@ export function createPopOps() { rate: 'camOutlineSpeed', plunge: 'camOutlinePlunge', dogbones: 'camOutlineDogbone', - omitvoid: 'camOutlineOmitVoid', + revbones: 'camOutlineRevbone', omitthru: 'camOutlineOmitThru', - outside: 'camOutlineOut', - inside: 'camOutlineIn', wide: 'camOutlineWide', - top: 'camOutlineTop', ov_topz: 0, ov_botz: 0, ov_conv: '~camConventional', @@ -287,14 +280,10 @@ export function createPopOps() { step: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.01, 1.0), show: () => env.popOp.outline.rec.wide }), steps: UC.newInput(LANG.cc_sovc_s, { title: LANG.cc_sovc_l, convert: toInt, bound: UC.bound(1, 500), show: () => env.popOp.outline.rec.wide }), sep: UC.newBlank({ class: "pop-sep" }), - top: UC.newBoolean(LANG.co_clrt_s, undefined, { title: LANG.co_clrt_l }), - inside: UC.newBoolean(LANG.co_olin_s, undefined, { title: LANG.co_olin_l, show: (op) => { return !op.inputs.outside.checked } }), - outside: UC.newBoolean(LANG.co_olot_s, undefined, { title: LANG.co_olot_l, show: (op) => { return !op.inputs.inside.checked } }), - sep: UC.newBlank({ class: "pop-sep" }), - omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l, xshow: (op) => { return op.inputs.outside.checked } }), - omitvoid: UC.newBoolean(LANG.co_omvd_s, undefined, { title: LANG.co_omvd_l, xshow: (op) => { return op.inputs.outside.checked } }), - wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l, show: (op) => { return !op.inputs.inside.checked } }), + omitthru: UC.newBoolean(LANG.co_omit_s, undefined, { title: LANG.co_omit_l }), + wide: UC.newBoolean(LANG.co_wide_s, undefined, { title: LANG.co_wide_l }), dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: (op) => { return !op.inputs.wide.checked } }), + revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), sep: UC.newBlank({ class: "pop-sep" }), exp: UC.newExpand("overrides"), ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), @@ -444,7 +433,6 @@ export function createPopOps() { refine: 'camPocketRefine', follow: 'camPocketFollow', contour: 'camPocketContour', - engrave: 'camPocketEngrave', outline: 'camPocketOutline', ov_topz: 0, ov_botz: 0, @@ -467,7 +455,6 @@ export function createPopOps() { follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }), sep: UC.newBlank({ class: "pop-sep" }), contour: UC.newBoolean(LANG.cp_cont_s, undefined, { title: LANG.cp_cont_s }), - engrave: UC.newBoolean(LANG.cp_engr_s, undefined, { title: LANG.cp_engr_l, show: () => env.poppedRec.contour }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), exp: UC.newExpand("overrides"), sep: UC.newBlank({ class: "pop-sep" }), @@ -564,12 +551,12 @@ export function createPopOps() { down: UC.newInput(LANG.ch_sdwn_s, {title:LANG.ch_sdwn_l, convert:toFloat, units:true}), startAng: UC.newInput(LANG.ch_stra_s, {title:LANG.ch_stra_l, convert:UC.toDegsFloat, bound:UC.bound(-360,360),show:() => env.poppedRec.forceStartAng}), offOver: UC.newInput(LANG.cc_offd_s, {title:LANG.cc_offd_l, convert:toFloat, units:true, bound:UC.bound(0,Infinity)}), - sep: UC.newBlank({class:"pop-sep"}), + sep: UC.newBlank({class:"pop-sep"}), entry: UC.newBoolean(LANG.ch_entr_s,undefined, {title:LANG.ch_entr_l}), entryOffset: UC.newInput(LANG.ch_ento_s, {title:LANG.ch_ento_l, convert:toFloat, units:true, show:() => env.poppedRec.entry}), reverse: UC.newBoolean(LANG.ch_rvrs_s,undefined, {title:LANG.ch_rvrs_l}), clockwise:UC.newBoolean(LANG.ch_clkw_s,undefined, {title:LANG.ch_clkw_l}), - sep: UC.newBlank({class:"pop-sep"}), + sep: UC.newBlank({class:"pop-sep"}), finish: UC.newBoolean(LANG.ch_fini_s,undefined, { title:LANG.ch_fini_l ,show: ()=>!env.poppedRec.reverse}), forceStartAng: UC.newBoolean(LANG.ch_fsta_s, undefined, {title:LANG.ch_fsta_l }), fromTop: UC.newBoolean(LANG.cd_ftop_s,undefined, {title:LANG.cd_ftop_l}), @@ -670,6 +657,8 @@ export function createPopOps() { return env.poppedRec.mode === 'surface' && env.poppedRec.sr_type === 'linear'; } + const open = true; + createPopOp('area', { spindle: 'camAreaSpindle', tool: 'camAreaTool', @@ -683,14 +672,16 @@ export function createPopOps() { plunge: 'camAreaPlunge', expand: 'camAreaExpand', smooth: 'camAreaSmooth', + follow: 'camAreaFollow', refine: 'camAreaRefine', outline: 'camAreaOutline', + tolerance: 'camTolerance', + dogbones: 'camAreaDogbones', + revbones: 'camAreaRevbones', ov_topz: 0, ov_botz: 0, - ov_conv: '~camConventional', - tolerance: 'camTolerance', + direction: 'camAreaDirection', }).inputs = { - tool: UC.newSelect(LANG.cc_tool, {}, "tools"), mode: UC.newSelect(LANG.mo_menu, {}, "opmode"), tr_type: UC.newSelect(LANG.cc_offs_s, { title: LANG.cc_offs_l, show: isTrace }, "traceoff"), sr_type: UC.newSelect("pattern", { title: "pattern", show: isSurface }, "surftyp"), @@ -701,26 +692,29 @@ export function createPopOps() { ], { class: "ext-buttons f-row" }), outline: UC.newBoolean(LANG.cp_outl_s, undefined, { title: LANG.cp_outl_l }), expand: UC.newInput(LANG.cp_xpnd_s, { title: LANG.cp_xpnd_l, convert: toFloat, units }), - sep: UC.newBlank({ class: "pop-sep" }), + smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, xshow: isSurface }), + follow: UC.newInput(LANG.cp_foll_s, { title: LANG.cp_foll_l, convert: toFloat }), + tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }), + exp: UC.newExpand("tool & stepping", { open }), + tool: UC.newSelect(LANG.cc_tool, {}, "tools"), + direction: UC.newSelect(LANG.ou_dire_s, { title: LANG.ou_dire_l }, "direction"), sr_angle: UC.newInput("step angle", { title: "step angle", convert: toFloat, bound: UC.bound(0, 360), show: isSurfaceLinear }), over: UC.newInput(LANG.cc_sovr_s, { title: LANG.cc_sovr_l, convert: toFloat, bound: UC.bound(0.001, 100.0), show: () => isClear() || isSurface() }), down: UC.newInput(LANG.cc_sdwn_s, { title: LANG.cc_sdwn_l, convert: toFloat, bound: UC.bound(0, 100.0), units, show: () => isClear() || isTrace() }), - sep: UC.newBlank({ class: "pop-sep", show: isSurface }), refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }), - smooth: UC.newInput(LANG.cp_smoo_s, { title: LANG.cp_smoo_l, convert: toInt, show: isSurface }), - tolerance: UC.newInput(LANG.ou_toll_s, { title: LANG.ou_toll_l, convert: toFloat, bound: UC.bound(0, 10.0), units, round: 4, show: isSurface }), - sep: UC.newBlank({ class: "pop-sep" }), - exp: UC.newExpand("feeds & speeds"), + dogbones: UC.newBoolean(LANG.co_dogb_s, undefined, { title: LANG.co_dogb_l, show: isTrace }), + revbones: UC.newBoolean(LANG.co_dogr_s, undefined, { title: LANG.co_dogr_l, show: () => env.poppedRec.dogbones }), + exp_end: UC.endExpand(), + exp: UC.newExpand("feeds & speeds", { open }), sep: UC.newBlank({ class: "pop-sep" }), spindle: UC.newInput(LANG.cc_spnd_s, { title: LANG.cc_spnd_l, convert: toInt, show: hasSpindle }), rate: UC.newInput(LANG.cc_feed_s, { title: LANG.cc_feed_l, convert: toInt, units }), plunge: UC.newInput(LANG.cc_plng_s, { title: LANG.cc_plng_l, convert: toInt, units }), exp_end: UC.endExpand(), - exp: UC.newExpand("overrides"), + exp: UC.newExpand("bounds", { open }), sep: UC.newBlank({ class: "pop-sep" }), ov_topz: UC.newInput(LANG.ou_ztop_s, { title: LANG.ou_ztop_l, convert: toFloat, units }), ov_botz: UC.newInput(LANG.ou_zbot_s, { title: LANG.ou_zbot_l, convert: toFloat, units }), - ov_conv: UC.newBoolean(LANG.ou_conv_s, undefined, { title: LANG.ou_conv_l }), exp_end: UC.endExpand(), }; diff --git a/src/kiri/mode/cam/client.js b/src/kiri/mode/cam/client.js index 39529575..ec7c98fc 100644 --- a/src/kiri/mode/cam/client.js +++ b/src/kiri/mode/cam/client.js @@ -291,7 +291,12 @@ export function opRender() { let clazz = notime ? ["draggable", "notime"] : ["draggable"]; let notable = rec.note ? rec.note.split(' ').filter(v => v.charAt(0) === '#') : undefined; if (clock) { clazz.push('clock'); title = ` title="end of ops chain\ndrag/drop like an op\nops after this are disabled"` } - if (notable?.length) label += ` (${notable[0].slice(1)})`; + if (notable?.length) { + rec.rename = notable[0].slice(1); + label += ` (${rec.rename})`; + } else { + delete rec.rename; + } html.appendAll([ `
`, ``, @@ -526,10 +531,6 @@ export function opRender() { export function init() { - if (api.devel.enabled) { - $('op:area').classList.remove('hide'); - } - api.event.on('tool.mesh.face-normal', normal => { // console.log({ env.poppedRec }); env.poppedRec.degrees = (Math.atan2(normal.y, normal.z) * RAD2DEG).round(2); diff --git a/src/kiri/mode/cam/export.js b/src/kiri/mode/cam/export.js index 1a6f9866..7dfb657c 100644 --- a/src/kiri/mode/cam/export.js +++ b/src/kiri/mode/cam/export.js @@ -256,6 +256,11 @@ export function cam_export(print, online) { return; } + // skip arc points for display only + if (out.emit === -1) { + return; + } + let dx = opt.dx || newpos.x - pos.x, dy = opt.dy || newpos.y - pos.y, dz = opt.dz || newpos.z - pos.z, diff --git a/src/kiri/mode/cam/init-menu.js b/src/kiri/mode/cam/init-menu.js index c1ae578b..0be5b92e 100644 --- a/src/kiri/mode/cam/init-menu.js +++ b/src/kiri/mode/cam/init-menu.js @@ -2,6 +2,7 @@ import { api } from '../../core/api.js'; import { originReset, originSelect } from './cl-origin.js'; +import { updateTool } from './tools.js'; let LANG = api.language.current; let { CAM } = api.const.MODES, @@ -32,8 +33,37 @@ function zAnchorSave() { export function menu() { let anim = ui.anim = {}; + uc.setGroup($('tool-details')); + return { + /** Tool Editor Menu */ + + _____: uc.setGroup($('tool-details')), + toolName: newInput('name', { title:'tool name', id: 'tool-name', size: 0, text: true }), + toolType: newSelect('type', { title: 'tool type', id: 'tool-type', action:updateTool }, "camtool"), + toolNum: newInput('tool #', { title:'tool number', id: 'tool-num', convert: toInt }), + toolMetric: newBoolean('metric', updateTool, { title: 'metric', id: 'tool-metric' }), + _____: newGroup(LANG.td_shft), + toolShaftDiam: newInput('diameter', { convert: toFloat }), + toolShaftLen: newInput('length', { convert: toFloat }), + _____: newGroup(LANG.td_flut), + toolFluteDiam: newInput('diameter', { convert: toFloat }), + toolFluteLen: newInput('length', { convert: toFloat }), + _____: newGroup(LANG.td_tapr), + toolTaperAngle: newInput('angle', { convert: toFloat }), + toolTaperTip: newInput('tip', { convert: toFloat }), + // toolTaperAngle: $('tool-tangle'), + + toolsSave: $('tools-save'), + toolsClose: $('tools-close'), + toolsImport: $('tools-import'), + toolsExport: $('tools-export'), + toolSelect: $('tool-select'), + toolAdd: $('tool-add'), + toolCopy: $('tool-dup'), + toolDelete: $('tool-del'), + /** Animation Bar */ _____: { @@ -92,8 +122,9 @@ export function menu() { camZBottom: newInput(LANG.ou_zbot_s, {title:LANG.ou_zbot_l, convert:toFloat, units, trigger}), camZThru: newInput(LANG.ou_ztru_s, {title:LANG.ou_ztru_l, convert:toFloat, bound:bound(0.0,100), units }), camZClearance: newInput(LANG.ou_zclr_s, {title:LANG.ou_zclr_l, convert:toFloat, bound:bound(0.01,100), units }), - camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), + separator: newBlank({ class:"set-sep", driven }), camFastFeed: newInput(LANG.cc_rapd_s, {title:LANG.cc_rapd_l, convert:toFloat, units}), + camFastFeedZ: newInput(LANG.cc_rzpd_s, {title:LANG.cc_rzpd_l, convert:toFloat, units}), _____: newGroup(LANG.ou_menu, $('cam-output'), { modes:CAM, driven, separator, group:"cam-output" }), camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}), camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}), @@ -119,12 +150,10 @@ export function menu() { newButton("reset", originReset), ], { class: "ext-buttons f-row" }), _____: newGroup(LANG.op_xprt_s, $('cam-expert'), { group:"cam_expert", modes:CAM, marker: false, driven, separator }), - camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, {title:LANG.cx_fast_l, show: () => !ui.camTrueShadow.checked }), - camTrueShadow: newBoolean(LANG.cx_true_s, onBooleanClick, {title:LANG.cx_true_l, show: () => !ui.camExpertFast.checked }), - separator: newBlank({ class:"set-sep", driven }), - camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, {title:LANG.cx_arce_l}), + camArcEnabled: newBoolean(LANG.cx_arce_s, onBooleanClick, { title:LANG.cx_arce_l }), camArcTolerance: newInput(LANG.cx_arct_s, {title:LANG.cx_arct_l, convert:toFloat, bound:bound(0,100), units, trigger, show:() => ui.camArcEnabled.checked}), camArcResolution: newInput(LANG.cx_arcr_s, {title:LANG.cx_arcr_l, convert:toFloat, bound:bound(0,180), trigger, show:() => ui.camArcEnabled.checked}), + camExpertFast: newBoolean(LANG.cx_fast_s, onBooleanClick, { title:LANG.cx_fast_l }), }; }; diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 7ead88f1..e86ee218 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -1,5 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ +// todo: surface offset pattern +// todo: trace dogbones, merge overlap + import { CamOp } from './op.js'; import { Tool } from './tool.js'; import { newSlice } from '../../core/slice.js'; @@ -22,16 +25,19 @@ class OpArea extends CamOp { async slice(progress) { let { op, state } = this; - let { tool, mode, down, over, follow, expand, outline, refine, smooth } = op; - let { ov_topz, ov_botz, ov_conv } = op; + let { tool, mode, down, over, follow, expand, outline, smooth } = op; + let { ov_topz, ov_botz, direction, rename } = op; let { settings, widget, tabs, color } = state; let { addSlices, setToolDiam, cutTabs, healPolys, shadowAt, workarea } = state; let areaTool = new Tool(settings, tool); + let smoothVal = (smooth ?? 0) / 10; let toolDiam = areaTool.fluteDiameter(); - let toolOver = areaTool.hasTaper() ? over : toolDiam * over; - let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_stock; - let zBottom = ov_botz ? workarea.bottom_stock + ov_botz : workarea.bottom_part; + let toolOver = areaTool.getStepSize(over); + let zTop = ov_topz ? workarea.bottom_stock + ov_topz : workarea.top_z; + let zBottom = ov_botz ? workarea.bottom_stock + ov_botz : Math.max(workarea.bottom_z, workarea.bottom_part); + let shadowBase = state.shadow.base; + let thruHoles = state.shadow.holes; // also updates tab offsets setToolDiam(toolDiam); @@ -62,7 +68,7 @@ class OpArea extends CamOp { // connect open poly edge segments into closed loops (when possible) // surface and edge selections produce open polygons by default - polys = POLY.nest(healPolys(polys)); + polys = POLY.nest(healPolys(polys, false)); // gather surface selections let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); @@ -83,7 +89,9 @@ class OpArea extends CamOp { // add in unioned surface areas polys.push(...POLY.setZ(POLY.union(fpoly, 0.00001, true), fminz)); - // todo: implement `refine` and `smooth` + // smoothing for jaggies usually caused by vertical walls + if (smoothVal) + polys = polys.map(poly => POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal)).flat(); // expand selections (flattens z variable polys) if (Math.abs(expand) > 0) { @@ -95,7 +103,9 @@ class OpArea extends CamOp { nupolys.push(...expanded.flat()); } } - polys = nupolys; + // polys = nupolys; + // re-merge after expansion in case it produces overlap + polys = POLY.union(nupolys, 0.00001, true); } // process each area separately @@ -104,17 +114,17 @@ class OpArea extends CamOp { for (let area of polys) { let bounds = area.getBounds3D(); - if (devel) newLayer().output() - .setLayer("area", { line: 0xff8800 }, false) - .addPolys([ area ]); - - newArea(); - if (outline) { // remove inner voids when processing outline only area.inner = undefined; } + newLayer().output() + .setLayer("area", { line: 0xff8800 }, false) + .addPolys([ area ]); + + newArea(); + if (mode === 'clear') { let zs = down ? base_util.lerp(zTop, zBottom, down) : [ bounds.min.z ]; let zroc = 0; @@ -126,7 +136,22 @@ class OpArea extends CamOp { let layers = slice.output(); let outs = []; let clip = []; - let shadow = shadowAt(z); + let shadow = await shadowAt(z); + let tool_shadow = POLY.offset(shadow, [ toolDiam / 2 - 0.01 ], { count: 1, z }); + // for roughing backward compatability + if (op.omitthru) { + shadow = shadow.clone(true); + for (let poly of shadow.filter(p => p.inner)) { + poly.inner = poly.inner.filter(inner => { + for (let ho of thruHoles) { + if (inner.isEquivalent(ho)) { + return false; + } + } + return true; + }); + } + } POLY.subtract([ area ], shadow, clip, undefined, undefined, 0); POLY.offset(clip, [ -toolDiam / 2, -toolOver ], { count: 999, outs, flat: true, z, minArea: 0 @@ -142,16 +167,30 @@ class OpArea extends CamOp { break outer; } // cut tabs when present - if (tabs) outs = cutTabs(tabs, outs); + if (tabs.length) outs = cutTabs(tabs, outs); + // for roughing backward compatability + if (op.leave_z) { + for (let out of outs) + for (let p of out) + p.z += op.leave_z; + } + if (op.leave_xy) { + outs = outs.map(poly => poly.offset(-op.leave_xy)).flat(); + } + slice.tool_shadow = tool_shadow; slice.camLines = outs; zroc += zinc; lzo = z; progress(proc + (pinc * zroc), 'clear'); if (devel) layers - .setLayer("shadow", { line: 0x0088ff }, false) - .addPolys(shadow); + .setLayer("base", { line: 0xff0000 }, false) + .addPolys(shadowBase) + .setLayer("shadow", { line: 0x00ff00 }, false) + .addPolys(shadow) + .setLayer("tool shadow", { line: 0x44ff88 }, false) + .addPolys(tool_shadow); layers - .setLayer("clear", { line: 0x88ff00 }, false) + .setLayer(rename ?? "clear", { line: 0x88ff00 }, false) .addPolys(outs); // of the last output still cuts, we need an escape if (z === zs.peek()) { @@ -166,15 +205,16 @@ class OpArea extends CamOp { let zs = down ? base_util.lerp(zTop, bounds.min.z, down) : [ bounds.min.z ]; let zroc = 0; let zinc = 1 / zs.length; - let lzo; for (let z of zs) { let slice = newLayer(); let layers = slice.output(); let outs = []; if (tr_type === 'none') { + // todo: move this out of the zs loop and only setZ when needed area = area.clone(true); outs = [ zs.length > 1 ? area.setZ(z) : area ]; } else { + // todo: move this out of the zs loop POLY.offset([ area ], tr_type === 'inside' ? [ -toolDiam / 2 ] : [ toolDiam / 2 ], { count: 1, outs, flat: true, z, minArea: 0 }); @@ -183,14 +223,16 @@ class OpArea extends CamOp { // terminate z descent when no further output possible break; } + // add dogbones when specified + if (op.dogbones) outs.forEach(out => out.addDogbones(toolDiam / 5, op.revbones)); // cut tabs when present if (tabs) outs = cutTabs(tabs, outs); slice.camLines = outs; + slice.tool_shadow = POLY.offset(await shadowAt(z), [ toolDiam / 2 - 0.01 ], { count: 1, z }); zroc += zinc; - lzo = z; progress(proc + (pinc * zroc), 'trace'); layers - .setLayer("trace", { line: 0x88ff00 }, false) + .setLayer(rename ?? "trace", { line: 0x88ff00 }, false) .addPolys(outs); } proc += pinc; @@ -226,8 +268,12 @@ class OpArea extends CamOp { paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); } else if (sr_type === 'offset') { - // todo: progressive inset from perimeter - console.log({ sr_offset: toolOver }); + // progressive inset from perimeter + POLY.offset([ area ], [ -toolDiam / 2, -toolOver ], { + count: 999, outs: paths, flat: true, z: 0, minArea: 0 + }); + paths.forEach(poly => poly.isClosed() && poly.push(poly.first())); + paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); } // prepare tool mesh points @@ -265,9 +311,10 @@ class OpArea extends CamOp { // convert terrain raster output back to open polylines for (let path of output.paths) { path = newPolygon().fromArray([1, ...path]); + if (op.refine) path.refine(op.refine); surface.push(path); newLayer().output() - .setLayer("linear", { line: 0x00ff00 }, false) + .setLayer(rename ?? "linear", { line: 0x00ff00 }, false) .addPolys([ path ]); } @@ -281,29 +328,28 @@ class OpArea extends CamOp { prepare(ops, progress) { let { op, state, areas, surfaces } = this; - let { getPrintPoint, newLayer, pocket, polyEmit, setTool, setSpindle, tip2tipEmit } = ops; + let { newLayer, pocket, polyEmit, printPoint, tip2tipEmit } = ops; + let { setContouring, setNextIsMove } = ops; let { process } = state.settings; - setTool(op.tool, op.rate); - setSpindle(op.spindle); - - let printPoint = getPrintPoint(); - // process surface paths - for (let surface of surfaces) { - let array = surface.map(poly => { return { - el: poly, - first: poly.first(), - last: poly.last() - } }); - tip2tipEmit(array, printPoint, (next, first, count) => { - printPoint = polyEmit(next.el, 0, 1, printPoint, {}); - newLayer(); - }); - } - - // skip areas when processing surfaces if (surfaces.length) { + setContouring(true); + for (let surface of surfaces) { + let array = surface.map(poly => { return { + el: poly, + first: poly.first(), + last: poly.last() + } }); + tip2tipEmit(array, printPoint, (next, point) => { + setNextIsMove(); + if (next.last === point) next.el.reverse(); + printPoint = polyEmit(next.el); + newLayer(); + }); + } + setContouring(false); + // skip areas when processing surfaces return; } @@ -313,7 +359,7 @@ class OpArea extends CamOp { dist: Infinity, area: undefined }; - for (let area of areas.filter(p => !p.used)) { + for (let area of areas.filter(p => p.length && !p.used)) { // skip devel / debug only areas let topPolys = area[0].camLines; if (!topPolys) continue; diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index 9f5d7021..f5528a9c 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -83,10 +83,13 @@ class OpContour extends CamOp { async slice(progress) { let { op, state } = this; let { color, addSlices, settings, updateToolDiams } = state; + let conTool = new Tool(settings, op.tool); let filter = createFilter(op, settings.origin, op.axis.toLowerCase()); - let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); + let toolDiam = this.toolDiam = conTool.fluteDiameter(); + this.toolStep = conTool.getStepSize(op.step); updateToolDiams(toolDiam); + // we need topo for safe travel moves when roughing and outlining // not generated when drilling-only. then all z moves use bounds max. // also generates x and y contouring when selected @@ -114,21 +117,19 @@ class OpContour extends CamOp { } prepare(ops, progress) { - let { op, state, sliceOut } = this; + let { op, state, sliceOut, toolStep } = this; let { settings } = state; let { process } = settings; - let { setTolerance, setTool, setSpindle } = ops; + let { polyEmit, setContouring, setTolerance, setTool } = ops; let { widget, camOut, newLayer, zmax } = ops; let bounds = widget.getBoundingBox(); let toolDiam = this.toolDiam; - let stepover = toolDiam * op.step * 2; - let depthFirst = process.camDepthFirst; let depthData = []; setTool(op.tool, op.rate, process.camFastFeedZ); - setSpindle(op.spindle); + setContouring(true, toolStep); setTolerance(this.tolerance); let printPoint = newPoint(bounds.min.x, bounds.min.y, zmax); @@ -139,38 +140,21 @@ class OpContour extends CamOp { continue; } let polys = [], poly; - slice.camLines.forEach(function (poly) { - if (depthFirst) poly = poly.clone(true).annotate({ slice: slice.index + 1 }); + slice.camLines.forEach((poly) => { + poly = poly.clone(true).annotate({ slice: slice.index + 1 }); polys.push({ first: poly.first(), last: poly.last(), poly: poly }); }); - if (depthFirst) { - depthData.appendAll(polys); - } else { - printPoint = tip2tipEmit(polys, printPoint, function (el, point, count) { - poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint(function (point, pidx) { - camOut(point.clone(), pidx > 0, { moveLen: stepover }); - }, false); - }); - newLayer(); - } + depthData.appendAll(polys); } - if (depthFirst) { - tip2tipEmit(depthData, printPoint, function (el, point, count) { - let poly = el.poly; - if (poly.last() === point) { - poly.reverse(); - } - poly.forEachPoint(function (point, pidx) { - camOut(printPoint = point.clone().annotate({ slice: poly.slice }), pidx > 0, { moveLen: stepover }); - }, false); - newLayer(); - }); - } + tip2tipEmit(depthData, printPoint, (el, point) => { + let poly = el.poly; + if (poly.last() === point) { + poly.reverse(); + } + polyEmit(poly); + newLayer(); + }); } } diff --git a/src/kiri/mode/cam/op-drill.js b/src/kiri/mode/cam/op-drill.js index 7ce27232..319d9ebb 100644 --- a/src/kiri/mode/cam/op-drill.js +++ b/src/kiri/mode/cam/op-drill.js @@ -21,9 +21,11 @@ class OpDrill extends CamOp { drillToolDiam = drillTool.fluteDiameter(), sliceOut = this.sliceOut = []; + const allDrills = drills[widget.id] ?? [] + if (allDrills.length === 0) return; + updateToolDiams(drillToolDiam); - const allDrills = drills[widget.id] ?? [] // drill points to use center (average of all points) of the polygon allDrills.forEach((drill) => { if (!drill.selected) { @@ -61,9 +63,10 @@ class OpDrill extends CamOp { let { op, sliceOut } = this; let { setTool, setSpindle, setDrill, emitDrills } = ops; + if (sliceOut.length === 0) return; + setTool(op.tool, undefined, op.rate); setDrill(op.down, op.lift, op.dwell); - setSpindle(op.spindle); emitDrills(sliceOut.map(slice => slice.camLines).flat()); } } diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/op-helical.js index be1aa20d..a75ea632 100644 --- a/src/kiri/mode/cam/op-helical.js +++ b/src/kiri/mode/cam/op-helical.js @@ -240,11 +240,10 @@ export class OpHelical extends CamOp { } async prepare(ops, progress) { - let { polyEmit, printPoint, setTool, setSpindle } = ops; + let { polyEmit, printPoint, setTool } = ops; let { tool, spindle, rate, feed } = this.op; setTool(tool, feed, rate); - setSpindle(spindle); let [ polys ] = this.sliceOut.map((s) => s.camLines); polys = polys.slice(); @@ -268,7 +267,7 @@ export class OpHelical extends CamOp { if (!closest) break; poly = polys[closestI] polys[closestI] = null; - printPoint = polyEmit(poly, 0, 1, poly.points[0]) + printPoint = polyEmit(poly); } } } diff --git a/src/kiri/mode/cam/op-index.js b/src/kiri/mode/cam/op-index.js index 136dd349..341ed456 100644 --- a/src/kiri/mode/cam/op-index.js +++ b/src/kiri/mode/cam/op-index.js @@ -25,12 +25,12 @@ export class OpIndex extends CamOp { } prepare(ops, progress) { - const { zmax, zsafe, camOut } = ops; + let { zmax, zsafe, camOut, printPoint } = ops; // max point of stock corner radius when rotating (safe z when indexing) - const ztop = Math.max(zsafe, zmax); + let ztop = Math.max(zsafe, zmax); // move above rotating stock - camOut(last = last.clone().setZ(ztop), 0); + camOut(printPoint = printPoint.clone().setZ(ztop), 0); // issue rotation command - camOut(last = last.clone().setZ(ztop).setA(this.degrees), 0); + camOut(printPoint = printPoint.clone().setZ(ztop).setA(this.degrees), 0); } } diff --git a/src/kiri/mode/cam/op-lathe.js b/src/kiri/mode/cam/op-lathe.js index da0e451d..7271cd08 100644 --- a/src/kiri/mode/cam/op-lathe.js +++ b/src/kiri/mode/cam/op-lathe.js @@ -54,22 +54,13 @@ class OpLathe extends CamOp { } prepare(ops, progress) { - let { op, state, slices, topo } = this; - let { settings } = state; + let { op, slices, topo } = this; + let { camOut, newLayer, zSafe } = ops; - let { setTool, setSpindle } = ops; - let { camOut, newLayer, printPoint } = ops; - let { zmax } = ops; - - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let stepover = toolDiam * op.step * 2; let rez = topo.resolution; - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - // start top center, X = 0, Y = 0 closest to 4th axis chuck - printPoint = newPoint(0, 0, zmax); + camOut(newPoint(0, 0, zSafe), 0); for (let slice of slices) { // ignore debug slices @@ -90,14 +81,14 @@ class OpLathe extends CamOp { return; } if (latent) { - camOut(latent, true, stepover); + camOut(latent, 1); latent = undefined; } } - camOut(last = point.clone(), pidx > 0, stepover); + camOut(last = point.clone(), pidx > 0 ? 1 : 0); }, false); if (latent) { - camOut(latent, true, stepover); + camOut(latent, 1); } } @@ -110,7 +101,7 @@ class OpLathe extends CamOp { // camOut(last = last.clone().setZ(zmax), 0); // camOut(last = last.clone().setA(amax), 0); newLayer(); - ops.addGCode([`G0 Z${zmax.round(2)}`, `G0 A${amax}`, "G92 A0"]); + ops.addGCode([`G0 Z${zSafe.round(2)}`, `G0 A${amax}`, "G92 A0"]); } } diff --git a/src/kiri/mode/cam/op-level.js b/src/kiri/mode/cam/op-level.js index 4cac15c1..39260ba6 100644 --- a/src/kiri/mode/cam/op-level.js +++ b/src/kiri/mode/cam/op-level.js @@ -14,22 +14,23 @@ class OpLevel extends CamOp { async slice(progress) { let { op, state } = this; - let { addSlices, color, settings, tshadow, updateToolDiams, zMax, ztOff } = state; + let { addSlices, color, settings, shadow, updateToolDiams, zMax, ztOff } = state; + let { down, tool, step, stepz, inset } = op; let { stock } = settings; - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let stepOver = this.stepOver = toolDiam * op.step; + let toolDiam = new Tool(settings, tool).fluteDiameter(); + let stepOver = this.stepOver = toolDiam * step; let wpos = state.widget.track.pos; let zTop = zMax + ztOff; - let zBot = zTop - op.down; - let zList = op.stepz ? util.lerp(zTop, zBot, op.stepz) : [ zBot ]; + let zBot = zTop - down; + let zList = stepz && down ? util.lerp(zTop, zBot, stepz) : [ zBot ]; updateToolDiams(toolDiam); let points = []; let clear = op.stock ? [ newPolygon().centerRectangle({x:-wpos.x,y:-wpos.y,z:wpos.z}, stock.x + toolDiam/2, stock.y) ] : - POLY.outer(POLY.offset(tshadow, toolDiam * (op.inset || 0))); + POLY.outer(POLY.offset(shadow.base, toolDiam * (inset || 0))); POLY.fillArea(clear, 1090, stepOver, points); @@ -49,12 +50,10 @@ class OpLevel extends CamOp { } prepare(ops, progress) { - let { op, layers, stepOver } = this; - let { setTool, setSpindle, printPoint } = ops; + let { layers, stepOver } = this; + let { printPoint } = ops; let { newLayer, tip2tipEmit, camOut } = ops; - setTool(op.tool, op.rate); - setSpindle(op.spindle); for (let lines of layers) { lines = lines.map(p => { return { first: p.first(), last: p.last(), poly: p } }); printPoint = tip2tipEmit(lines, printPoint, (el, point, count) => { diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js index 9169ccbe..8c369f54 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/op-outline.js @@ -1,311 +1,46 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; +import { OpArea } from './op-area.js'; import { polygons as POLY } from '../../../geo/polygons.js'; -import { util as base_util } from '../../../geo/base.js'; -import { poly2polyEmit } from '../../../geo/paths.js'; -import { newPoint } from '../../../geo/point.js'; -import { addDogbones } from './slice.js'; +import { newPolygon } from '../../../geo/polygon.js'; class OpOutline extends CamOp { constructor(state, op) { super(state, op); } + // todo: wide cutout, dogbones + async slice(progress) { let { op, state } = this; - let { settings, slicer, addSlices, tshadow, thruHoles, unsafe, color, widget } = state; - let { updateToolDiams, tabs, cutTabs, cutPolys, workarea, zMax, shadowAt } = state; - let { process, stock } = settings; - let { controller } = settings; - let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0}; - - if (op.down <= 0) { - throw `invalid step down "${op.down}"`; - } - let toolDiam = this.toolDiam = new Tool(settings, op.tool).fluteDiameter(); - updateToolDiams(toolDiam); - - let shadow = []; - let slices = []; - let intopt = { - off: 0.01, - fit: true, - down: true, - min: Math.max(0, workarea.bottom_z), - max: workarea.top_z + let { shadow, tool, widget } = state; + let areas = POLY.flatten(POLY.expand(shadow.base, tool.fluteDiameter() / 2 - 0.001)); + let cutout = { + areas: { [widget.id]: areas.map(p => p.toArray()) }, + dogbones: op.dogbones, + down: op.down, + expand: 0, + mode: 'trace', + outline: op.omitthru, + ov_botz: op.ov_botz, + ov_topz: op.ov_topz, + plunge: op.plunge, + rate: op.rate, + rename: op.rename ?? "outline", + revbones: op.revbones, + smooth: 0, + spindle: op.spindle, + surfaces: {}, + tool: op.tool, + tr_type: 'none', }; - let indices = slicer.interval(op.down, intopt); - let trueShadow = process.camTrueShadow === true; - let lastShadowZ; - let stockClip; - - // shift out first (top-most) slice - indices.shift(); - // add flats to shadow - const flats = Object.keys(slicer.zFlat) - .map(v => (parseFloat(v) - 0.01).round(5)) - .filter(v => v > 0 && indices.indexOf(v) < 0); - indices = indices.appendAll(flats).sort((a,b) => b-a); - let cnt = 0; - let tot = 0; - if (op.outside && !op.inside) { - // console.log({outline_bypass: indices, down: op.down}); - indices.forEach((ind,i) => { - if (flats.indexOf(ind) >= 0) { - // exclude flats - return; - } - let slice = newSlice(ind); - slice.shadow = shadow.clone(true); - slices.push(slice); - }); - } else - await slicer.slice(indices, { each: data => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - data.shadow = trueShadow ? shadowAt(data.z, lastShadowZ) : shadow.clone(true); - data.slice.shadow = data.shadow; - // data.slice.tops[0].inner = data.shadow; - // data.slice.tops[0].inner = POLY.setZ(tshadow.clone(true), data.z); - slices.push(data.slice); - // data.slice.xray(); - // onupdate(0.2 + (index/total) * 0.1, "outlines"); - progress(0.5 + 0.5 * (++cnt / tot)); - lastShadowZ = data.z; - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.5); - } }); - shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); - - // start slices at top of stock when `clear top` enabled - if (op.top) { - let first = slices[0]; - let zlist = slices.map(s => s.z); - for (let z of indices.filter(v => v >= zMax)) { - if (zlist.contains(z)) { - continue; - } - let add = first.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = first.shadow.clone(true); - add.z = z; - slices.splice(0,0,add); - } - } - - // extend cut thru (only when z bottom is 0) - if (workarea.bottom_z < 0) { - let last = slices[slices.length-1]; - // when step down > full cut depth, clear slices and - // leave only the cut-thru pass(es) - if (op.down > workarea.top_stock - workarea.bottom_part) { - slices.length = 0; - } - for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - let add = last.clone(true); - add.tops.forEach(top => top.poly.setZ(add.z)); - add.shadow = last.shadow.clone(true); - add.z -= zneg; - slices.push(add); - } - } - - slices.forEach(slice => { - let tops = slice.shadow; - - // outside only (use tshadow for entire cut) - if (op.outside) { - tops = tshadow; - } - - if (op.omitthru) { - // eliminate thru holes from shadow - for (let hole of thruHoles) { - for (let top of tops) { - if (!top.inner) continue; - top.inner = top.inner.filter(innr => { - return !innr.isEquivalent(hole, false, 0.1); - }); - } - } - } - - if (op.omitvoid) { - for (let top of tops) { - delete top.inner; - } - } - - let offset = POLY.expand(tops, toolDiam / 2, slice.z); - if (!(offset && offset.length)) { - return; - } - - // when pocket only, drop first outer poly - // if it matches the shell and promote inner polys - if (op.inside) { - let shell = POLY.expand(tops.clone(), toolDiam / 2); - offset = POLY.filter(offset, [], function(poly) { - if (poly.area() < 1) { - return null; - } - for (let sp=0; sp 0; c--){ - let wideCut = POLY.expand(offset.clone(true), stepover * c, slice.z, [], 1); - wideCut.forEach(cut =>{ //set order of cuts when wide - cut.order = c - if(cut.inner) cut.inner.forEach(inn =>{ inn.order = c }) - }); - wideCuts.push(...wideCut) - } - offset.appendAll(wideCuts); - } - } - - if (op.dogbones && !op.wide) { - addDogbones(offset, toolDiam / 5); - } - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let { center } = stock; - let x = center.x - center_off.x; - let y = center.y - center_off.y; - stockClip = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001); - offset = cutPolys([stockClip], offset, slice.z, true); - } - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - offset = cutTabs(tabs, offset); - } - - // offset.xout(`slice ${slice.z}`); - slice.camLines = offset; - }); - - // when top expand fails above, it creates an empty slice - slices = slices.filter(s => s.camLines); - - // project empty up and render - for (let slice of slices) { - if (controller.devel) slice.output() - .setLayer("slice", {line: 0xaaaa00}, false) - .addPolys(slice.topPolys()) - if (controller.devel) slice.output() - .setLayer("shadow", {line: 0x557799}, false) - .addPolys(slice.shadow) - if (controller.devel && stockClip) slice.output() - .setLayer("stock clip", {line: 0x557799}, false) - .addPolys([ stockClip ]) - slice.output() - .setLayer(state.layername, {face: color, line: color}) - .addPolys(slice.camLines); - } - - addSlices(slices); - this.sliceOut = slices; + this.op_cutout = new OpArea(state, cutout); + return this.op_cutout.slice(progress); } - prepare(ops, progress) { - let { op, state, sliceOut } = this; - let { settings } = state; - let { process } = settings; - let { depthOutlinePath, newLayer, polyEmit, printPoint, setTool, setSpindle } = ops; - - let cutdir = op.ov_conv; - let depthFirst = process.camDepthFirst; - let depthData = []; - let easeDown = process.camEaseDown; - let toolDiam = this.toolDiam; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // printPoint becomes NaN in engine mode - if (Object.values(printPoint).some(v => Number.isNaN(v))) { - printPoint = newPoint(0, 0, 0); - } - - for (let slice of sliceOut) { - let polys = [], t = [], c = []; - let lines =POLY.flatten(slice.camLines) - // console.log(lines); - lines.forEach((poly)=> { - poly.order = poly.order ?? 0; - let child = poly.parent; - if (depthFirst) { poly = poly.clone(); poly.parent = child ? 1 : 0 } - if (child) c.push(poly); else t.push(poly); - poly.layer = depthData.layer; - polys.push(poly); - }); - - // set cut direction on outer polys - POLY.setWinding(t, !cutdir); - // set cut direction on inner polys - POLY.setWinding(c, cutdir); - - if (depthFirst) { - depthData.push(polys); - } else { - let orderSplit = {} - polys.forEach(poly => { - if(poly.order in orderSplit) orderSplit[poly.order].push(poly); - else orderSplit[poly.order] = [poly]; - }) - Object.entries(orderSplit) //split the polys by order - .sort((a,b) => -(a[0] - b[0] )) //sort by order (highest first) - .forEach(([order, orderPolys]) => { // emit based on closest for each order - let polyLast; - // console.log({order, orderPolys}); - printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) { - polyLast = polyEmit(poly, index, count, polyLast); - }, { - swapdir: false, - weight: process.camInnerFirst - }); - }) - newLayer(); - } - } - - if (depthFirst) { - let flatLevels = depthData.map(level => { - return POLY.flatten(level.clone(true), [], true).filter(p => !(p.depth = 0)); - }).filter(l => l.length > 0); - if (flatLevels.length && flatLevels[0].length) { - // start with the smallest polygon on the top - printPoint = flatLevels[0] - .sort((a,b) => { return a.area() - b.area() })[0] - .average(); - // experimental start of ease down - let ease = op.down && easeDown ? 0.001 : 0; - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, false, ease); - printPoint = depthOutlinePath(printPoint, 0, flatLevels, toolDiam, polyEmit, true, ease); - } - } + async prepare(ops, progress) { + return this.op_cutout.prepare(ops, progress); } } diff --git a/src/kiri/mode/cam/op-pocket.js b/src/kiri/mode/cam/op-pocket.js index 4e1826e2..d44d6478 100644 --- a/src/kiri/mode/cam/op-pocket.js +++ b/src/kiri/mode/cam/op-pocket.js @@ -1,16 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { generate as Topo } from './topo3.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { util as base_util } from '../../../geo/base.js'; -import { calc_normal, calc_vertex } from '../../../geo/paths.js'; -import { CAM } from './driver-be.js'; - -const DEG2RAG = Math.PI / 180; +import { OpArea } from './op-area.js'; class OpPocket extends CamOp { constructor(state, op) { @@ -18,253 +9,37 @@ class OpPocket extends CamOp { } async slice(progress) { - const pocket = this; let { op, state } = this; - let { tool, rate, down, plunge, expand, contour, smooth, tolerance } = op; - let { ov_botz, ov_conv } = op; - let { settings, widget, addSlices, zBottom, tabs, color } = state; - let { updateToolDiams, cutTabs, healPolys, shadowAt, workarea } = state; - zBottom = ov_botz ? workarea.bottom_stock + ov_botz : zBottom; - // generate tracing offsets from chosen features - let sliceOut; - let pockets = this.pockets = []; - let camTool = new Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let cutdir = ov_conv; - let engrave = contour && op.engrave; - let zTop = workarea.top_z; - let devel = settings.controller.devel; - let smoothVal = (smooth ?? 0) / 10; - if (contour) { - down = 0; - this.contour = { - axis: "-", - inside: true, - nogpu: true, - step: toolOver, - tolerance, - tool, - }; - } - updateToolDiams(toolDiam); - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - function newPocket() { - pockets.push(sliceOut = []); - } - function newSliceOut(z) { - let slice = newSlice(z); - sliceOut.push(slice); - return slice; - } - async function clearZ(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; - if (engrave) { - toolDiam = toolOver; - } - if (contour) { - expand = engrave ? 0 : expand; - } else if (expand) { - polys = POLY.offset(polys, expand); - } - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - newPocket(); - for (let z of zs) { - let clip = [], shadow; - if (contour) { - if (smooth) { - clip = POLY.offset(POLY.offset([ poly ], smoothVal), -smoothVal); - } else { - clip = [ poly ]; - } - } else { - shadow = shadowAt(z); - if (smooth) { - shadow = POLY.setZ(POLY.offset(POLY.offset(shadow, smoothVal), -smoothVal), z); - } - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - if (op.outline) { - POLY.clearInner(clip); - } - } - if (clip.length === 0) { - continue; - } - let slice = newSliceOut(z); - let count = engrave ? 1 : 999; - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = contour ? - [ expand || (-0.02), -toolOver ] : - [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, cutdir, false); - if (contour) { - slice.camLines = await pocket.conform(slice.camLines, op.refine, engrave, pct => { - progress(0.9 + (zpro + zinc * pct) * 0.1, "conform"); - }); - } - slice.output() - .setLayer(state.layername, {line: color}, false) - .addPolys(slice.camLines) - if (devel && shadow) slice.output() - .setLayer("pocket shadow", {line: 0xff8811}, false) - .addPolys(shadow); - if (!contour) { - progress(zpro, "pocket"); - } - zpro += zinc; - addSlices(slice); - } - } - } - let surfaces = op.surfaces[widget.id] || []; - let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - // let vert = widget.getVertices().array.map(v => v.round(4)); - let outline = []; - let faces = CAM.surface_find(widget, surfaces, (op.follow || 5) * DEG2RAG); - let zmin = Infinity; - let j=0, k=faces.length; - for (let face of faces) { - let i = face * 9; - outline.push(newPolygon() - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - .add(vert[i++], vert[i++], zmin = Math.min(zmin, vert[i++])) - ); - } - zmin = Math.max(zBottom, zmin); - outline = POLY.union(outline, 0.0001, true); - outline = POLY.setWinding(outline, cutdir, false); - outline = healPolys(outline); - if (smooth) { - outline = POLY.offset(POLY.offset(outline, smoothVal), -smoothVal); - } - if (outline.length) { - // option to skip interior features (holes, pillars) - if (op.outline) { - POLY.clearInner(outline); - } - await clearZ(outline, zmin + 0.0001, down); - if (devel && sliceOut?.length) sliceOut[0].output() - .setLayer("pocket area", {line: 0x1188ff}, false) - .addPolys(outline) - progress(1, "pocket"); - } - } - - // mold cam output lines to the surface of the topo offset by tool geometry - async conform(camLines, refine, engrave, progress) { - if (!this.topo) { - console.log('deferred topo'); - this.topo = await Topo({ - // onupdate: (update, msg) => { - onupdate: (index, total, msg) => { - progress((index / total) * 0.9, msg); - }, - ondone: (slices) => { - // console.log({ contour: slices }); - }, - contour: this.contour, - state: this.state - }); - } - const topo = this.topo; - // re-segment polygon to a higher resolution - const hirez = camLines.map(p => p.segment(topo.tolerance * 2)); - // walk points and offset from surface taking into account tool geometry - let steps = hirez.length; - let iter = 0; - for (let poly of hirez) { - for (let point of poly.points) { - point.z = engrave ? topo.zAtXY(point.x, point.y) : topo.toolAtXY(point.x, point.y); - } - progress((iter++ / steps) * 0.8); - } - steps = steps * refine; - iter = 0; - // walk points noting z deltas and smoothing z sawtooth patterns - for (let j=0; j p.midpoints(topo.tolerance * 8)); - return hirez; + let { contour, down, expand, follow, outline, ov_botz, ov_topz } = op; + let { plunge, rate, refine, smooth, spindle, surfaces, tolerance, tool } = op; + let pocket = { + areas: {}, + down, + expand, + follow, + mode: contour ? 'surface' : 'clear', + outline, + ov_botz, + ov_topz, + over: op.step, + plunge, + rate, + refine, + rename: op.rename ?? "pocket", + smooth, + spindle, + sr_type: 'offset', + surfaces, + tolerance, + tool, + tr_type: 'none', + }; + this.op_pocket = new OpArea(state, pocket); + return this.op_pocket.slice(progress); } prepare(ops, progress) { - let { op, state, pockets } = this; - let { pocket, setTool, setSpindle, setTolerance } = ops; - let { process } = state.settings; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - - if (this.topo) { - setTolerance(this.topo.tolerance); - } - - // eliminate empty pockets - pockets = pockets.filter(p => p.length); - - // naive output order - for (let slices of pockets) { - pocket({ - cutdir: op.ov_conv, - depthFirst: process.camDepthFirst && !state.isIndexed, - easeDown: op.down && process.easeDown ? op.down : 0, - progress: (n,m) => progress(n/m, "pocket"), - slices - }); - } + return this.op_pocket.prepare(ops, progress); } } diff --git a/src/kiri/mode/cam/op-register.js b/src/kiri/mode/cam/op-register.js index 3448d864..a2c22fbe 100644 --- a/src/kiri/mode/cam/op-register.js +++ b/src/kiri/mode/cam/op-register.js @@ -147,18 +147,16 @@ class OpRegister extends CamOp { prepare(ops, progress) { let { op } = this; - let { emitDrills, setDrill, setSpindle, setTool } = ops; + let { emitDrills, setDrill, setTool } = ops; if (op.axis === '-' || op.axis === '=') { setTool(op.tool, op.feed, op.rate); - setSpindle(op.spindle); for (let slice of this.sliceOut) { ops.emitTrace(slice); } } else { setTool(op.tool, undefined, op.rate); setDrill(op.down, op.lift, op.dwell); - setSpindle(op.spindle); emitDrills(this.sliceOut.map(slice => slice.camLines).flat()); } } diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 809c3d40..39585bb1 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -1,341 +1,87 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CamOp } from './op.js'; -import { Tool } from './tool.js'; +import { OpArea } from './op-area.js'; import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; import { polygons as POLY } from '../../../geo/polygons.js'; -import { util as base_util } from '../../../geo/base.js'; -import { poly2polyEmit } from '../../../geo/paths.js'; class OpRough extends CamOp { constructor(state, op) { super(state, op); } + // todo: cutThruBypass + async slice(progress) { let { op, state } = this; - let { settings, slicer, addSlices, unsafe, color, widget } = state; - let { updateToolDiams, thruHoles, tabs, cutTabs, cutPolys } = state; - let { ztOff, zMax, shadowAt, isIndexed} = state; + let { shadow, tool, widget } = state; let { workarea } = state; - let { controller, process, stock } = settings; - let center_off = widget.track.pos ?? { x: 0, y: 0, z: 0}; + + let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part; + let cutOutside = !op.inside; + let shadowBase = shadow.base; if (op.down <= 0) { throw `invalid step down "${op.down}"`; } - let roughIn = op.inside; - let roughDown = op.down; - let roughLeave = op.leave || 0; - let roughLeaveZ = op.leavez || 0; - let roughStock = op.all && isIndexed; - let toolDiam = new Tool(settings, op.tool).fluteDiameter(); - let trueShadow = process.camTrueShadow === true; - let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part; - - updateToolDiams(toolDiam); - - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); + if (op.all) { + shadowBase = [ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ]; } - // clear the stock above the area to be roughed out - if (workarea.top_z > workarea.top_part && !cutThruBypass) { - let shadow = state.shadow.base.clone(); - let step = toolDiam * op.step; - let inset = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], step) : - POLY.offset(shadow, roughIn ? step : step + roughLeave + toolDiam / 2); - let facing = POLY.offset(inset, -step, { count: 999, flat: true }); - let zdiv = ztOff / roughDown; - let zstep = (zdiv % 1 > 0) ? ztOff / (Math.floor(zdiv) + 1) : roughDown; - if (ztOff === 0) { - // compensate for lack of z top offset in this scenario - ztOff = zstep; - } - let zsteps = Math.round(ztOff / zstep); - let camFaces = this.camFaces = []; - let zstart = zMax + ztOff - zstep; - for (let z = zstart; zsteps > 0; zsteps--) { - let slice = newSlice(); - slice.z = z; - let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ); - if (tabs) { - polys = cutTabs(tabs, polys, slice.z); - } - slice.camLines = polys; - slice.output() - .setLayer(state.layername, {face: color, line: color}) - .addPolys(slice.camLines); - addSlices(slice); - camFaces.push(slice); - z -= zstep; - } + let areas = POLY.flatten(POLY.expand(shadowBase, tool.fluteDiameter() / 2 - 0.001)); + + let rough = { + rename: op.rename ?? "rough", + spindle: op.spindle, + tool: op.tool, + rate: op.rate, + plunge: op.plunge, + mode: 'clear', + over: op.step, + down: op.down, + expand: 0, + smooth: 0, + outline: true, + omitthru: op.omitthru, + leave_xy: op.leave, + leave_z: op.leavez, + ov_botz: op.ov_botz, + ov_topz: op.ov_topz, + areas: { [widget.id]: areas.map(p => p.toArray()) }, + surfaces: {} + }; + + this.op_rough = new OpArea(state, rough); + await this.op_rough.slice(progress); + + if (cutOutside) { + let cutout = { + rename: op.rename ?? "rough", + spindle: op.spindle, + tool: op.tool, + rate: op.rate, + plunge: op.plunge, + mode: 'trace', + tr_type: 'none', + down: op.down, + expand: 0, + smooth: 1, + outline: !op.omitthru, + ov_botz: op.ov_botz, + ov_topz: op.ov_topz, + areas: { [widget.id]: areas.map(p => p.toArray()) }, + surfaces: {} + }; + this.op_cutout = new OpArea(state, cutout); + await this.op_cutout.slice(progress); } - // create roughing slices - let flats = []; - let shadow = []; - let slices = []; - let indices = slicer.interval(roughDown, { - down: true, min: 0, fit: true, off: 0.01 - }); - - // shift out first (top-most) slice - indices.shift(); - - // find flats and add to indices for slicing - if (op.flats) { - let flatArea = (Math.PI * (toolDiam/2) * (toolDiam/2)) / 2; - let flats = Object.entries(slicer.zFlat) - .filter(row => row[1] > flatArea) - .map(row => row[0]) - .map(v => parseFloat(v).round(5)) - .filter(v => v >= workarea.bottom_z); - flats.forEach(v => { - if (!indices.contains(v)) { - indices.push(v); - } - }); - indices = indices.sort((a,b) => { return b - a }); - // if layer is not on a flat and next one is, - // then move this layer up to mid-point to previous layer - // this is not perfect. the best method is to interpolate - // between flats so that each step is < step down. on todo list - for (let i=1; i (parseFloat(v) - 0.01).round(5)) - .filter(v => v > 0 && indices.indexOf(v) < 0); - indices = indices.appendAll(flats).sort((a,b) => b-a); - } - - indices = indices.filter(v => v >= workarea.bottom_z); - // console.log('indices', ...indices, {zBottom}); - - let lsz; - let cnt = 0; - let tot = 0; - await slicer.slice(indices, { each: data => { - shadow = unsafe ? data.tops : POLY.union(shadow.slice().appendAll(data.tops), 0.01, true); - if (flats.indexOf(data.z) >= 0) { - // exclude flats injected to complete shadow - return; - } - if (data.z > workarea.top_z) { - return; - } - data.shadow = trueShadow ? shadowAt(data.z, lsz) : shadow.clone(true); - data.slice.shadow = data.shadow; - slices.push(data.slice); - lsz = data.z; - progress(0.25 + 0.25 * (++cnt / tot)); - }, progress: (index, total) => { - tot = total; - progress((index / total) * 0.25); - } }); - - if (trueShadow) { - shadow = state.shadow.base.clone(true); - } else { - shadow = POLY.union(shadow.appendAll(state.shadow.base), 0.01, true); - } - - // inset or eliminate thru holes from shadow - shadow = POLY.flatten(shadow.clone(true), [], true); - if (!op.omitthru) - thruHoles.forEach(hole => { - shadow = shadow.map(p => { - if (p.isEquivalent(hole)) { - let po = POLY.offset([p], -(toolDiam / 2 + roughLeave + 0.05)); - return po ? po[0] : undefined; - } else { - return p; - } - }).filter(p => p); - }); - shadow = POLY.nest(shadow); - if (op.voids) { - // eliminate voids from shadow when "clear voids" enables - for (let s of shadow) s.inner = undefined; - } - - // shell = shadow expanded by half tool diameter + leave stock - const sadd = roughIn ? toolDiam / 2 : toolDiam / 2; - const shell = roughStock ? - POLY.offset([ newPolygon().centerRectangle(stock.center, stock.x, stock.y) ], sadd) : - POLY.offset(shadow, sadd + roughLeave); - - slices.forEach((slice, index) => { - let offset = [shell.clone(true),slice.shadow.clone(true)].flat(); - let flat = POLY.flatten(offset, [], true); - let nest = POLY.setZ(POLY.nest(flat), slice.z); - - // inset offset array by 1/2 diameter then by tool overlap % - offset = POLY.offset(nest, [-(toolDiam / 2 + roughLeave), -toolDiam * op.step], { - minArea: Math.min(0.01, toolDiam * op.step / 4), - z: slice.z, - count: 999, - flat: true, - call: (polys, count, depth) => { - // used in depth-first path creation - polys.forEach(p => { - p.depth = depth; - if (p.inner) { - p.inner.forEach(p => p.depth = depth); - } - }); - } - }) || []; - - // add outside pass if not inside only - if (!roughIn && !roughStock) { - const outside = POLY.offset(shadow.clone(), toolDiam / 2 + roughLeave, {z: slice.z}); - if (outside) { - outside.forEach(p => p.depth = -p.depth); - offset.appendAll(outside); - } - } - - // elimate double inset on inners - offset.forEach(op => { - if (op.inner) { - let pv1 = op.perimeter(); - let newinner = []; - op.inner.forEach(oi => { - let pv2 = oi.perimeter(); - let pct = pv1 > pv2 ? pv2/pv1 : pv1/pv2; - if (pct < 0.98) { - newinner.push(oi); - } - }); - op.inner = newinner; - } - }); - - if (process.camStockClipTo && stock.x && stock.y && stock.center) { - let { center } = stock; - let x = center.x - center_off.x; - let y = center.y - center_off.y; - let rect = newPolygon().centerRectangle({ x, y }, stock.x + 0.001, stock.y + 0.001); - offset = cutPolys([rect], offset, slice.z, true); - } - - if (!offset) return; - - slice.camLines = offset; - if (roughLeaveZ) { - // offset roughing in Z as well to minimize tool marks on curved surfaces - // const roughLeaveZ = 1 * Math.min(roughDown, roughLeave / 2); - for (let poly of slice.camLines) { - for (let point of poly.points) point.z += roughLeaveZ; - } - } - if (controller.devel) { - if (tabs) slice.output() - .setLayer("tabs clip", {line: 0xaa00aa}, true) - .addPolys(tabs.map(tab => tab.off).flat()); - slice.output() - .setLayer("slice", {line: 0xaaaa00}, true) - .addPolys(slice.topPolys()) - // .setLayer("top shadow", {line: 0x0000aa}) - // .addPolys(tshadow) - // .setLayer("rough shadow", {line: 0x00aa00}) - // .addPolys(shadow) - .setLayer("shadow clip", {line: 0xaa0000}) - .addPolys(shell); - } - progress(0.5 + 0.5 * (index / slices.length)); - }); - - let last = slices[slices.length-1]; - - // when step down > full cut depth, clear slices and - // leave only the cut-thru pass(es) - if (cutThruBypass) { - slices.length = 0; - } - // add cut thru passes - if (workarea.bottom_z < 0) - for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { - if (!last) continue; - let add = last.clone(true); - add.z -= zneg; - add.camLines = last.camLines.clone(true); - add.camLines.forEach(p => p.setZ(add.z + roughLeaveZ)); - // add.tops.forEach(top => top.poly.setZ(add.z)); - // add.shadow = last.shadow.clone(true); - slices.push(add); - } - - slices.forEach(slice => { - if (slice.camLines && tabs) { - slice.camLines = cutTabs(tabs, slice.camLines); - } - slice.output() - .setLayer(state.layername, {face: color, line: color}) - .addPolys(slice.camLines); - }); - this.sliceOut = slices.filter(slice => slice.camLines); - - addSlices(this.sliceOut); } - prepare(ops, progress) { - let { op, state, sliceOut, camFaces } = this; - let { setTool, setSpindle, pocket, polyEmit, newLayer, printPoint } = ops; - let { settings } = state; - let { process } = settings; - - let easeDown = process.camEaseDown; - let cutdir = op.ov_conv; - let depthFirst = process.camDepthFirst && !state.isIndexed; - - setTool(op.tool, op.rate, op.plunge); - setSpindle(op.spindle); - - // output the clearing of stock above roughing - for (let slice of (camFaces || [])) { - const level = []; - for (let poly of slice.camLines) { - level.push(poly); - if (poly.inner) { - poly.inner.forEach(function(inner) { - level.push(inner); - }); - } - } - // set winding specified in output - POLY.setWinding(level, cutdir, false); - poly2polyEmit(level, printPoint, (poly, index, count) => { - printPoint = polyEmit(poly, index, count, printPoint, {cutFromLast: true}); - }, { - weight: process.camInnerFirst - }); - newLayer(); - } - - // output the roughing passes - pocket({ - cutdir, - depthFirst, - easeDown: op.down && easeDown ? 0.001 : 0, - progress: (n,m) => progress(n/m, "routing"), - slices: sliceOut - }); + async prepare(ops, progress) { + await this.op_rough.prepare(ops, progress); + if (this.op_cutout) await this.op_cutout.prepare(ops, progress); } } diff --git a/src/kiri/mode/cam/op-shadow.js b/src/kiri/mode/cam/op-shadow.js index 5c782636..edef958c 100644 --- a/src/kiri/mode/cam/op-shadow.js +++ b/src/kiri/mode/cam/op-shadow.js @@ -1,7 +1,8 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CamOp } from './op.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; +import { newSlice } from '../../core/slice.js'; +import { newPolygon } from '../../../geo/polygon.js'; /** * Computes the Part "shadow" and attaches relevant data to the "state" object @@ -26,90 +27,63 @@ class OpShadow extends CamOp { async slice(progress) { let state = this.state; - let { ops, slicer, widget, unsafe, addSlices, shadowAt } = state; + let { addSlices, settings, shadowAt, unsafe, widget } = state; + let { devel } = settings.controller; - let realOps = ops.map(rec => rec.op).filter(op => op); - let trueShadow = state.settings.process.camTrueShadow === true; - - let minStepDown = realOps - .map(op => (op.down || 3) / (trueShadow ? 1 : 3)) - .reduce((a,v) => Math.min(a, v, 1)); - - let tslices = []; - let tshadow = []; - let tzindex = slicer.interval(minStepDown, { - fit: true, off: 0.01, down: true, flats: true - }); + let bounds = widget.getBoundingBox(); + let slices = []; + let shadowBase; let skipTerrain = unsafe; + let terrain = []; + let tzindex = []; + + let minZ = Math.floor(bounds.min.z); + let maxZ = Math.floor(bounds.max.z); + for (let z = maxZ; z >= minZ; z--) { + tzindex.push(z); + } if (skipTerrain) { console.log("skipping terrain generation"); - tzindex = [ tzindex.pop() ]; + shadowBase = [ newPolygon() ]; + tzindex = [ ]; } - let lsz; // only shadow up to bottom of last shadow for progressive union - let cnt = 0; - let tot = 0; - // terrain is the "shadow stack" where index 0 = top of part // thus array.length -1 = bottom of part - let terrain = await slicer.slice(tzindex, { each: data => { - let shadow = trueShadow ? shadowAt(data.z, lsz) : []; - tshadow = POLY.union(tshadow.slice().appendAll(data.tops).appendAll(shadow), 0.01, true); - tslices.push(data.slice); - // capture current shadow for this slice - data.slice.shadow = tshadow; - if (false) { - const slice = data.slice; - addSlices(slice); + for (let i=0; i { - tot = total; - progress((index / total) * 0.5); - } }); + progress(i / tzindex.length); + } + + if (devel && slices.length) { + addSlices(slices); + } if (terrain.length === 0) { throw `invalid widget shadow`; } - // TODO: deprecate use of separate shadow vars in state - state.center = tshadow[0].bounds.center(); - state.tshadow = tshadow; // true shadow (base of part) - state.terrain = terrain; // stack of shadow slices stored in tops - state.tslices = tslices; // raw slicer 'data' layer outputs - state.skipTerrain = skipTerrain; - // TODO: refactor ops to use a unified shadow object state.shadow = { - base: tshadow, // computed shadow union at base of part + base: shadowBase, // computed shadow union at base of part + holes: shadowBase.map(p => p.inner || []).flat(), + skip: skipTerrain, + slices: slices, // legacy / transitional stack: terrain, // stack of shadow slices - slices: tslices, // raw slicer 'data' objects - skip: skipTerrain }; - // identify through holes which are inner/child polygons - // on the bottom-most layer of the shadow stack (tshadow, index == 0) - state.thruHoles = tshadow.map(p => p.inner || []).flat(); } } diff --git a/src/kiri/mode/cam/op-trace.js b/src/kiri/mode/cam/op-trace.js index 1ce69b33..763c7806 100644 --- a/src/kiri/mode/cam/op-trace.js +++ b/src/kiri/mode/cam/op-trace.js @@ -1,307 +1,47 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CamOp } from './op.js'; -import { Tool } from './tool.js'; -import { newPolygon } from '../../../geo/polygon.js'; -import { newSlice } from '../../core/slice.js'; -import { polygons as POLY } from '../../../geo/polygons.js'; -import { util as base_util } from '../../../geo/base.js'; -import { poly2polyEmit } from '../../../geo/paths.js'; -import { newPoint } from '../../../geo/point.js'; +import { OpArea } from './op-area.js'; class OpTrace extends CamOp { constructor(state, op) { super(state, op); } - async slice(progress) { - const debug = false; - let { op, state } = this; - let { tool, rate, down, plunge, offset, offover, thru } = op; - let { ov_conv } = op; - let { settings, widget, addSlices, zThru, tabs, workarea } = state; - let { updateToolDiams, cutTabs, cutPolys, healPolys, color, shadowAt } = state; - let { process, stock } = settings; - let { camStockClipTo } = process; - if (state.isIndexed) { - throw 'trace op not supported with indexed stock'; - } - // generate tracing offsets from chosen features - let zTop = workarea.top_z; - let zBottom = workarea.bottom_z; - let sliceOut = this.sliceOut = []; - let areas = op.areas[widget.id] || []; - let camTool = new Tool(settings, tool); - let toolDiam = camTool.fluteDiameter(); - let toolOver = toolDiam * op.step; - let traceOffset = camTool.traceOffset() - let cutdir = ov_conv; - let polys = []; - let reContour = false; - let canRecontour = offset !== 'none' && down < 0; - let stockRect = stock.center && stock.x && stock.y ? - newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; - updateToolDiams(toolDiam); + // todo: cut thru, wide steps - if (tabs) { - tabs.forEach(tab => { - tab.off = POLY.expand([tab.poly], toolDiam / 2).flat(); - }); - } - for (let arr of areas) { - let poly = newPolygon().fromArray(arr); - POLY.setWinding([ poly ], cutdir, false); - polys.push(poly); - let zs = poly.points.map(p => p.z); - let min = Math.min(...zs); - let max = Math.max(...zs); - if (max - min > 0.0001 && canRecontour) { - reContour = true; - down = 0; - } - } - if (false) newSliceOut(0).output() - .setLayer("polys", {line: 0xaaaa00}, false) - .addPolys(polys); - function newSliceOut(z) { - let slice = newSlice(z); - addSlices(slice); - sliceOut.push(slice); - return slice; - } - function minZ(z) { - return zBottom ? Math.max(zBottom, z - thru) : z - thru; - } - function followZ(poly) { - if (op.dogbone) { - addDogbones(poly, toolDiam / 5, !op.revbone); - } - let z = poly.getZ(); - let slice = newSliceOut(z); - slice.camTrace = { tool, rate, plunge }; - if (tabs) { - slice.camLines = cutTabs(tabs, [poly], z); - } else { - slice.camLines = [ poly ]; - } - if (camStockClipTo && stockRect) { - slice.camLines = cutPolys([stockRect], slice.camLines, z, true); - } - if (reContour) { - state.contourPolys(widget, slice.camLines); - } - POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false); - slice.output() - .setLayer(state.layername, {line: color}, false) - .addPolys(slice.camLines) - } - function clearZnew(polys, z, down) { - if (down) { - // adjust step down to a value <= down that - // ends on the lowest z specified - let diff = zTop - z; - down = diff / Math.ceil(diff / down); - } - let zs = down ? base_util.lerp(zTop, z, down) : [ z ]; - let zpro = 0, zinc = 1 / (polys.length * zs.length); - for (let poly of polys) { - // newPocket(); - for (let z of zs) { - let clip = [], shadow; - shadow = shadowAt(z); - // for cases where the shadow IS the poly like - // with lettering without a bounding frame, clip - // will fail and we need to restore the matching poly - let subshadow = true; - for (let spo of shadow) { - if (poly.isInside(spo, 0.01)) { - subshadow = false; - clip = [ poly ]; - break; - } - } - if (subshadow) { - POLY.subtract([ poly ], shadow, clip, undefined, undefined, 0); - } - if (op.outline) { - POLY.clearInner(clip); - } - if (clip.length === 0) { - continue; - } - let count = 999; - let slice = newSliceOut(z); - slice.camTrace = { tool, rate, plunge }; - if (toolDiam) { - const offs = [ -toolDiam / 2, -toolOver ]; - POLY.offset(clip, offs, { - count, outs: slice.camLines = [], flat:true, z, minArea: 0 - }); - } else { - // when engraving with a 0 width tip - slice.camLines = clip; - } - if (tabs) { - slice.camLines = cutTabs(tabs, POLY.flatten(slice.camLines, null, true), z); - } else { - slice.camLines = POLY.flatten(slice.camLines, null, true); - } - POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false); - if (debug && shadow) slice.output() - .setLayer("trace shadow", {line: 0xff8811}, false) - .addPolys(shadow) - if (debug) slice.output() - .setLayer("trace poly", {line: 0x1188ff}, false) - .addPolys([ poly ]) - slice.output() - .setLayer(state.layername, {line: color}, false) - .addPolys(slice.camLines) - progress(zpro, "trace"); - zpro += zinc; - addSlices(slice); - } - } - } - function similar(v1, v2, epsilon = 0.01) { - return Math.abs(v1-v2) <= epsilon; - } - function centerPoly(p1, p2) { - // follow poly with most points - if (p2.length > p1.length) { - let t = p1; - p1 = p2; - p2 = t; - } - let np = newPolygon().setOpen(true); - for (let p of p1.points) { - let q = p2.findClosestPointTo(p); - np.push(p.midPointTo3D(q.point)); - } - return np; - } - function centerPolys(polys) { - // select open polys and sort by length - let ptst = polys.filter(p => p.isOpen()).sort((a,b) => b.perimeter() - a.perimeter()); - if (ptst.length < 2) { - return polys; - } - let pt = newPoint(0,0,0); - // ensure polys are ordered with start point closest to 0,0 - ptst.forEach(p => { - if (p.last().distTo2D(pt) < p.first().distTo2D(pt)) { - p.reverse(); - } - }); - let pout = polys.filter(p => p.isClosed()); - outer: for (let i=0,l=ptst.length; i p)); - return pout; - } - // connect selected segments if open and touching - polys = healPolys(polys); - // find center line for open polys spaced by tool diameter - polys = centerPolys(polys); - switch (op.mode) { - case "follow": - let routed = []; - poly2polyEmit(polys, newPoint(0,0,0), (poly, index, count, spoint) => { - routed.push(poly); - }); - let output = []; - for (let poly of POLY.nest(routed)) { - let offdist = offset !== 'none' ? offover : 0; - if (!offdist) - switch (offset) { - case "outside": offdist = traceOffset; break; - case "inside": offdist = -traceOffset; break; - } else if (offset === "inside") { - offdist = -offdist; - } - if (offdist) { - let pnew = POLY.offset([poly], offdist, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - for (let pi of POLY.flatten(poly, [], true)) - if (down) { - let zto = minZ(pi.getZ()); - if (zThru && similar(zto,0)) { - zto -= zThru; - } - for (let z of base_util.lerp(zTop, zto, down)) { - output.push(pi.clone().setZ(z)); - } - } else { - if (thru) { - pi.setZ(pi.getZ() - thru); - } - output.push(pi); - } - if (!down && op.merge) { - let nest = POLY.nest(output); - let union = POLY.union(nest, 0, true); - output = POLY.flatten(union, [], true); - } - } - for (let poly of output) { - followZ(poly); - } - break; - case "clear": - const zbo = widget.track.top - widget.track.box.d; - let zmap = {}; - polys = POLY.nest(polys); - for (let poly of polys) { - let z = minZ(poly.minZ()); - if (offover) { - let pnew = POLY.offset([poly], -offover, { minArea: 0, open: true }); - if (pnew) { - poly = POLY.setZ(pnew, poly.getZ()); - } else { - continue; - } - } else { - poly = [ poly ]; - } - (zmap[z] = zmap[z] || []).appendAll(poly); - } - for (let [zv, polys] of Object.entries(zmap)) { - clearZnew(polys, parseFloat(zv), down); - } - } + async slice(progress) { + let { op, state } = this; + let { areas, down, expand, follow, offset, outline, mode, ov_botz, ov_topz } = op; + let { plunge, rate, refine, smooth, spindle, step, steps, thru, tolerance, tool } = op; + let trace = { + areas, + down, + expand, + follow, + mode: mode === 'clear' ? 'clear' : 'trace', + outline, + ov_botz, + ov_topz, + over: op.step, + plunge, + rate, + refine, + rename: op.rename ?? "trace", + smooth, + spindle, + step, + surfaces: {}, + tolerance, + tool, + tr_type: offset + }; + this.op_trace = new OpArea(state, trace); + return this.op_trace.slice(progress); } - prepare(ops, progress) { - let { op, state } = this; - let { setTool, setSpindle } = ops; - - setTool(op.tool, op.rate); - setSpindle(op.spindle); - for (let slice of this.sliceOut) { - ops.emitTrace(slice); - } + async prepare(ops, progress) { + return this.op_trace.prepare(ops, progress); } } diff --git a/src/kiri/mode/cam/op-xray.js b/src/kiri/mode/cam/op-xray.js index ab147a3d..12245c6c 100644 --- a/src/kiri/mode/cam/op-xray.js +++ b/src/kiri/mode/cam/op-xray.js @@ -2,7 +2,7 @@ import { CamOp } from './op.js'; import { newSlice } from '../../core/slice.js'; -import { Slicer } from './slicer.js'; +import { Slicer } from './slicer_cam.js'; class OpXRay extends CamOp { constructor(state, op) { diff --git a/src/kiri/mode/cam/prepare.js b/src/kiri/mode/cam/prepare.js index d9c0e9d1..c426438e 100644 --- a/src/kiri/mode/cam/prepare.js +++ b/src/kiri/mode/cam/prepare.js @@ -1,14 +1,17 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { base, util } from '../../../geo/base.js'; -import { tip2tipEmit, poly2polyEmit, arcToPath } from '../../../geo/paths.js'; +import { tip2tipEmit, poly2polyEmit } from '../../../geo/paths.js'; import { newPoint } from '../../../geo/point.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { render } from '../../core/render.js'; import { newPrint } from '../../core/print.js'; import { Tool } from './tool.js'; +import { newPolygon } from '../../../geo/polygon.js'; -const { toRadians } = util +const debug = false; +const debug_push = false; +const CLOSEST_TO_PP = -999; /** * DRIVER PRINT CONTRACT @@ -63,7 +66,7 @@ export async function cam_prepare(widgets, settings, update) { moves: true, other: "moving", action: "milling", - maxspeed: settings.process.camFastFeed || 6000 + // maxspeed: settings.process.camFastFeed || 6000 } ); }; @@ -75,63 +78,62 @@ export function prepare_one(widget, settings, print, firstPoint, update) { { alignTop } = settings.controller, { camArcEnabled, camArcResolution, camArcTolerance } = process, { camDepthFirst, camEaseAngle, camEaseDown } = process, + { camFastFeed, camFastFeedZ } = process, + { camStockX, camStockY, camStockZ, camStockIndexed, camStockOffset } = process, { camForceZMax, camFullEngage, camInnerFirst, camOriginCenter } = process, - { camOriginOffX, camOriginOffY, camOriginOffZ } = process, - { camStockOffset, camStockIndexed, camZClearance } = process, - stock = settings.stock || {}, + { camOriginOffX, camOriginOffY, camOriginOffZ, camZClearance } = process, + bounds = widget.getBoundingBox(), + stock = camStockOffset ? { + x: bounds.dim.x + camStockX, + y: bounds.dim.y + camStockY, + z: bounds.dim.z + camStockZ, + } : { + x: camStockX, + y: camStockY, + z: camStockZ + }, stockZ = stock.z * (camStockIndexed ? 0.5 : 1), stockZClear = stockZ + camZClearance, widgetTrackTop = widget.track.top, widgetTopToStock = stockZ - widgetTrackTop, - bounds = widget.getBoundingBox(), boundsZ = camStockIndexed ? stock.z / 2 : bounds.max.z + widgetTopToStock, - zSafe = camStockIndexed ? Math.hypot(stock.y, stock.z) / 2 + camZClearance : stockZClear, wmpos = widget.track.pos, wmx = wmpos.x, wmy = wmpos.y, wmz = !camStockIndexed ? stock.z - boundsZ : alignTop ? 0 : 0, + zSafe = camStockIndexed ? Math.hypot(stock.y, stock.z) / 2 + camZClearance : stockZClear, originx = (camOriginCenter ? 0 : -stock.x / 2) + (camOriginOffX || 0), originy = (camOriginCenter ? 0 : -stock.y / 2) + (camOriginOffY || 0), origin = newPoint(originx, originy, zSafe), - arcRadians = toRadians(camArcResolution), - arcEnabled = camArcEnabled && camArcTolerance > 0 && arcRadians > 0, + contouring = false, currentOp, drillDown = 0, drillLift = 0, drillDwell = 0, feedRate, - isPocket, - isContour, - isRough, isLathe, isIndex, layerOut = [], lastOp, lastTool, - lastPush, lasering = false, laserPower = 0, - maxToolDiam = widget.maxToolDiam, newOutput = print.output || [], - plungeRate = process.camFastFeedZ, + nextIsMove = true, + plungeRate = camFastFeedZ, printPoint, tool, toolType, toolDiam, toolDiamMove, - nextIsMove = true, - synthPlunge = false, + travelBounds, spindle = 0, spindleMax = device.spindleMax, - terrain = widget.terrain ? widget.terrain.map(data => { - return { - z: data.z, - tops: data.tops, - }; - }) : zSafe, - tolerance = 0; + tolerance = 0, + easeThrottle = (90 - Math.min(90, camEaseAngle)) / 180, + easeDzPerMm = Math.tan(camEaseAngle * Math.PI / 180); - // console.log({ zSafe, stockZ, boundsZ, wmz }); + if (debug) console.log({ zSafe, wmx, wmy, wmz }); function newLayer(op) { if (layerOut.length || layerOut.mode) { @@ -157,30 +159,34 @@ export function prepare_one(widget, settings, print, firstPoint, update) { } } - // non-zero means contouring - function setTolerance(dist) { - tolerance = dist; - if (isContour) { - // avoid moves to safe Z when contouring short steps - toolDiamMove = currentOp.step * toolDiam * 1.5; - } + function setContouring(bool, step) { + contouring = bool; + toolDiamMove = step ?? tool.getStepSize(currentOp.step) * 2; } function setSpindle(speed) { spindle = Math.min(speed, spindleMax); } + function setTolerance(dist) { + tolerance = dist; + } + + function getTool() { + return tool; + } + function setTool(toolID, feed, plunge) { if (toolID !== lastTool) { tool = new Tool(settings, toolID); toolType = tool.getType(); toolDiam = tool.fluteDiameter(); - toolDiamMove = toolType === 'endmill' ? toolDiam : tolerance * 2; + toolDiamMove = (tool.hasTaper() ? tolerance ?? toolDiam : toolDiam) * 2; lastTool = toolID; } - feedRate = feed || feedRate || plunge; - plungeRate = Math.min(feedRate || plunge, plunge || plungeRate || feedRate); - // console.log({ setTool: toolID, feed, plunge, plungeRate }); + feedRate = Math.min(camFastFeed, feed || feedRate || plunge); + plungeRate = Math.min(camFastFeed, feedRate || plunge, plunge || plungeRate || feedRate); + if (debug) console.log({ setTool: toolID, feed, plunge, plungeRate }); } function setLasering(bool, power = 0) { @@ -243,31 +249,35 @@ export function prepare_one(widget, settings, print, firstPoint, update) { break; } } - camOut(point.clone().setZ(stockZClear), 0); + setNextIsMove(); points.forEach(function (point, index) { - camOut(point, 1); + newLayer(); + camOut(point); if (index > 0 && index < points.length - 1) { + newLayer(); if (dwell) camDwell(dwell); if (lift) camOut(point.clone().setZ(point.z + lift), 0); } + newLayer(); }) - camOut(point.clone().setZ(stockZClear), 0); - newLayer(); } /** * @param {Point} point - * @param {number} emit (0=move, 1=/laser on/cut mode, 2/3= G2/G3 arc) + * @param {number} emit (0=move, 1=/laser on/cut mode) * @param {number} [speed] feed/plunge rate in mm/min * @param {number} [tool] tool number */ function layerPush(point, emit, speed, tool, options) { - const { type, center, arcPoints } = options ?? {}; - const dz = (point && lastPush?.point) ? point.z - lastPush.point.z : 0; - if (dz < -0.05 && speed > plungeRate) { - speed = plungeRate; + const { type, center } = options ?? {}; + if (debug_push && options?.type !== 'lerp') { + console.log( + currentOp.type, + emit | 0, + speed | 0, + ...[point.x,point.y,point.z,point.a??0].map(v => v.toFixed(3)) + ); } - // if (options?.type !== 'lerp') console.log( point, currentOp.type ); layerOut.mode = currentOp; if (lasering) { let power = emit ? laserPower : 0; @@ -295,10 +305,9 @@ export function prepare_one(widget, settings, print, firstPoint, update) { } print.addOutput(layerOut, point, power, speed, tool, { type: 'laser' }); } else { - print.addOutput(layerOut, point, emit, speed, tool, { type, center, arcPoints }); + print.addOutput(layerOut, point, emit, speed, tool, { type, center }); } - lastPush = { point, emit, speed, tool }; - printPoint = point ?? printPoint; + printPoint = (point ?? printPoint).clone(); return point; } @@ -311,12 +320,16 @@ export function prepare_one(widget, settings, print, firstPoint, update) { ); } + function setNextIsMove() { + nextIsMove = true; + } + /** * Move a point by the widget's movement offset. * @param {Point} p - point to move * @return {Point} new point with offset applied */ - function applyWidgetMovement(p) { + function toWorkCoords(p) { return newPoint( p.x + wmx, p.y + wmy, @@ -326,72 +339,72 @@ export function prepare_one(widget, settings, print, firstPoint, update) { .annotate({ slice: p.slice }); } - /** - * wrapper: translate point into workspace coordinates and call `camOut` - */ - function camOut(point, emit = 1, opts) { - _camOut(applyWidgetMovement(point), emit, opts); + function toWidgetCoords(p) { + return newPoint( + p.x - wmx, + p.y - wmy, + p.z - wmz + ) } /** - * emit a cut, arc, or move operation from the current location to a new location - * @param {Point} point destination for move - * @param {0|1|2|3} emit G0, G1, G2, G3 - * @param {number} opts.radius arc radius; truthy values for arc move - * @param {boolean} opts.clockwise arc direction - * @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection + * emit a cut or move operation from the current location to a new location + * @param {Point} point destination for move in widget coordinate space + * @param {-1|0|1|2|3} emit ignore, G0, G1, G2, G3 + * @param {number} opts.shortCut used to convert short moves to cuts * @param {number} opts.factor speed scale factor + * @param {Object} opts.center arc center parameter + * @return {Point} translated emitted point */ - function _camOut(point, emit, opts) { + function camOut(point, emit = 1, opts) { let lop = lastOp; lastOp = currentOp; + // translate widget point into workspace coordinates + point = toWorkCoords(point); + + let { + factor = 1, + feed = feedRate, + shortCut = toolDiamMove, + moveOnly = false, + center, + } = opts ?? {}; + let pointA = point.a; + let rate = feed * factor; + // on operation changes: // 1. move to safe z of current point preserving angle // 2. move to safe z of new point preserving old angle // 3. move to safe z of new point with new angle if (lop !== currentOp) { - _camOut(printPoint.clone().setZ(zSafe).setA(printPoint.a), 0); - _camOut(point.clone().setZ(zSafe).setA(printPoint.a), 0); - _camOut(point.clone().setZ(zSafe), 0); + layerPush(printPoint.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool); + layerPush(point.clone().setZ(zSafe).setA(printPoint.a), 0, feedRate, tool); + layerPush(point.clone().setZ(zSafe), 0, feedRate, tool); + newLayer(); } - if (lop?.type === 'index' && lop !== currentOp ) { - // console.log('post index first point', point); - _camOut(point.clone().setZ(printPoint.z).setA(printPoint.a), 1); - } - - let { - center = {}, - clockwise = true, - arcPoints = [], - moveLen = toolDiamMove, - factor = 1, - } = opts ?? {} - - const isArc = emit == 2 || emit == 3; - const pointA = point.a; - + // consume forced next move flag and convert to move + // this is usually set right before a `polyEmit` if (nextIsMove) { emit = 0; nextIsMove = false; } - let rate = feedRate * factor; - // carry rotation forward when not overridden if (pointA !== undefined && printPoint.a !== undefined) { let DA = printPoint.a - pointA; let MZ = Math.max(printPoint.z, point.z) - // find arc length + // find rotary arc length let AL = (Math.abs(DA) / 360) * (2 * Math.PI * MZ); if (AL >= 1) { + newLayer(); let lerp = base.util.lerp(printPoint.a, pointA, 1); // create interpolated point set for rendering and animation - // console.log({ DA, MZ, AL }, lerp.length); + if (debug) console.log({ DA, MZ, AL }, lerp.length); for (let a of lerp) { let lp = point.clone().setA(a); - // console.log(lp.a, lp.x, lp.y, lp.z); + if (debug) console.log(lp.a, lp.x, lp.y, lp.z); layerPush( lp, emit, @@ -400,175 +413,158 @@ export function prepare_one(widget, settings, print, firstPoint, update) { { type: "lerp" }, ); } + newLayer(); } } - // measure deltas to last point in XY and Z + // measure deltas from last point in XY and Z let deltaXY = printPoint.distTo2D(point), deltaZ = point.z - printPoint.z, absDeltaZ = Math.abs(deltaZ), - hasDelta = deltaXY >= 0.001 && absDeltaZ >= 0, - isMove = emit == 0; + isMove = (emit === 0 || emit === false), + isArc = (emit > 1), + upAndOver = false; - // when rapid pluge could cut thru stock, rapid to just above stock - // then continue plunge as a plunge cut - if (deltaZ < 0 && printPoint.z > stockZ && point.z < stockZ && emit === 0) { - // console.log('detected plunge cut as rapid move', printPoint.z, stockZ, point.z); - layerPush(point.clone().setZ(stockZ + 1), 0, 0, tool); + // contouring logic + if (isMove && contouring) { + if (deltaXY > toolDiamMove) { + upAndOver = true; + } else if (absDeltaZ < 0.01) { + if (debug) console.log('contour move as cut'); + emit = 1; + } else if (absDeltaZ < 0.001) { + if (debug) console.log('contour up for travel'); + layerPush(printPoint.clone().move({ z: 0.1 }), 0, 0, tool); + layerPush(point.clone().move({ z: 0.1 }), 0, 0, tool); + } + } else + // when rapid pluge could cut thru stock: + // * rapid to just above stock + // * continue plunge as cut + if (isMove && deltaZ < 0 && printPoint.z > stockZ && point.z < stockZ) { + if (debug) console.log('detected plunge cut as rapid move', printPoint.z, stockZ, point.z); + layerPush(point.clone().setZ(zSafe), 0, 0, tool); // change to cutting move for remainder of plunge emit = 1; - isMove = false; + newLayer(); + } else + // convert short planar moves to cuts when not lasering + if (isMove && deltaXY <= shortCut && deltaZ <= 0 && !lasering) { + // but only if the z plunge is not too far + if (absDeltaZ < 0.01 || (tolerance > 0 && absDeltaZ <= tolerance)) { + emit = 1; + } else + // otherwise move over before descending + if (deltaZ <= -tolerance) { + if (debug) console.log('over before descend'); + layerPush(point.clone().setZ(printPoint.z), 0, 0, tool); + newLayer(); + } + } else + // when moving in lathe mode ... + if (isMove && isLathe) { + if (point.z > printPoint.z) { + layerPush(printPoint.clone().setZ(point.z), 0, 0, tool); + newLayer(); + } else if (point.z < printPoint.z) { + layerPush(point.clone().setZ(printPoint.z), 0, 0, tool); + newLayer(); + } + } else + // check move against a known boundary (pocketing) + if (isMove && travelBounds) { + for (let poly of travelBounds) { + let ints = poly.intersections( + toWidgetCoords(printPoint), + toWidgetCoords(point) + ); + if (ints.length) { + if (debug) console.log({ ints, poly, deltaXY, deltaZ }); + upAndOver = "bounds"; + break; + } + } + } else + // for longer moves, check the terrain to see if we need to go up and over + if (isMove) { + const bigXY = (deltaXY > shortCut && !lasering); + const bigZ = (absDeltaZ > toolDiam / 2 && deltaXY > tolerance); + const midZ = (tolerance && absDeltaZ >= tolerance); + if (bigXY || bigZ || midZ) { + if (debug) console.log({ fromz: printPoint.z, toz: point.z }); + // for big moves inside stock... + if (camForceZMax || printPoint.z < stockZ) { + upAndOver = true; + } + } } - // drop points too close together - if (!isLathe && !isArc && deltaXY < 0.001 && point.z === printPoint.z && point.a === printPoint.a) { - // console.trace(["drop dup",printPoint,point]); + if (upAndOver) { + if (debug) console.log('upAndOver', { upAndOver, camForceZMax }); + layerPush(printPoint.clone().setZ(zSafe), 0, 0, tool); + layerPush(point.clone().setZ(zSafe), 0, 0, tool); + newLayer(); + // when plunge goes below stock, convert to cut + if (point.z < stockZ) { + if (debug) console.log('point.z < stockZ'); + layerPush(point.clone().setZ(stockZ + 0.1), 0, 0, tool); + newLayer(); + emit = 1; + rate = plungeRate; + } + } + + if (moveOnly) { return; } - // no jump moves in contour mode to adjacent slice points - let steady = currentOp.type === 'contour' && Math.abs(point.slice - printPoint.slice) < 4; - - // convert short planar moves to cuts in some cases - if (hasDelta && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) { - let iscontour = tolerance > 0; - let isflat = absDeltaZ < 0.001; - // restrict this to contouring - if (isflat || (iscontour && absDeltaZ <= tolerance)) { - emit = 1; - isMove = false; - } else if (deltaZ <= -tolerance) { - // move over before descending - layerPush(point.clone().setZ(printPoint.z), 0, 0, tool); - // new pos for plunge calc - deltaXY = 0; - } - } else if (isMove && isLathe) { - if (point.z > printPoint.z) { - layerPush(printPoint.clone().setZ(point.z), 0, 0, tool); - } else if (point.z < printPoint.z) { - layerPush(point.clone().setZ(printPoint.z), 0, 0, tool); - } - } else if (isMove && !steady) { - // for longer moves, check the terrain to see if we need to go up and over - const bigXY = (deltaXY > moveLen && !lasering); - const bigZ = (deltaZ > toolDiam / 2 && deltaXY > tolerance); - const midZ = (tolerance && absDeltaZ >= tolerance) && !isContour; - - if (bigXY || bigZ || midZ) { - let maxz = getZClearPath( - terrain, - printPoint.x, - printPoint.y, - point.x, - point.y, - Math.max(point.z, printPoint.z), - 0, // zadd, - maxToolDiam / 2, - camZClearance - ), - maxZdelta = Math.max(maxz - point.z, maxz - printPoint.z), - mustGoUp = maxZdelta >= tolerance, - clearz = maxz; - let zIsBelow = point.z <= maxz; - if (camForceZMax) { - clearz = maxz = zSafe; - zIsBelow = true; - } - // up if any point between higher than start/outline, go up first - if (mustGoUp || zIsBelow) { - const zClearance = clearz + (camStockIndexed ? 0 : widgetTopToStock); - if (zIsBelow) { - layerPush(printPoint.clone().setZ(zClearance), 0, 0, tool); - } - layerPush(point.clone().setZ(zClearance), 0, 0, tool); - // new pos for plunge calc - deltaXY = 0; - // if plunge goes below stock, convert to cut - if (emit === 0 && point.z < stockZ) { - layerPush(point.clone().setZ(stockZ + 1), 0, 0, tool); - emit = 1; - } - } - } else if (isRough && deltaZ < 0) { - layerPush(point.clone().setZ(printPoint.z), 0, 0, tool); - } + // plunge safety catch + if (deltaZ < 0 && !contouring) { + if (debug) console.log('deltaZ snap', rate, plungeRate); + emit = 1; + rate = plungeRate; } - // set new plunge rate - let tmprate; - if (false) - if (!lasering && !isLathe && deltaZ < -tolerance) { - let threshold = Math.min(deltaXY / 2, absDeltaZ), - modifier = threshold / absDeltaZ; - if (synthPlunge && threshold && modifier && deltaXY > tolerance) { - // use modifier to speed up long XY move plunge rates - // console.log('modifier', modifier); - tmprate = Math.max( - plungeRate, - Math.round(plungeRate + ((feedRate - plungeRate) * modifier)) - ); - } else { - let L = Math.hypot(deltaXY, absDeltaZ); - let limXY = feedRate * L / deltaXY; - let limZ = plungeRate * L / Math.abs(deltaZ); - tmprate = Math.min(feedRate, limXY, limZ); - console.log({ rate_override: rate, was: rate }); - // let zps = len / absDeltaZ; - // rate = Math.max( - // plungeRate, - // 1 / Math.hypot(deltaXY / feedRate, absDeltaZ / plungeRate) - // ); - // console.log({ rate, deltaXY, deltaZ }); - } - } + layerOut.mode = currentOp; + layerOut.spindle = spindle; + layerPush( + point, + emit, + rate, + tool, + isArc ? { center: toWorkCoords(center) } : undefined + ); - if (isArc) { - layerOut.mode = currentOp; - layerOut.spindle = spindle; - layerPush( - point, - clockwise ? 2 : 3, - tmprate ?? rate, - tool, - { - center, - arcPoints - } - ); - } else { - // for g1 moves - // TODO: synthesize move speed from feed / plunge accordingly - layerOut.mode = currentOp; - layerOut.spindle = spindle; - layerPush( - point, - emit, - tmprate ?? rate, - tool - ); - } + return point; + } + + function setTravelBoundary(polys) { + travelBounds = polys; + } + + function clearTravelBoundary() { + travelBounds = undefined; } /** * output an array of slices that form a pocket - * used by rough and pocket ops, does not support arcs + * used by rough and pocket ops * * @param {Slice[]} slices top-down Z stack of slices * @param {boolean} cutdir true=CW false=CCW * @param {boolean} depthFirst prioritize cut depth in pockets by nesting */ - function pocket({ slices, cutdir, depthFirst, easeDown, progress }) { + function pocket({ slices, cutdir, depthFirst, progress }) { let total = 0; let depthData = []; for (let slice of slices) { let polys = [], t = [], c = []; - POLY.flatten(slice.camLines).forEach(function (poly) { + // use shadow + tool radius offset when available (roughing) + POLY.flatten(slice.camLines).forEach((poly) => { 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); }); @@ -578,15 +574,13 @@ export function prepare_one(widget, settings, print, firstPoint, update) { POLY.setWinding(c, !cutdir); if (depthFirst) { + polys = POLY.nest(polys,true,true); + polys.tool_shadow = POLY.flatten(slice.tool_shadow.clone(true)); depthData.push(polys); } else { // if not depth first, output the polys in slice order - poly2polyEmit(polys, printPoint, function (poly, index, count) { - poly.forEachPoint(function (point, pidx, points, offset) { - // scale speed of first cutting poly since it engages the full bit - camOut(point.clone(), offset !== 0, undefined, count === 1 ? camFullEngage : 1); - }, poly.isClosed(), index); - }, { swapdir: false }); + setTravelBoundary(shadow); + poly2polyEmit(polys, printPoint, polyEmit, { swapdir: false }); newLayer(); } progress(++total, slices.length); @@ -598,95 +592,67 @@ export function prepare_one(widget, settings, print, firstPoint, update) { } if (depthFirst) { - // get inside vals (the positive ones) - let ins = depthData.map(a => a.filter(p => !isNeg(p.depth))); - let itops = ins.map(level => { - return POLY.nest(level.filter(poly => poly.depth === 0).clone()); - }); - // get outside vals (the negative ones) - let outs = depthData.map(a => a.filter(p => isNeg(p.depth))); - let otops = outs.map(level => { - return POLY.nest(level.filter(poly => poly.depth === 0).clone()); - }); - depthRoughPath(printPoint, 0, ins, itops, polyEmit, false, easeDown); - depthRoughPath(printPoint, 0, outs, otops, polyEmit, false, easeDown); + descend(depthData); } + + clearTravelBoundary(); } - /** - * 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, - */ - function generateEaseDown(fn, poly, fromPoint, degrees = 45) { - let index = poly.findClosestPointTo(fromPoint).index, - fromZ = fromPoint.z, - offset = 0, - points = poly.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; + function descend(stack, inside) { + if (stack.length === 0) return; + let tops = stack[0]; + let flat = tops.filter(poly => !poly.marked); + if (flat.length === 0) return; + if (inside) { + flat = flat.filter(p => p.isNested(inside)); + } - 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); + setTravelBoundary(tops.tool_shadow); + + for (;;) { + let wpp = getWidgetPrintPoint(); + let poly = flat.filter(poly => !poly.marked) + .map(p => p.findClosestPointTo(wpp)) + .sort((a,b) => a.distance - b.distance) + .map(rec => rec.poly)[0]; + + if (poly) { + let output = []; + emit_flat([ poly ], output); + for (let poly of output) { + polyEmit(poly, CLOSEST_TO_PP); } - - 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); + descend(stack.slice(1), poly); + } else { + return; } - last = next; - fn(next, offset++); - 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); - break; //break after touch - } - index++; } - - return touch; } - function emitTrace(slice) { - let { tool, rate, plunge } = slice.camTrace; - setTool(tool, rate, plunge); - poly2polyEmit(slice.camLines, printPoint, polyEmit, { + function emit_flat(flat, output) { + flat = flat.filter(p => !p.marked); + if (!flat.length) return; + flat.sort((a,b) => a.area() - b.area()); + let next = flat[0]; + next.marked = true; + if (!camInnerFirst) output.push(next); + if (next.inner) emit_flat(next.inner, output); + if (camInnerFirst) output.push(next); + emit_flat(flat, output); + } + + function emitTraces(camLines) { + poly2polyEmit(camLines, printPoint, polyEmit, { swapdir: false, weight: camInnerFirst }); newLayer(); } + function getWidgetPrintPoint() { + return printPoint.clone().move({ x: -wmx, y: -wmy }); + } + /** * Output a single polygon as gcode. The polygon is walked in either the * clockwise or counter-clockwise direction depending on the winding of the @@ -697,286 +663,118 @@ export function prepare_one(widget, settings, print, firstPoint, update) { * the polygon if that point is not the current position. * * @param {Polygon} poly - the polygon to output - * @param {number} index - unused - * @param {number} count - 1 to set engage factor - * @param {Point} fromPoint - the point to rapid move from - * @param {boolean} ops.cutFromLast - whether to emit a 1 when moving from last point. Defaults to false - * @returns {Point} - the last point of the polygon + * @param {number} index - optional: starting point index + * @returns {Point} - the last point emitted (in widget coordinates) */ - function polyEmit(poly, index, count, fromPoint, ops) { - let { cutFromLast } = ops ?? { cutFromLast: false }; - let arcQ = [], - arcMax = Infinity, // no max arc radius - lineTolerance = 0.001, // do not consider points under 0.001mm for arcs - zTolerance = 0.1; // allow zs of Points to be off by 0.001mm per radian of rotation + function polyEmit(poly, index) { + let eased = 0; + let arcing = camArcEnabled && !contouring; + let points = poly.points; - fromPoint = fromPoint || printPoint; - arcQ.angle = [] - - // scale speed of first cutting poly since it engages the full bit - let scale = ((isRough || isPocket) && count === 1) ? camFullEngage : 1; - let startIndex = index; - let polyLastPoint = fromPoint; - - // easeDown only allowed on closed polys (that we can continue around indefinitly) - if (camEaseDown && poly.isClosed()) { - let closest = poly.findClosestPointTo(fromPoint); - polyLastPoint = closest.point; - startIndex = closest.index; - let last = generateEaseDown((point, offset) => { // generate ease-down points - if (offset == 0) camOut(point.clone(), 0, { factor: camFullEngage }); - camOut(point.clone(), 1, { factor: scale }); // and pass them to camOut - }, poly, fromPoint, camEaseAngle); - polyLastPoint = poly.points[last]; - startIndex = last; + if (index === CLOSEST_TO_PP) { + let found = poly.findClosestPointTo(getWidgetPrintPoint()); + index = found.index; + } + if (index) { + points = [...points.slice(index), ...points.slice(0,index)]; } - // console.log(poly,poly.isClosed(),startIndex) - // A is first point of segment, B is last - poly.forEachSegment((pointA, pointB, indexA, indexB) => { - // if(offset == 0) console.log("forEachPoint",point,pidx,points) - // console.log({pointA, pointB, indexA, indexB,startIndex}) - if (indexA == startIndex) { - //if cutFromLast is true, emit a 1 for a cutting move - camOut(pointA.clone(), cutFromLast ? 1 : 0, { factor: camFullEngage }); - // if first point, move to and call export function - if (arcEnabled) arcQ.push(pointA); - } - polyLastPoint = arcExport(pointB, pointA); - }, !poly.isClosed(), startIndex); - - // console.log("at end of arcExport",structuredClone(arcQ)); - if (arcQ.length > 3) { - // if few points left, emit as lines - drainQ(); - } - while (arcQ.length) { - camOut(arcQ.shift(), 1); + if (!contouring && poly.isClosed()) { + points.push(points[0].clone()); } - function arcExport(point, lastp) { - let dist = lastp ? point.distTo2D(lastp) : 0; - if (lastp) { - if (arcEnabled && dist > lineTolerance && lastp) { - let rec = Object.assign(point, { dist }); - arcQ.push(rec); + // run arc detection when enabled + if (arcing) { + poly = newPolygon(points).setOpenValue(poly.open).detectArcs({ + tolerance: camArcTolerance, + arcRes: camArcResolution, + minPoints: 5 + }); + points = poly.points; + } - // ondebug({arcQ}); - if (arcQ.length > 2) { - let el = arcQ.length; - let e1 = arcQ[0]; // first in arcQ - let e2 = arcQ[Math.floor(el / 2)]; // mid in arcQ - let e3 = arcQ[el - 1]; // last in arcQ - let e4 = arcQ[el - 2]; // second last in arcQ - let e5 = arcQ[el - 3]; // third last in arcQ - let cc = util.center2d(e1, e2, e3, 1); // find center - let lr = util.center2d(e3, e4, e5, 1); // find local radius - let dc = 0; + setNextIsMove(); - let radFault = false; - if (lr) { - let angle = 2 * Math.asin(dist / (2 * lr.r)); - radFault = Math.abs(angle) > arcRadians; // enforce arcRadians(olution) - } else { - // console.log("too much angle") - radFault = true; - } + // we skip ease-down logic in contouring mode + if (!contouring && camEaseDown) { + let point0 = points[0]; - let endDelta = e1.distTo2D(e3) - if(endDelta < 0.01){ - let last = arcQ.peek() - drainQ(true) - arcQ.push(last) - return e3 - } + // perform "up and over" and get a new printPoint without "emit" + camOut(point0, 0, { moveOnly: true }); + setContouring(true); - let ddz = Math.abs((e3.z - e4.z) - (e4.z - e5.z)) // take second derivitive of z - let zFault = Math.abs(ddz) > zTolerance + // poly points are in untranslated widget space + // so we need to translate printPoint into widget coordinates + let startPoint = printPoint.clone().move({ x: -wmx, y: -wmy, z: -wmz }); - if (cc) { - if ([cc.x, cc.y, cc.z, cc.r].hasNaN()) { - // console.log({cc, e1, e2, e3}); - } - if (arcQ.length === 3) { - arcQ.center = [cc]; - arcQ.xSum = cc.x; - arcQ.ySum = cc.y; - arcQ.rSum = cc.r; - - // check if first angles should have caused radFault - let angle = toRadians(arcQ[0].slopeTo(cc).angleDiff(arcQ[1].slopeTo(cc)).angle) - radFault = Math.abs(angle) > arcRadians - if (radFault) { - // if so, remove first point - console.log("secondary radfault,",structuredClone(arcQ),{angle,arcRadians,a,b}) - camOut(arcQ.shift(), 1) - } - - } else { - // check center point delta - arcQ.xSum = arcQ.center.reduce(function (t, v) { return t + v.x }, 0); - arcQ.ySum = arcQ.center.reduce(function (t, v) { return t + v.y }, 0); - arcQ.rSum = arcQ.center.reduce(function (t, v) { return t + v.r }, 0); - let dx = cc.x - arcQ.xSum / arcQ.center.length; - let dy = cc.y - arcQ.ySum / arcQ.center.length; - dc = Math.hypot(dx, dy); // delta center distance - } - // if new point is off the arc - // if point is off-center, or too far from center, or too large of a radius - if (dc * arcQ.center.length / arcQ.rSum > camArcTolerance || dist > cc.r || cc.r > arcMax || radFault || zFault) { - // let debug = [ dc * arcQ.center.length / arcQ.rSum > camArcTolerance, dist > cc.r, cc.r > arcMax, radFault]; - // console.log("point off the arc,",structuredClone(arcQ),radFault,zFault,[dc * arcQ.center.length / arcQ.rSum > camArcTolerance , dist > cc.r , cc.r > arcMax]); - if (arcQ.length === 4) { - // not enough points for an arc, drop first point and recalc center - camOut(arcQ.shift(), 1); - let tc = util.center2d(arcQ[0], arcQ[1], arcQ[2], 1); - // the new center is invalid as well. drop the first point - if (!tc) { - camOut(arcQ.shift(), 1); - } else { - arcQ.center = [tc]; - let angle = 2 * Math.asin(arcQ[1].dist / (2 * tc.r)); - if (Math.abs(angle) > arcRadians) { // enforce arcRadians on initial angle - camOut(arcQ.shift(), 1); - } - } - } else { - // enough to consider an arc, emit and start new arc - let defer = arcQ.pop(); - drainQ(); - // re-add point that was off the last arc - arcQ.push(defer); - } - } else { - // new point is on the arc - arcQ.center.push(cc); - } - } else { - // drainQ on invalid center - drainQ(); - } + // calculate ease down for poly path output + if (startPoint.z > point0.z) { + let easeFeed = plungeRate + ((feedRate - plungeRate) * easeThrottle); + let zat = startPoint.z; + let lp; + for (let i=0; ; i++) { + let ii = i % points.length; + let pt = points[ii]; + if (zat <= pt.z) { + // rotate points to start at end of ease + points = [...points.slice(i), ...points.slice(0,i)]; + eased = i; + break; } - } else { - // if dist to small, output as a cut - // console.trace('point too small', point,lastp,dist); - camOut(point, 1); + if (i > 0) { + let dd = lp.distTo2D(pt); + zat = Math.max(pt.z, zat - (dd * easeDzPerMm)); + } + lp = pt.clone().setZ(zat); + camOut(lp, 1, { feed: easeFeed }); } - } else { - // if first point, emit and set - camOut(point, 1); - // TODO disabling out of plane z moves until a better mechanism - // can be built that doesn't rely on computed zpos from layer heights... - // when making z moves (like polishing) allow slowdown vs fast seek - // let moveSpeed = (lastp && lastp.z !== z) ? speedMMM : seekMMM; - // moveTo({x:x, y:y, z:z}, moveSpeed); } - return point; + + // resume normal emit rules + setContouring(false); } - /** - * Emits arcs and/or lines from the arcQ to the current point set. - * @param {boolean} forceCircle emits a single circle iven if poly is not closed. - */ - function drainQ(forceCircle = false) { - // console.trace("draining") - let arcPreviewRes = 64 + let lastOut; - if (!camArcTolerance) { - return; - } - - if (arcQ.length > 4) { - // ondebug({arcQ}); - let vec1 = new THREE.Vector2(arcQ[1].x - arcQ[0].x, arcQ[1].y - arcQ[0].y); - let vec2 = new THREE.Vector2(arcQ.center[0].x - arcQ[0].x, arcQ.center[0].y - arcQ[0].y); - let clockwise = vec1.cross(vec2) < 0 - let gc = clockwise ? 2 : 3 - let from = arcQ[0]; - let to = arcQ.peek(); - let delta = from.distTo2D(to) - let closed = poly.isClosed() - arcQ.xSum = arcQ.center.reduce((t, v) => t + v.x, 0); - arcQ.ySum = arcQ.center.reduce((t, v) => t + v.y, 0); - arcQ.rSum = arcQ.center.reduce((t, v) => t + v.r, 0); - let cl = arcQ.center.length; - let center = newPoint( - arcQ.xSum / cl, - arcQ.ySum / cl, - ) - - // console.log("draining") - if (closed && arcQ.length == poly.points.length || forceCircle ) { - //if is a circle - // generate circle - // console.log("circle",{from, to,center}); - to = forceCircle? from.clone().setZ(to.z) : from - let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }); - // console.log({arcPoints}) - camOut(from, 1); - camOut(to, gc, { center: center.sub(from), clockwise, arcPoints }); - } else { - //if a non-circle arc - let arcPoints = arcToPath(from, to, arcPreviewRes, { clockwise, center }); - // console.log("arc") - // first arc point - camOut(from, 1); - // rest of arc to final point - camOut(to, gc, { center: center.sub(from), clockwise, arcPoints }); - polyLastPoint = to.clone(); + // arc output must handle shortened arcs from ease-down + // future support for 3d helical arcs will fix this + if (arcing) { + let skip = 0; + let type; + let center; + let lastP = points.peek(); + for (let point of points) { + lastOut = point.clone(); + if (type) { + // terminate arc early (caused by ease eating points) + skip = point === lastP ? 0 : skip - 1; + camOut(lastOut, skip ? -1 : type, { center, xfactor: xfactors[0] }); + if (!skip) center = type = undefined; + continue; + } else if (point.arc) { + let { arc } = point; + skip = arc.skip; + type = arc.clockwise ? 2 : 3; + center = arc.center.clone().move({ x: -point.x, y: -point.y }); + xfactors.push(xfactors.shift()); } - } else { - //if q too short, emit as lines - for (let rec of arcQ) { - camOut(rec, 1); - } - polyLastPoint = arcQ.peek().clone(); + camOut(lastOut); + } + } else { + for (let point of points) { + camOut(lastOut = point.clone()); } - arcQ.length = 0; - arcQ.center = undefined; } if (camDepthFirst) { newLayer(); } - return polyLastPoint; + return lastOut; } - function depthRoughPath(start, depth, levels, tops, emitter, fit, ease) { - let level = levels[depth]; - if (!(level && level.length)) { - return start; - } - let ltops = tops[depth]; - let fitted = fit ? ltops.filter(poly => poly.isInside(fit, 0.05)) : ltops; - let ftops = fitted.filter(top => !top.level_emit); - if (ftops.length > 1) { - ftops = POLY.route(ftops, start); - } - - function roughTopEmit(top, index, count, start) { - top.level_emit = true; - let inside = level.filter(poly => poly.isInside(top)); - if (ease) { - start.z += ease; - } - start = poly2polyEmit(inside, start, emitter, { mark: "emark", perm: true, swapdir: false }); - if (ease) { - start.z += ease; - } - start = depthRoughPath(start, depth + 1, levels, tops, emitter, top, ease); - return start; - } - - // output fragments (due to tabs) last - let frag = ftops.filter(p => p.open); - let full = ftops.filter(p => !p.open); - - poly2polyEmit(full, start, roughTopEmit, { mark: "emark", swapdir: false }); - poly2polyEmit(frag, start, roughTopEmit, { mark: "emark", swapdir: false }); - - return start; - } + // debug arc creation with visual speed cues + let xfactors = [0.2,0.5]; function depthOutlinePath(start, depth, levels, radius, emitter, dir, ease) { let bottm = depth < levels.length - 1 ? levels[levels.length - 1] : null; @@ -1006,12 +804,13 @@ export function prepare_one(widget, settings, print, firstPoint, update) { // limit level search to polys matching winding (inside vs outside) level = level.filter(p => p.isClockwise() === dir); // omit polys that match bottom level polys unless level above is cleared - start = poly2polyEmit(level, start, (poly, index, count, fromPoint) => { + start = poly2polyEmit(level, start, (poly, index) => { poly.level_emit = true; + let fromPoint = printPoint.clone(); if (ease) { fromPoint.z += ease; } - fromPoint = polyEmit(poly, index, count, fromPoint); + fromPoint = polyEmit(poly, index); if (ease) { fromPoint.z += ease; } @@ -1025,8 +824,6 @@ export function prepare_one(widget, settings, print, firstPoint, update) { return start; } - console.log({ prep: widget, firstPoint: firstPoint?.clone(), wmx, wmy, printPoint, origin: origin.clone() }); - // coming from a previous widget, use previous last point as starting point // make top start offset configurable printPoint = firstPoint || origin; @@ -1035,16 +832,18 @@ export function prepare_one(widget, settings, print, firstPoint, update) { addGCode, camOut, depthOutlinePath, - depthRoughPath, emitDrills, - emitTrace, + emitTraces, + getTool, newLayer, pocket, poly2polyEmit, polyEmit, printPoint, + setContouring, setDrill, setLasering, + setNextIsMove, setSpindle, setTolerance, setTool, @@ -1054,19 +853,32 @@ export function prepare_one(widget, settings, print, firstPoint, update) { }; let opSum = 0; - let opTot = widget.camops.map(op => op.weight()).reduce((a, v) => a + v); + let opTot = 0; + + // pre-flight check of ops + for (let op of widget.camops) { + opTot += op.weight(); + // ensure tool related parameters are available + // for the first index call when no tool is specified + if (!tool && op.op.tool) { + setTool(op.op.tool); + } + } for (let op of widget.camops) { - setTolerance(0); - nextIsMove = true; - currentOp = op.op; - isIndex = currentOp.type === 'index'; - isLathe = currentOp.type === 'lathe'; - isRough = currentOp.type === 'rough'; - isPocket = currentOp.type === 'pocket'; - isContour = currentOp.type === 'contour' || (isPocket && currentOp.contour); + contouring = false; + lasering = false; + let cop = currentOp = op.op; + isIndex = cop.type === 'index'; + isLathe = cop.type === 'lathe'; let weight = op.weight(); - newLayer(op.op); + newLayer(cop); + setTolerance(0); + setNextIsMove(); + if (cop.tool) setTool(cop.tool, cop.rate ?? feedRate, cop.plunge ?? plungeRate); + if (cop.spindle) setSpindle(cop.spindle); + // set printPoint in widget coordinate space + ops.printPoint = printPoint.clone().move({ x: -wmx, y: -wmy, z: -wmz }); op.prepare(ops, (progress, message) => { update((opSum + (progress * weight)) / opTot, message || op.type(), message); }); @@ -1095,51 +907,4 @@ export function prepare_one(widget, settings, print, firstPoint, update) { print.output = newOutput; return printPoint; -}; - -/** - * return tool Z clearance height for a line segment movement path - */ -function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) { - // when terrain skipped, top + pass used - if (terrain > 0) { - return terrain; - } - let maxz = z; - let check = []; - for (let i = 0; i < terrain.length; i++) { - let data = terrain[i]; - check.push(data); - if (data.z + zadd < z) { - break; - } - } - check.reverse(); - let p1 = newPoint(x1, y1); - let p2 = newPoint(x2, y2); - for (let i = 0; i < check.length; i++) { - let data = check[i]; - let int = data.tops.map(p => p.intersections(p1, p2, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; - } - let s1 = p1.slopeTo(p2).toUnit().normal(); - let s2 = p2.slopeTo(p1).toUnit().normal(); - let pa = p1.projectOnSlope(s1, off); - let pb = p2.projectOnSlope(s1, off); - int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; - } - pa = p1.projectOnSlope(s2, off); - pb = p2.projectOnSlope(s2, off); - int = data.tops.map(p => p.intersections(pa, pb, true)).flat(); - if (int.length) { - maxz = Math.max(maxz, data.z + zadd + over); - continue; - } - } - return maxz; } diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index 35fd16b4..96b92cdf 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -9,7 +9,7 @@ import { polygons as POLY } from '../../../geo/polygons.js'; import { setSliceTracker } from '../../core/slice.js'; import { ops as OPS } from './ops.js'; import { Tool } from './tool.js'; -import { Slicer as cam_slicer } from './slicer.js'; +import { Slicer as cam_slicer } from './slicer_cam.js'; import { CAM } from './driver-be.js'; /** @@ -25,19 +25,17 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let tabW = widget.group.filter(w => w != widget); let proc = settings.process, - sliceAll = widget.slices = [], camOps = widget.camops = [], + sliceAll = widget.slices = [], isIndexed = proc.camStockIndexed; - let stock, bounds, track, - camZTop, camZBottom, camZThru, wztop, ztOff, zbOff, - zBottom, zMin, zMax, zThru, zTop, - minToolDiam, maxToolDiam, dark, color, tabs, unsafe, units, - axisRotation, axisIndex, - part_size, + let axisRotation, axisIndex, + bounds, dark, color, stock, tabs, track, tool, unsafe, units, workarea, + camZTop, camZBottom, camZThru, minToolDiam, maxToolDiam, bottom_gap, bottom_part, bottom_stock, bottom_thru, bottom_z, bottom_cut, top_stock, top_part, top_gap, top_z, - workarea; + zBottom, zMin, zMax, zThru, zTop, + ztOff, zbOff, wztop; axisRotation = axisIndex = undefined; dark = settings.controller.dark; @@ -51,17 +49,20 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // allow recomputing later if widget or settings changes const var_compute = () => { let { camStockX, camStockY, camStockZ, camStockOffset } = proc; + ({ camZTop, camZBottom, camZThru } = proc); bounds = widget.getBoundingBox(); + let pos = widget.track.pos; stock = camStockOffset ? { x: bounds.dim.x + camStockX, y: bounds.dim.y + camStockY, z: bounds.dim.z + camStockZ, + center: newPoint(pos.x, pos.y, pos.z) } : { x: camStockX, y: camStockY, - z: camStockZ + z: camStockZ, + center: newPoint(pos.x, pos.y, pos.z) }; - ({ camZTop, camZBottom, camZThru } = proc); track = widget.track; wztop = track.top; ztOff = isIndexed ? (stock.z - bounds.dim.z) / 2 : (stock.z - wztop); @@ -71,7 +72,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) { zMax = bounds.max.z; zThru = camZThru; zTop = zMax + ztOff; - part_size = bounds.dim; bottom_gap = zbOff; bottom_part = 0; bottom_stock = -bottom_gap; @@ -142,7 +142,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let opList = []; let opSum = 0; let opTot = 0; - let shadows = {}; let slicer; let state = { addSlices, @@ -159,9 +158,10 @@ export async function cam_slice(settings, widget, onupdate, ondone) { setAxisIndex, setToolDiam, settings, - shadowAt, + shadowAt(z) { return widget.shadowAt(z) }, slicer, tabs, + tool, unsafe, updateSlicer, updateToolDiams, @@ -203,9 +203,9 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } async function computeShadows() { - shadows = {}; + // console.log('(re)compute shadows'); await new OPS.shadow(state, { type: "shadow", silent: true }).slice(progress => { - // console.log('reshadow', progress.round(3)); + // console.log('reshadowing', progress.round(3)); }); } @@ -223,21 +223,6 @@ export async function cam_slice(settings, widget, onupdate, ondone) { maxToolDiam = Math.max(maxToolDiam, toolDiam); } - function shadowAt(z) { - let cached = shadows[z]; - if (cached) { - return cached; - } - // find closest shadow above and use to speed up delta shadow gen - let zover = Object.keys(shadows).map(v => parseFloat(v)).filter(v => v > z); - let minZabove = Math.min(Infinity, ...zover); - let shadow = computeShadowAt(widget, z, minZabove); - if (minZabove < Infinity) { - shadow = POLY.union([...shadow, ...shadows[minZabove]], 0.001, true); - } - return shadows[z] = POLY.setZ(shadow, z); - } - async function setAxisIndex(degrees = 0, absolute = true) { axisIndex = absolute ? degrees : (axisIndex || 0) + degrees; axisRotation = (Math.PI / 180) * axisIndex; @@ -284,14 +269,15 @@ export async function cam_slice(settings, widget, onupdate, ondone) { let activeOps = proc.ops.filter(op => !op.disabled); - // silently preface op list with OpShadow if (isIndexed) { + // preface op list with OpIndex if (activeOps.length === 0 || activeOps[0].type !== 'index') { opList.push(new OPS.index(state, { type: "index", index: 0 })); opTot += opList.peek().weight(); } } else { + // preface op list with OpShadow opList.push(new OPS.shadow(state, { type: "shadow", silent: true })); opTot += opList.peek().weight(); } @@ -324,11 +310,15 @@ export async function cam_slice(settings, widget, onupdate, ondone) { workover.bottom_z = isIndexed ? valz.ov_botz : bottom_stock + valz.ov_botz; workover.bottom_cut = Math.max(workover.bottom_z, -zThru); } + if (valz.tool) { + tool = new Tool(settings, valz.tool); + } let { note, type } = op.op; let named = note ? note.split(' ').filter(v => v.charAt(0) === '#') : []; let layername = named.length ? named : (note ? `${type} (${note})` : type); Object.assign(state, { layername, + tool, zBottom, zThru, ztOff, @@ -336,9 +326,16 @@ export async function cam_slice(settings, widget, onupdate, ondone) { zTop, workarea: workover }); + let operr; await op.slice((progress, message) => { onupdate((opSum + (progress * weight)) / opTot, message || op.type()); + }).catch(e => { + operr = e; + console.trace(e); }); + if (operr) { + return error(operr); + } // update tracker rotation for next slice output() visualization tracker.rotation = isIndexed ? axisRotation : 0; camOps.push(op); @@ -349,77 +346,12 @@ export async function cam_slice(settings, widget, onupdate, ondone) { // reindex sliceAll.forEach((slice, index) => slice.index = index); - // used in printSetup() - // used in CAM.prepare.getZClearPath() - // add tabs to terrain tops so moves avoid them - if (tabs) { - state.terrain.forEach(slab => { - tabs.forEach(tab => { - if (tab.pos.z + tab.dim.z / 2 >= slab.z) { - let all = [...slab.tops, tab.poly]; - slab.tops = POLY.union(all, 0, true); - // slab.slice.output() - // .setLayer("debug-tabs", {line: 0x880088, thin: true }) - // .addPolys(POLY.setZ(slab.tops.clone(true), slab.z), { thin: true }); - } - }); - }); - } - - // add shadow perimeter to terrain to catch outside moves off part - let tabpoly = tabs ? tabs.map(tab => tab.poly) : []; - let allpoly = POLY.union([...state.shadow.base, ...tabpoly], 0, true); - let shadowOff = maxToolDiam < 0 ? allpoly : - POLY.offset(allpoly, [minToolDiam / 2, maxToolDiam / 2], { count: 2, flat: true, minArea: 0 }); - state.terrain.forEach(level => level.tops.appendAll(shadowOff)); - - widget.terrain = state.skipTerrain ? null : state.terrain; widget.minToolDiam = minToolDiam; widget.maxToolDiam = maxToolDiam; ondone(); }; -export function addDogbones(poly, dist, reverse) { - if (Array.isArray(poly)) { - return poly.forEach(p => addDogbones(p, dist)); - } - let open = poly.open; - let isCW = poly.isClockwise(); - if (reverse || poly.parent) isCW = !isCW; - let oldpts = poly.points.slice(); - let lastpt = oldpts[oldpts.length - 1]; - let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); - let length = oldpts.length + (open ? 0 : 1); - let newpts = []; - for (let i = 0; i < length; i++) { - let nextpt = oldpts[i % oldpts.length]; - let nextsl = lastpt.slopeTo(nextpt).toUnit(); - let adiff = lastsl.angleDiff(nextsl, true); - let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180; - if (!open || (i > 1 && i < length)) { - if (isCW && adiff > 45) { - let newa = newSlopeFromAngle(lastsl.angle + bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } else if (!isCW && adiff < -45) { - let newa = newSlopeFromAngle(lastsl.angle - bdiff); - newpts.push(lastpt.projectOnSlope(newa, dist)); - newpts.push(lastpt.clone()); - } - } - lastsl = nextsl; - lastpt = nextpt; - if (i < oldpts.length) { - newpts.push(nextpt); - } - } - poly.points = newpts; - if (poly.inner) { - addDogbones(poly.inner, dist, true); - } -}; - export async function traces(settings, widget) { if (widget.traces) { return false; @@ -703,7 +635,7 @@ export async function holes(settings, widget, individual, rec, onProgress) { for (let [i, slice] of slices.entries()) { for (let top of slice.tops) { // console.log("slicing",slice.z,top) - slice.shadow = computeShadowAt(widget, slice.z, 0); + slice.shadow = await widget.shadowAt(slice.z); let inner = top.inner; if (!inner) { //no holes continue; @@ -903,75 +835,4 @@ function healPolys(noff, sameZ = true) { } } return noff; -} - -// union triangles > z (opt cap < ztop) into polygon(s) -export function computeShadowAt(widget, z, ztop) { - const geo = widget.cache.geo; - const length = geo.length; - // cache faces with normals up - if (!widget.cache.shadow) { - const faces = []; - for (let i = 0, ip = 0; i < length; i += 3) { - const a = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const b = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const c = new THREE.Vector3(geo[ip++], geo[ip++], geo[ip++]); - const n = THREE.computeFaceNormal(a, b, c); - if (n.z > 0.001) { - faces.push(a, b, c); - // faces.push(newPoint(...a), newPoint(...b), newPoint(...c)); - } - } - widget.cache.shadow = faces; - } - const found = []; - const faces = widget.cache.shadow; - const { checkOverUnderOn, intersectPoints } = cam_slicer; - for (let i = 0; i < faces.length;) { - const a = faces[i++]; - const b = faces[i++]; - const c = faces[i++]; - let where = undefined; - if (ztop && a.z > ztop && b.z > ztop && c.z > ztop) { - // skip faces over top threshold - continue; - } - if (a.z < z && b.z < z && c.z < z) { - // skip faces under threshold - continue; - } else if (a.z > z && b.z > z && c.z > z) { - found.push([a, b, c]); - } else { - // check faces straddling threshold - const where = { under: [], over: [], on: [] }; - checkOverUnderOn(newPoint(a.x, a.y, a.z), z, where); - checkOverUnderOn(newPoint(b.x, b.y, b.z), z, where); - checkOverUnderOn(newPoint(c.x, c.y, c.z), z, where); - if (where.on.length === 0 && (where.over.length === 2 || where.under.length === 2)) { - // compute two point intersections and construct line - let line = intersectPoints(where.over, where.under, z); - if (line.length === 2) { - if (where.over.length === 2) { - found.push([where.over[1], line[0], line[1]]); - found.push([where.over[0], where.over[1], line[0]]); - } else { - found.push([where.over[0], line[0], line[1]]); - } - } else { - console.log({ msg: "invalid ips", line: line, where: where }); - } - } - } - } - - let polys = found.map(a => { - return newPolygon() - .add(a[0].x, a[0].y, a[0].z) - .add(a[1].x, a[1].y, a[1].z) - .add(a[2].x, a[2].y, a[2].z); - }); - - polys = POLY.union(polys, 0, true); - - return polys; } \ No newline at end of file diff --git a/src/kiri/mode/cam/slicer.js b/src/kiri/mode/cam/slicer_cam.js similarity index 86% rename from src/kiri/mode/cam/slicer.js rename to src/kiri/mode/cam/slicer_cam.js index 0cd36e38..4a4109ea 100644 --- a/src/kiri/mode/cam/slicer.js +++ b/src/kiri/mode/cam/slicer_cam.js @@ -1,15 +1,19 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base, util } from '../../../geo/base.js'; +import { util } from '../../../geo/base.js'; import { decode, decodePointArray } from '../../core/codec.js'; -import { newLine, newOrderedLine } from '../../../geo/line.js'; +import { newLine } from '../../../geo/line.js'; import { newPoint } from '../../../geo/point.js'; import { newSlice } from '../../core/slice.js'; import { polygons as POLY } from '../../../geo/polygons.js'; -import { slicer } from '../../../geo/slicer.js'; +import { + checkOverUnderOn, + intersectPoints, + makeZLine, + removeDuplicateLines, + sliceConnect, +} from '../../../geo/slicer.js'; -const { sliceConnect, sliceDedup } = slicer; -const { config } = base; const timing = false; const zDecimal = 3; const epsilon = 10e-5; @@ -232,15 +236,18 @@ export class Slicer { // console.log({ oneach: slice, ...track }); })); } - const data = (await Promise.all(promises)).flat(); - data.sort((a, b) => b.z - a.z).forEach((rec, i) => { + const data = (await Promise.all(promises)).flat(); + data.sort((a, b) => b.z - a.z); + + for (let i=0; i 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; diff --git a/src/kiri/mode/cam/slicer_topo.js b/src/kiri/mode/cam/slicer_topo.js index 3fe74ca6..7aeeec3b 100644 --- a/src/kiri/mode/cam/slicer_topo.js +++ b/src/kiri/mode/cam/slicer_topo.js @@ -1,18 +1,16 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -import { base } from '../../../geo/base.js'; import { newPoint } from '../../../geo/point.js'; -import { newOrderedLine } from '../../../geo/line.js'; - -const { config } = base; +import { + checkOverUnderOn, + makeZLine, + removeDuplicateLines, + intersectPoints +} from '../../../geo/slicer.js'; /** Slicer used in Topo3 and Topo4 to find contour lines */ export class Slicer { - static intersectPoints = intersectPoints; - static checkOverUnderOn = checkOverUnderOn; - static removeDuplicateLines = removeDuplicateLines; - constructor(index = 0) { this.min = Infinity; this.max = -Infinity; @@ -143,177 +141,3 @@ export class Slicer { } } - -/** - * given a point, append to the correct - * 'where' objec tarray (on, over or under) - * - * @param {Point} p - * @param {number} z offset - * @param {Obejct} where - */ -function checkOverUnderOn(p, z, where) { - let delta = p.z - z; - if (Math.abs(delta) < config.precision_slice_z) { // on - where.on.push(p); - } else if (delta < 0) { // under - where.under.push(p); - } else { // over - where.over.push(p); - } -} - -/** - * Given a point over and under a z offset, calculate - * and return the intersection point on that z plane - * - * @param {Point} over - * @param {Point} under - * @param {number} z offset - * @returns {Point} intersection point - */ -function intersectPoints(over, under, z) { - let ip = []; - for (let i = 0; i < over.length; i++) { - for (let j = 0; j < under.length; j++) { - ip.push(over[i].intersectZ(under[j], z)); - } - } - return ip; -} - -/** - * Ensure points are unique with a cache/key algorithm - */ -function getCachedPoint(phash, p) { - let cached = phash[p.key]; - if (!cached) { - phash[p.key] = p; - return p; - } - return cached; -} - -/** - * Given two points and hints about their edges, - * return a new Line object with points sorted - * lexicographically by key. This allows for future - * line de-duplication and joins. - * - * @param {Object} phash - * @param {Point} p1 - * @param {Point} p2 - * @param {boolean} [coplanar] - * @param {boolean} [edge] - * @returns {Line} - */ -function makeZLine(phash, p1, p2, coplanar, edge) { - p1 = getCachedPoint(phash, p1); - p2 = getCachedPoint(phash, p2); - let line = newOrderedLine(p1, p2); - line.coplanar = coplanar || false; - line.edge = edge || false; - return line; -} - -/** - * eliminate duplicate lines and interior-only lines (coplanar) - * - * lines are sorted using lexicographic point keys such that - * they are comparable even if their points are reversed. hinting - * for deletion, co-planar and suspect shared edge is detectable at - * this time. - * - * @param {Line[]} lines - * @returns {Line[]} - */ -function removeDuplicateLines(lines, debug) { - let output = [], - tmplines = [], - points = [], - pmap = {}; - - function cachePoint(p) { - let cp = pmap[p.key]; - if (cp) return cp; - points.push(p); - pmap[p.key] = p; - return p; - } - - function addLinesToPoint(point, line) { - cachePoint(point); - if (!point.group) point.group = [line]; - else point.group.push(line); - } - - // mark duplicates for deletion preserving edges - lines.sort(function (l1, l2) { - if (l1.key === l2.key) { - l1.del = !l1.edge; - l2.del = !l2.edge; - if (debug && (l1.del || l2.del)) { - console.log('dup', l1, l2); - } - return 0; - } - return l1.key < l2.key ? -1 : 1; - }); - - // associate points with their lines, cull deleted - lines.forEach(function (line) { - if (!line.del) { - tmplines.push(line); - addLinesToPoint(line.p1, line); - addLinesToPoint(line.p2, line); - } - }); - - // merge collinear lines - points.forEach(function (point) { - if (point.group.length != 2) return; - let l1 = point.group[0], - l2 = point.group[1]; - if (l1.isCollinear(l2)) { - l1.del = true; - l2.del = true; - // find new endpoints that are not shared point - let p1 = l1.p1 != point ? l1.p1 : l1.p2, - p2 = l2.p1 != point ? l2.p1 : l2.p2, - newline = newOrderedLine(p1, p2); - // remove deleted lines from associated points - p1.group.remove(l1); - p1.group.remove(l2); - p2.group.remove(l1); - p2.group.remove(l2); - // associate new line with points - p1.group.push(newline); - p2.group.push(newline); - // add new line to lines array - newline.edge = l1.edge || l2.edge; - tmplines.push(newline); - } - }); - - // mark duplicates for deletion - // but preserve one if it's an edge - tmplines.sort(function (l1, l2) { - if (l1.key === l2.key) { - l1.del = true; - l2.del = !l2.edge; - return 0; - } - return l1.key < l2.key ? -1 : 1; - }); - - // create new line array culling deleted - tmplines.forEach(function (line) { - if (!line.del) { - output.push(line); - line.p1.group = null; - line.p2.group = null; - } - }); - - return output; -} diff --git a/src/kiri/mode/cam/tool.js b/src/kiri/mode/cam/tool.js index 73d7ebd5..4cdae884 100644 --- a/src/kiri/mode/cam/tool.js +++ b/src/kiri/mode/cam/tool.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- All Rights Reserved */ -const HPI = Math.PI / 2; const RAD2DEG = 180 / Math.PI; +const DEG2RAD = Math.PI / 180; class Tool { constructor(settings, id, number) { @@ -15,6 +15,7 @@ class Tool { this.tool.number = number >= 0 ? number : this.tool.number; this.tool.id = id >= 0 ? id : this.tool.id; } + this.work_units = settings.controller.units === 'mm' ? 1 : 25.4; } getID() { @@ -33,6 +34,12 @@ class Tool { return this.tool.number; } + // for taper tips, returns value in tool units + // otherwise returns a fraction of the flute diameter in tool units + getStepSize(frac) { + return (this.hasTaper() ? frac : this.fluteDiameter() * (frac ?? 1)) * this.work_units; + } + isMetric() { return this.tool.metric; } @@ -66,15 +73,22 @@ class Tool { return diam ? diam * step : step; } - setTaperLengthFromAngle(angle) { - const rad = (this.flute_diam - this.taper_tip) / 2; - return this.flute_len = calcTaperLength(rad, angle); - } + // setTaperLengthFromAngle(angle) { + // const rad = (this.flute_diam - this.taper_tip) / 2; + // return this.flute_len = calcTaperLength(rad, angle); + // } getTaperAngle() { - return calcTaperAngle((this.flute_diam - this.taper_tip) / 2, this.flute_len); + let { flute_diam, flute_len, taper_tip } = this.tool; + return calcTaperAngle((flute_diam - taper_tip) / 2, flute_len); } + // getTaperBallExtent() { + // let rad = this.tipDiameter() / 2; + // let ang = this.getTaperAngle() * DEG2RAD; + // return calcTaperBallExtent(rad, ang); + // } + shaftLength() { return this.unitScale() * this.tool.shaft_len; } @@ -102,12 +116,16 @@ class Tool { return this.tool.type === "tapermill"; } + isTaperBall() { + return this.tool.type === "taperball"; + } + isDrill() { return this.tool.type === "drill"; } hasTaper() { - return this.isTaperMill(); + return this.isTaperMill() || this.isTaperBall(); } /** @@ -115,60 +133,77 @@ class Tool { * Float32Array stored in `this.profile` and `this.profileDim` containing * dimensions of the profile in pixels and shared array buffer size. * - * @param {number} resolution - number of pixels for the tool profile + * @param {number} resolution - pixel size in mm for the raster tool profile * * @return {Tool} this */ generateProfile(resolution) { // generate tool profile let ball = this.isBallMill(), + taperball = this.isTaperBall(), taper = this.hasTaper(), drill = this.isDrill(), - tip_diameter = this.tipDiameter(), - drill_tip_length = this.drillTipLength(), - shaft_offset = this.fluteLength(), - flute_diameter = this.fluteDiameter(), - shaft_diameter = Math.max(flute_diameter, this.shaftDiameter()), - max_diameter = Math.max(flute_diameter, shaft_diameter), - shaft_pix_float = max_diameter / resolution, - shaft_pix_int = Math.round(shaft_pix_float), - shaft_radius_pix_float = shaft_pix_float / 2, - flute_radius = flute_diameter / 2, - flute_pix_float = flute_diameter / resolution, - flute_radius_pix_float = flute_pix_float / 2, - tip_pix_float = tip_diameter / resolution, - tip_radius_pix_float = tip_pix_float / 2, - tip_max_radius_offset = flute_radius_pix_float - tip_radius_pix_float, - profile_pix_iter = shaft_pix_int + (1 - shaft_pix_int % 2), - toolCenter = (shaft_pix_int - (shaft_pix_int % 2)) / 2, + tip_dia = this.tipDiameter(), + flute_dia = this.fluteDiameter(), + flute_len = this.fluteLength(), + flute_rad = flute_dia / 2, + shaft_dia = Math.max(flute_dia, this.shaftDiameter()), + max_dia = Math.max(flute_dia, shaft_dia), + larger_shaft = shaft_dia - flute_dia > 0.001, + pix_sh_dia_float = max_dia / resolution, + pix_sh_dia_int = Math.round(pix_sh_dia_float), + pix_sh_rad_float = pix_sh_dia_float / 2, + pix_fl_dia_float = flute_dia / resolution, + pix_fl_rad_float = pix_fl_dia_float / 2, + pix_tip_dia_float = tip_dia / resolution, + pix_tip_rad_float = pix_tip_dia_float / 2, + tip_max_rad_offset = pix_fl_rad_float - pix_tip_rad_float, + pix_profile_iter = pix_sh_dia_int + (1 - pix_sh_dia_int % 2), + toolCenter = (pix_sh_dia_int - (pix_sh_dia_int % 2)) / 2, toolOffset = [], - larger_shaft = shaft_diameter - flute_diameter > 0.001, - rpixsq = flute_radius_pix_float * flute_radius_pix_float, - maxo = -Infinity; + maxo = -Infinity, + // ball taper magic + a = this.getTaperAngle() * DEG2RAD, + r = (tip_dia/2) * (1 + Math.sin(a)) / Math.cos(a), + b = r * Math.cos(a), + pix_b = b / resolution, + pix_r = r / resolution; // for each point in tool profile, check inside radius - for (let x = 0; x < profile_pix_iter; x++) { - for (let y = 0; y < profile_pix_iter; y++) { + for (let x = 0; x < pix_profile_iter; x++) { + for (let y = 0; y < pix_profile_iter; y++) { let dx = x - toolCenter, dy = y - toolCenter, dist_from_center = Math.sqrt(dx * dx + dy * dy); - if (dist_from_center <= flute_radius_pix_float) { // if xy point inside flute radius + if (dist_from_center <= pix_fl_rad_float) { // if xy point inside flute radius maxo = Math.max(maxo, dx, dy); // flute offset points let z_offset = 0; if (ball) { - let rd = dist_from_center * dist_from_center; - z_offset = Math.sqrt(rpixsq - rd) * resolution - flute_radius; - // z_offset = (1 - Math.cos((dist_from_center / flute_radius_pix_float) * HPI)) * -flute_radius; - } else if (taper && dist_from_center >= tip_radius_pix_float) {// if tapered and not in the flat tip radius - z_offset = ((dist_from_center - tip_radius_pix_float) / tip_max_radius_offset) * -shaft_offset; + let ball_rad_sq = dist_from_center * dist_from_center; + let ball_rpixsq = pix_fl_rad_float * pix_fl_rad_float; + z_offset = Math.sqrt(ball_rpixsq - ball_rad_sq) * resolution - flute_rad; + } else if (taperball) { + // taperball: spherical tip, conical above + if (dist_from_center <= pix_b) { + // inside ball radius - spherical surface + let ball_rad_sq = dist_from_center * dist_from_center; + let ball_rpixsq = pix_r * pix_r; + z_offset = Math.sqrt(ball_rpixsq - ball_rad_sq) * resolution - r; + } else { + // outside ball radius - conical taper + z_offset = ((dist_from_center - pix_tip_rad_float) / tip_max_rad_offset) * -flute_len; + } + } else if (taper && dist_from_center >= pix_tip_rad_float) { + // if tapered and not in the flat tip radius + z_offset = ((dist_from_center - pix_tip_rad_float) / tip_max_rad_offset) * -flute_len; } else if (drill) { z_offset = -dist_from_center / 45; } toolOffset.push(dx, dy, z_offset); - } else if (shaft_offset && larger_shaft && dist_from_center <= shaft_radius_pix_float) { + } else if (flute_len && larger_shaft && dist_from_center <= pix_sh_rad_float) { // shaft offset points - toolOffset.push(dx, dy, -shaft_offset); + toolOffset.push(dx, dy, -flute_len); } } } @@ -179,8 +214,8 @@ class Tool { this.profile = profile; this.profileDim = { - size: shaft_diameter, - pix: profile_pix_iter + 2, + size: shaft_dia, + pix: pix_profile_iter + 2, maxo }; @@ -190,19 +225,24 @@ class Tool { function calcTaperAngle(rad, len) { return (Math.atan(rad / len) * RAD2DEG); -}; +} function calcTaperLength(rad, angle) { return (rad / Math.tan(angle)); -}; +} + +function calcTaperBallExtent(rad, angle) { + return rad * (1 - Math.sin(angle * 2)); +} function getToolDiameter(settings, id) { return new Tool(settings, id).fluteDiameter(); -}; +} export { Tool, calcTaperAngle, + calcTaperBallExtent, calcTaperLength, getToolDiameter }; diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 8c6b43d2..16dc117e 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -4,7 +4,7 @@ import { $ } from '../../../moto/webui.js'; import { api } from '../../core/api.js'; import { consts } from '../../core/consts.js'; import { settings as setconf } from '../../core/settings.js'; -import { calcTaperLength, calcTaperAngle } from './tool.js'; +import { Tool, calcTaperAngle, calcTaperBallExtent, calcTaperLength } from './tool.js'; const DEG2RAD = Math.PI / 180; @@ -12,8 +12,7 @@ let { MODES } = consts, DOC = document, selectedTool = null, editTools = null, - maxTool = 0, - toolNames = ['endmill','ballmill','tapermill','drill']; + maxTool = 0; function settings() { return api.conf.get(); @@ -35,27 +34,110 @@ function renderTools() { function selectTool(tool) { const { ui } = api; selectedTool = tool; - ui.toolName.value = tool.name; - ui.toolNum.value = tool.number; - ui.toolFluteDiam.value = tool.flute_diam; - ui.toolFluteLen.value = tool.flute_len; - ui.toolShaftDiam.value = tool.shaft_diam; - ui.toolShaftLen.value = tool.shaft_len; - ui.toolTaperTip.value = tool.taper_tip || 0; - ui.toolMetric.checked = tool.metric; - ui.toolType.selectedIndex = toolNames.indexOf(tool.type); - if (tool === 'tapermill') { - ui.toolTaperAngle.value = calcTaperAngle( - (tool.flute_diam - tool.taper_tip) / 2, tool.flute_len - ).round(1); - } else if(tool === 'drill'){ - ui.toolTaperAngle.value = 118; + + api.util.rec2ui({ + toolName: selectedTool.name, + toolType: selectedTool.type, + toolNum: selectedTool.number, + toolMetric: selectedTool.metric, + toolShaftDiam: selectedTool.shaft_diam, + toolShaftLen: selectedTool.shaft_len, + toolFluteDiam: selectedTool.flute_diam, + toolFluteLen: selectedTool.flute_len, + toolTaperAngle: selectedTool.taper_angle, + toolTaperTip: selectedTool.taper_tip, + },{ + toolName: ui.toolName, + toolType: ui.toolType, + toolNum: ui.toolNum, + toolMetric: ui.toolMetric, + toolShaftDiam: ui.toolShaftDiam, + toolShaftLen: ui.toolShaftLen, + toolFluteDiam: ui.toolFluteDiam, + toolFluteLen: ui.toolFluteLen, + toolTaperAngle: ui.toolTaperAngle, + toolTaperTip: ui.toolTaperTip, + }); + + if (tool.type === 'tapermill' || tool.type === 'taperball') { + const taperLen = tool.flute_len; + ui.toolTaperAngle.value = + selectedTool.taper_angle = + calcTaperAngle( (tool.flute_diam - tool.taper_tip) / 2, taperLen ).round(1); + } else if (tool === 'drill') { + ui.toolTaperAngle.value = selectedTool.taper_angle = 90; } else { - ui.toolTaperAngle.value = 0; + ui.toolTaperAngle.value = selectedTool.taper_angle = 0; } + renderTool(tool); } +export function updateTool(ev) { + const { ui } = api; + + let changed = { + toolName: selectedTool.name, + toolType: selectedTool.type, + toolNum: selectedTool.number, + toolMetric: selectedTool.metric, + toolShaftDiam: selectedTool.shaft_diam, + toolShaftLen: selectedTool.shaft_len, + toolFluteDiam: selectedTool.flute_diam, + toolFluteLen: selectedTool.flute_len, + toolTaperAngle: selectedTool.taper_angle, + toolTaperTip: selectedTool.taper_tip, + }; + + api.util.ui2rec(changed,{ + toolName: ui.toolName, + toolType: ui.toolType, + toolNum: ui.toolNum, + toolMetric: ui.toolMetric, + toolShaftDiam: ui.toolShaftDiam, + toolShaftLen: ui.toolShaftLen, + toolFluteDiam: ui.toolFluteDiam, + toolFluteLen: ui.toolFluteLen, + toolTaperAngle: ui.toolTaperAngle, + toolTaperTip: ui.toolTaperTip, + }); + + selectedTool.name = changed.toolName; + selectedTool.type = changed.toolType; + selectedTool.number = changed.toolNum; + selectedTool.metric = changed.toolMetric; + selectedTool.shaft_diam = changed.toolShaftDiam; + selectedTool.shaft_len = changed.toolShaftLen; + selectedTool.flute_diam = changed.toolFluteDiam; + selectedTool.flute_len = changed.toolFluteLen; + selectedTool.taper_angle = changed.toolTaperAngle; + selectedTool.taper_tip = changed.toolTaperTip; + + if (selectedTool.type === 'tapermill' || selectedTool.type === 'taperball') { + const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2; + const ballRadius = selectedTool.type === 'taperball' ? selectedTool.taper_tip / 2 : 0; + + if (ev && ev.target === ui.toolTaperAngle) { + const angle = parseFloat(ev.target.value || 5); + const len = calcTaperLength(rad, angle * DEG2RAD); + selectedTool.flute_len = len + ballRadius; + ui.toolTaperAngle.value = angle.round(1); + ui.toolFluteLen.value = selectedTool.flute_len.round(4); + } else { + const taperLen = selectedTool.flute_len - ballRadius; + ui.toolTaperAngle.value = + selectedTool.taper_angle = + calcTaperAngle(rad, taperLen).round(1); + } + } else { + ui.toolTaperAngle.value = selectedTool.taper_angle = 0; + } + + renderTools(); + setToolChanged(true); + renderTool(selectedTool); +} + function otag(o) { if (Array.isArray(o)) { let out = [] @@ -77,157 +159,89 @@ function otag(o) { function renderTool(tool) { const { ui } = api; - let type = selectedTool.type; - let taper = type=== 'tapermill' - let drill = type === 'drill' - const drillAngleRad = 140 * Math.PI / 180 - ui.toolTaperAngle.disabled = taper ? undefined : 'true'; - ui.toolTaperTip.disabled = taper ? undefined : 'true'; $('tool-view').innerHTML = ''; setTimeout(() => { - let svg = $('tool-svg'), - pad = 10, - dim = { w: svg.clientWidth, h: svg.clientHeight }, - max = { w: dim.w - pad * 2, h: dim.h - pad * 2}, - off = { x: pad, y: pad }, - isBall = type === "ballmill", - shaft_fill = "#cccccc", - flute_fill = "#dddddd", - stroke = "#777777", - stroke_width = 3, - stroke_thin = stroke_width / 2, - shaft = tool.shaft_len || 1, - flute = tool.flute_len || 1, - drillTip = drill ? 0.5 * tool.flute_diam * Math.sin(drillAngleRad) : 0, - total_len = shaft + flute+drillTip, - units = dim.h / total_len, - shaft_len = (shaft / total_len) * max.h, - flute_len = (flute / total_len) * max.h, - drill_tip_len = (drillTip / total_len) * max.h, - shaft_diam = tool.shaft_diam * units, - flute_diam = tool.flute_diam * units, - // total_wid = Math.max(flute_diam, shaft_diam), - shaft_off = (max.w - shaft_diam) / 2, - flute_off = (max.w - flute_diam) / 2, - taper_off = (max.w - (tool.taper_tip || 0) * units) / 2, - parts = [ - // shaft rectangle - { rect: { - x: off.x + shaft_off, - y: off.y, - width: max.w - shaft_off * 2, - height: shaft_len, - fill: shaft_fill, - stroke_width, - stroke - } } - ]; - if (taper) { - let yoff = off.y + shaft_len; - // let mid = dim.w / 2; - parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ - `M ${off.x + flute_off} ${yoff}`, - `L ${off.x + taper_off} ${yoff + flute_len}`, - `L ${dim.w - off.x - taper_off} ${yoff + flute_len}`, - `L ${dim.w - off.x - flute_off} ${yoff}`, - `z` - ].join('\n')}}); - } else if(drill){ - const x1 = off.x + flute_off, - y1 = off.y + shaft_len, - x2 = dim.w - off.x - flute_off, - y2 = y1 + flute_len, - xMid = dim.w / 2 + let svg = $('tool-svg'); + let pad = 10; + let dim = { w: svg.clientWidth, h: svg.clientHeight }; + let max = { w: dim.w - pad * 2, h: dim.h - pad * 2 }; + let off = { x: pad, y: pad }; - parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ - `M ${x1} ${y1}`, //move to top left - `L ${x1} ${y2}`, //line to bottom left - `L ${xMid} ${y2+drill_tip_len}`, //line to bottom mid point - `L ${x2} ${y2}`, //line to bottom right - `L ${x2} ${y1}`, //line to top right - `z` - ].join('\n')}}); - //add drill flute lines - parts.push({ line: { - x1, y1, x2, y2: (y1 + y2) / 2, - stroke, stroke_width: stroke_thin - } }); - parts.push({ line: { - x1, y1: (y1 + y2) / 2, x2, y2, - stroke, stroke_width: stroke_thin - } }); - } else { - let fl = isBall ? flute_len - flute_diam/2 : flute_len; - let x1 = off.x + flute_off; - let y1 = off.y + shaft_len; - let x2 = x1 + max.w - flute_off * 2; - let y2 = y1 + fl; - // flute rectangle - parts.push({ rect: { - x: off.x + flute_off, - y: off.y + shaft_len, - width: max.w - flute_off * 2, - height: fl, - fill: flute_fill, - stroke_width, - stroke, - } }); - // hatch "fill" flute - parts.push({ line: { x1, y1, x2, y2, stroke, stroke_width: stroke_thin } }); - parts.push({ line: { - x1: (x1 + x2) / 2, y1, x2, y2: (y1 + y2) / 2, - stroke, stroke_width: stroke_thin - } }); - parts.push({ line: { - x1, y1: (y1 + y2) / 2, x2: (x1 + x2) / 2, y2, - stroke, stroke_width: stroke_thin - } }); - } - if (isBall) { - let rad = (max.w - flute_off * 2) / 2; - let xend = dim.w - off.x - flute_off; - let yoff = off.y + shaft_len + flute_len + stroke_width/2 - flute_diam/2; - parts.push({path: {stroke_width, stroke, fill:flute_fill, d:[ - `M ${off.x + flute_off} ${yoff}`, - `A ${rad} ${rad} 0 0 0 ${xend} ${yoff}`, - // `L ${off.x + flute_off} ${yoff}` - ].join('\n')}}) + // Create Tool instance and generate profile + const toolInst = new Tool(settings(), tool.id); + const resolution = (toolInst.maxDiameter() / toolInst.unitScale()) / 100; + toolInst.generateProfile(resolution); + + const profile = toolInst.profile; + const { pix } = toolInst.profileDim; + + // Extract cross-section at y=0 (center line) + // Profile is stored as [dx, dy, z_offset, dx, dy, z_offset, ...] + let crossSection = []; + for (let i = 0; i < profile.length; i += 3) { + let dx = profile[i]; + let dy = profile[i + 1]; + let z = profile[i + 2]; + // Only take points where dy is approximately 0 (center line) + if (Math.abs(dy) < 0.01) { + crossSection.push({ x: dx * resolution, z }); + } } + + // Section lengths + let slen = toolInst.shaftLength(); + let flen = toolInst.fluteLength(); + + // Find bounds + let minZ = Math.min(...crossSection.map(p => p.z), -(slen + flen)); + let maxZ = Math.max(...crossSection.map(p => p.z), 0); + let minX = Math.min(...crossSection.map(p => p.x)); + let maxX = Math.max(...crossSection.map(p => p.x)); + let zRange = maxZ - minZ; + let xRange = maxX - minX; + + // Scale to fit + let scale = Math.min(max.h / zRange, max.w / xRange); + + // Center horizontally if tool is narrower than viewport + let xOffset = off.x + (max.w - xRange * scale) / 2; + + // Draw vertical lines for each point + let parts = []; + let stroke_width = 1; + + crossSection.forEach(p => { + let x = xOffset + (p.x - minX) * scale; + let y1 = dim.h - off.y - (maxZ - p.z) * scale; // tip point + let y2 = dim.h - off.y - (flen) * scale; // top flute + let y3 = dim.h - off.y - (slen + flen) * scale; // top shaft + + // flute + parts.push({ line: { + x1: x, + x2: x, + y1: y1, + y2: y2, + stroke: "#999999", + stroke_width + }}); + + // shaft + parts.push({ line: { + x1: x, + x2: x, + y1: y2, + y2: y3, + stroke: "#666666", + stroke_width + }}); + + }); + svg.innerHTML = otag(parts); }, 10); } -export function updateTool(ev) { - const { ui } = api; - selectedTool.name = ui.toolName.value; - selectedTool.number = parseInt(ui.toolNum.value); - selectedTool.flute_diam = parseFloat(ui.toolFluteDiam.value); - selectedTool.flute_len = parseFloat(ui.toolFluteLen.value); - selectedTool.shaft_diam = parseFloat(ui.toolShaftDiam.value); - selectedTool.shaft_len = parseFloat(ui.toolShaftLen.value); - selectedTool.taper_tip = parseFloat(ui.toolTaperTip.value); - selectedTool.metric = ui.toolMetric.checked; - selectedTool.type = toolNames[ui.toolType.selectedIndex]; - if (selectedTool.type === 'tapermill') { - const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2; - if (ev && ev.target === ui.toolTaperAngle) { - const angle = parseFloat(ev.target.value); - const len = calcTaperLength(rad, angle * DEG2RAD); - selectedTool.flute_len = len; - ui.toolTaperAngle.value = angle.round(1); - ui.toolFluteLen.value = selectedTool.flute_len.round(4); - } else { - ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1); - } - } else { - ui.toolTaperAngle.value = 0; - } - renderTools(); - ui.toolSelect.selectedIndex = selectedTool.order; - setToolChanged(true); - renderTool(selectedTool); -} - function setToolChanged(changed) { editTools.changed = changed; api.ui.toolsSave.disabled = !changed; diff --git a/src/kiri/run/minion.js b/src/kiri/run/minion.js index 412da396..749d9462 100644 --- a/src/kiri/run/minion.js +++ b/src/kiri/run/minion.js @@ -12,7 +12,7 @@ import { doTopShells } from '../mode/fdm/post.js'; import { newPoint } from '../../geo/point.js'; import { polygons as POLY } from '../../geo/polygons.js'; import { sliceZ, sliceConnect } from '../../geo/slicer.js'; -import { Slicer as cam_slicer } from '../mode/cam/slicer.js'; +import { Slicer as cam_slicer } from '../mode/cam/slicer_cam.js'; import { Slicer as topo_slicer } from '../mode/cam/slicer_topo.js'; import { Probe, Trace, raster_slice } from '../mode/cam/topo3.js'; import { Topo as Topo4, rotatePoints } from '../mode/cam/topo4.js'; diff --git a/web/boot/index.html b/web/boot/index.html index 8f7c9381..cfee6307 100644 --- a/web/boot/index.html +++ b/web/boot/index.html @@ -43,6 +43,7 @@ } navigator.serviceWorker.controller.postMessage({ + mode: map.mode, clear: map.clear, disable: map.disable, version: map.version || version diff --git a/web/boot/service.js b/web/boot/service.js index c9e7a55c..3de012a6 100644 --- a/web/boot/service.js +++ b/web/boot/service.js @@ -142,9 +142,9 @@ async function _fetch(e) { } if (url.pathname.endsWith("/")) { - return e.respondWith(redirectOr404(appendURL(url, 'index.html').pathname)); + return e.respondWith(redirectToUrl(appendURL(url, 'index.html').pathname, request)); } else if (url.pathname.indexOf(".") < 0 || url.pathname.endsWith("/boot")) { - return e.respondWith(redirectOr404(appendURL(url, '/index.html').pathname)); + return e.respondWith(redirectToUrl(appendURL(url, '/index.html').pathname, request)); } else { e.respondWith(fromCacheOrNetwork(request)); } @@ -155,16 +155,9 @@ function appendURL(url, append) { return new URL(url.origin + url.pathname + append + url.search); } -async function redirectOr404(path) { - const cache = await cacheOpen; - const hit = await cache.match(path, { ignoreSearch: true }); - if (hit) { - if (debug) log('REDIRECT', path); - return Response.redirect(path, 302); - } else { - if (debug) log('404', path); - return new Response('Not Found', { status: 404, statusText: 'Not Found' }); - } +async function redirectToUrl(path, request) { + if (debug) log('REDIRECT', path); + return Response.redirect(path, 302); } async function fromCacheOrNetwork(req) { diff --git a/web/kiri/index.css b/web/kiri/index.css index 390e24ea..df43cb9b 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -189,7 +189,9 @@ th, tr, td, label { white-space: nowrap; } details summary { + display: flex; list-style: none; + padding-left: 0 !important; } details summary::-webkit-details-marker { display: none; @@ -438,6 +440,9 @@ details[open] summary::after { .dark .widopt { background-color: #222; } +.dark #camops summary { + background-color: var(--blue-0); +} /* top menu bar, drop menus */ #top, #app-name { @@ -1400,6 +1405,9 @@ details[open] summary::after { #camops .set-sep { margin-bottom: 4px; } +#camops summary { + background-color: var(--blue-4); +} #oplist .clock:hover { background-color: var(--blue-2); } @@ -2425,14 +2433,14 @@ details[open] summary::after { } .var-row input { /* flex: 1 1 auto; */ - min-width: 0; - max-width: 7ch; + min-width: 0; + max-width: 7ch; margin-right: 0; margin-bottom: 1px; padding-bottom: 1px; padding-top: 1px; } -.var-row #tool-name{ +.var-row #tool-name { max-width: 15ch; } .var-row button { diff --git a/web/kiri/index.html b/web/kiri/index.html index 6d45ea5e..5de0fcfb 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -452,16 +452,16 @@
laser off
flip
register
-
drill
-
level
-
trace
+
drill
+
level
+
helical
gcode
-
rough
-
outline
contour
-
pocket
-
helical
-
area
+
trace
+
pocket
+
rough
+
outline
+
area
@@ -572,34 +572,13 @@
- +
-
-
-
- -
-
-
-
shaft
-
-
-
flute
-
-
-
taper
-
-
-
+
diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index 4ac9afa8..f94002be 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -513,10 +513,10 @@ self.lang['en-us'] = { // CNC COMMON cc_menu: "limits", - cc_rapd_s: "xy feed", - cc_rapd_l: ["max xy moves feedrate","in workspace units / minute"], - cc_rzpd_s: "z feed", - cc_rzpd_l: ["max z moves feedrate","in workspace units / minute"], + cc_rapd_s: "feed rate", + cc_rapd_l: ["max xy cutting feedrate","in workspace units / minute"], + cc_rzpd_s: "plunge rate", + cc_rzpd_l: ["max z cutting plunge rate","in workspace units / minute"], // CNC LEVELING cc_loff_s: "z offset", @@ -538,10 +538,6 @@ self.lang['en-us'] = { cr_clst_l: ["in indexed mode, clear entire stock area, not just to part periemter"], cr_clrt_s: "clear top", cr_clrt_l: ["run a clearing pass over","the bounding area of the part","at z = 0"], - cr_clrp_s: "clear voids", - cr_clrp_l: ["mill out through pockets","instead of just the outline"], - cr_clrf_s: "clear faces", - cr_clrf_l: ["interpolate step down to","clear any detected flat areas"], cr_olin_s: "inside only", cr_olin_l: ["limit cutting to","inside part boundaries"], @@ -553,18 +549,10 @@ self.lang['en-us'] = { co_dogb_l: ["insert dogbone cuts","into inside corners"], co_dogr_s: "reverse bones", co_dogr_l: ["reverse dogbone direction"], - co_clrt_s: "clear top", - co_clrt_l: ["cut starting at the top of stock","when stock is enabled"], co_wide_s: "wide cutout", co_wide_l: ["widen outside cutout paths","for deep cuts in hard material"], - co_olin_s: "inside only", - co_olin_l: ["limit cutting to","inside part boundaries"], - co_olot_s: "outside only", - co_olot_l: ["limit cutting to","exterior part boundaries","which can be thought of","as the shadow outline"], co_omit_s: "omit through", co_omit_l: "eliminate thru holes", - co_omvd_s: "omit pocket", - co_omvd_l: "eliminate interior pockets", co_olen_s: "enable", co_olen_l: "enabled outline cutting", @@ -601,8 +589,6 @@ self.lang['en-us'] = { cp_refi_l: ["number of refining passes to perform on contoured polylines. meant to address Z delta sawtoothing caused by faced geometries whereas smoothing handles XY. values > 10 work well for gently contoured geometries with significant Z movement."], cp_cont_s: "contour", cp_cont_l: ["ignore interior voids and features"], - cp_engr_s: "engrave", - cp_engr_l: ["sets up a single pass around the perimeter of the selected area. also useful for 3D laser marking"], cp_outl_s: "outline only", cp_outl_l: ["ignore interior voids and features"], @@ -828,6 +814,8 @@ self.lang['en-us'] = { ou_feng_l: "speed scale (0.1-1) when endmill is fully engaged on a new cut. stepped over cuts when the endmill is less than fully engaged run at a factor of 1 relative to output speed. typically this relates to roughing or any operation with step over.", ou_eang_s: "ease angle", ou_eang_l: "descent angle for ease down in degrees", + ou_dire_s: "milling", + ou_dire_l: ["milling direction","climb, conventional, alternating"], // CAM STOCK cs_menu: "stock",