diff --git a/src/geo/paths.js b/src/geo/paths.js index a6f3822f..1e72369b 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -2,6 +2,7 @@ import { base } from './base.js'; import { newPoint } from './point.js'; +import { newPolygon } from './polygon.js'; /** * Emit each element in array based on next closest endpoint @@ -18,23 +19,26 @@ export function tip2tipEmit(array, startPoint, emitter) { for (;;) { found = null; mindist = Infinity; - array.forEach(function (el) { - if (el.delete) return; + for (let el of array) { + if (el.delete) continue; dist = startPoint.distTo2D(el.first); if (dist < mindist) { found = { el: el, first: el.first, last: el.last }; mindist = dist; } + // when heads is set, it's first to first + // not first/last to closest first/last + if (el.heads) continue; dist = startPoint.distTo2D(el.last); if (dist < mindist) { found = { el: el, first: el.last, last: el.first }; mindist = dist; } - }); + } if (found) { found.el.delete = true; startPoint = found.last; - emitter(found.el, found.first, ++count); + emitter(found.el, found.first, ++count, mindist); } else { break; } @@ -42,6 +46,38 @@ export function tip2tipEmit(array, startPoint, emitter) { return startPoint; } +/** + * Emit each element in array based on next closest endpoint + * Uses greedy nearest-neighbor algorithm to minimize travel distance + * When polygons are close, a new polygon is injected that joins them + * and the next polygon is reversed so that tips would merge on output + * @param {Array} paths - Array of elements with { first, last } Point properties + * @param {Point} startPoint - Starting point for path ordering + * @param {Number} mergeDist - threshold for merging Polygons + * @returns {Array} reduced merged Polygon array + */ +export function tip2tipJoin(paths, startPoint, mergeDist) { + let newp = []; + tip2tipEmit(paths.map(path => ({ + path, + last: path.last(), + first: path.first() + })), startPoint, (next, point, count, dist) => { + if (next.last === point) { + next.path.reverse(); + } + if (newp.length && dist < mergeDist) { + let lastpath = newp.peek(); + newp.push(newPolygon().addPoints([ + lastpath.last(), + next.path.first() + ])); + } + newp.push(next.path); + }); + return newp; +} + /** * Emit polygons in optimal order based on nearest point * Like tip2tipEmit but accepts polygons where next closest point can be anywhere diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index b8f7f438..a829966e 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -446,6 +446,7 @@ export const conf = { camAreaDogbones: false, camAreaRevbones: false, camAreaOutline: false, + camAreaZigZag: true, camContourAngle: 85, camContourBottom: false, camContourBridge: 0, diff --git a/src/kiri/mode/cam/cl-ops.js b/src/kiri/mode/cam/cl-ops.js index df9c612b..fc03e33f 100644 --- a/src/kiri/mode/cam/cl-ops.js +++ b/src/kiri/mode/cam/cl-ops.js @@ -678,6 +678,7 @@ export function createPopOps() { tr_type: 'camAreaTrace', sr_type: 'camAreaSurface', sr_angle: 'camAreaAngle', + sr_alter: 'camAreaZigZag', over: 'camAreaOver', down: 'camAreaDown', rate: 'camAreaSpeed', @@ -713,6 +714,7 @@ export function createPopOps() { 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() }), refine: UC.newInput(LANG.cp_refi_s, { title: LANG.cp_refi_l, convert: toInt, show: isSurface }), + sr_alter: UC.newBoolean("alternate", undefined, { title: "alternate cutting direction", show: isSurfaceLinear }), 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(), diff --git a/src/kiri/mode/cam/op-area.js b/src/kiri/mode/cam/op-area.js index 35c518e4..7764f07d 100644 --- a/src/kiri/mode/cam/op-area.js +++ b/src/kiri/mode/cam/op-area.js @@ -10,6 +10,7 @@ import { newPoint } from '../../../geo/point.js'; import { newPolygon } from '../../../geo/polygon.js'; import { polygons as POLY } from '../../../geo/polygons.js'; import { util as base_util } from '../../../geo/base.js'; +import { tip2tipJoin } from '../../../geo/paths.js'; import { CAM } from './driver-be.js'; const DEG2RAD = Math.PI / 180; @@ -41,6 +42,11 @@ class OpArea extends CamOp { let thruHoles = state.shadow.holes; let roundSharps = settings.process.camRoundCorners; + // get these once and re-use for each area + let vertices = mode === 'surface' ? + widget.getGeoVertices({ unroll: true, translate: true }) : + undefined; + // also updates tab offsets setToolDiam(toolDiam); @@ -302,7 +308,7 @@ class OpArea extends CamOp { progress(proc, 'trace'); } else if (mode === 'surface') { - let { sr_type, sr_angle, tolerance } = op; + let { sr_type, sr_angle, sr_alter, tolerance } = op; let resolution = tolerance || 0.05; let raster = await self.get_raster_gpu({ mode: "tracing", resolution }); @@ -327,8 +333,14 @@ class OpArea extends CamOp { paths.push(POLY.fromClipperNode(node, 0)); } } - // convert resulting poly lines to raster float32 array groups - paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + // in surfacing mode, direction is simply reversal + if (direction === 'climb') { + paths.forEach(path => path.reverse()); + } + // optional alternating paths + if (sr_alter) { + paths = tip2tipJoin(paths, paths[0].first(), toolDiam * 4); + } } else if (sr_type === 'offset') { // progressive inset from perimeter @@ -336,9 +348,12 @@ class OpArea extends CamOp { 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()); + POLY.setWinding(paths.filter(p => p.isClosed()), direction === 'climb'); } + // convert resulting poly lines to raster float32 array groups + paths = paths.map(poly => poly.points.map(p => [ p.x, p.y ]).flat().toFloat32()); + // prepare tool mesh points let toolBounds = new THREE.Box3() .expandByPoint({ x: -toolDiam/2, y: -toolDiam/2, z: 0 }) @@ -350,7 +365,6 @@ class OpArea extends CamOp { let toolData = { positions: toolPos, bounds: toolBounds }; // prepare terrain and raster paths over terrain - let vertices = widget.getGeoVertices({ unroll: true, translate: true }); let wbounds = bounds.clone().expandByVector({ x: toolDiam/2, y: toolDiam/2, z: 0 }); wbounds.min.z = zBottom; wbounds.max.z = zTop; @@ -408,17 +422,11 @@ class OpArea extends CamOp { 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) => { + for (let poly of surface) { setNextIsMove(); - if (next.last === point) next.el.reverse(); - printPoint = polyEmit(next.el); + printPoint = polyEmit(poly); newLayer(); - }); + } } setContouring(false); // skip areas when processing surfaces diff --git a/src/kiri/mode/cam/op-contour.js b/src/kiri/mode/cam/op-contour.js index baff1a70..b9116c86 100644 --- a/src/kiri/mode/cam/op-contour.js +++ b/src/kiri/mode/cam/op-contour.js @@ -122,10 +122,9 @@ class OpContour extends CamOp { let { process } = settings; let { polyEmit, setContouring, setTolerance, setTool } = ops; - let { widget, camOut, newLayer, zmax } = ops; + let { widget, newLayer, zmax } = ops; let bounds = widget.getBoundingBox(); - let toolDiam = this.toolDiam; let depthData = []; setTool(op.tool, op.rate, process.camFastFeedZ);