add optional alternating/zigzag for area linear surfacing

This commit is contained in:
Stewart Allen 2025-12-27 10:28:07 -05:00
commit c46f88b412
5 changed files with 66 additions and 20 deletions

View file

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

View file

@ -446,6 +446,7 @@ export const conf = {
camAreaDogbones: false,
camAreaRevbones: false,
camAreaOutline: false,
camAreaZigZag: true,
camContourAngle: 85,
camContourBottom: false,
camContourBridge: 0,

View file

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

View file

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

View file

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