Merge branch 'rel-4.3' of https://github.com/GridSpace/grid-apps into drill-optimization

This commit is contained in:
bakedpotatolord 2025-09-21 17:43:01 -06:00
commit cd462709d7
37 changed files with 955 additions and 223 deletions

View file

@ -1,7 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { api } from '../../core/api.js';
import { env, opRender } from './client.js';
import { env, opAdd, opRender } from './client.js';
import { CAM } from './driver-fe.js';
import { clearTabs, restoreTabs } from './cl-tab.js';
import { updateStock } from './cl-stock.js';

View file

@ -61,7 +61,7 @@ export function createTabBox(iw, ic, n) {
const { camZBottom, camStockIndexed } = process;
const sz = stock.z || bounds.max.z;
const zto = sz - iw.track.top;
const zp = (env.camZBottom || camStockIndexed ? env.camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2);
const zp = (camZBottom || camStockIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2);
ic.x += n.x * camTabsDepth / 2; // offset from part
ic.z -= n.y * camTabsDepth / 2; // offset swap z,y
ic.y = zp; // offset swap in world space y,z

View file

@ -226,7 +226,7 @@ const traceClear = () => {
api.conf.save();
};
const opAdd = (rec) => {
export const opAdd = (rec) => {
if (!env.isCamMode) return;
clearPops();
let oplist = env.current.process.ops;

View file

@ -53,8 +53,6 @@ export function cam_export(print, online) {
lasering = false,
laserOp,
stock = settings.stock || {},
ztOff = stock.z - widget.track.top,
bounds = widget.getBoundingBox(),
zmax = stock.z,
runbox = {
max: { x: -Infinity, y: -Infinity, z: -Infinity },
@ -234,7 +232,7 @@ export function cam_export(print, online) {
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: pos.a },
emit: 1,
emit: out.emit,
}, {
dx: 1, dy: 1, dz: 0, time: 0
});
@ -251,7 +249,7 @@ export function cam_export(print, online) {
// speed: Infinity,
tool: out.tool,
point: { x: newpos.x, y: newpos.y, z: newpos.z, a: newpos.a },
emit: 1
emit: out.emit
}, {
dx: 0, dy: 0, dz: 1, time: 0
});
@ -274,6 +272,9 @@ export function cam_export(print, online) {
dist = Math.sqrt(dx * dx + dy * dy + dz * dz),
newFeed = feed && feed !== pos.f;
// catch NaN in debugging
// if (nl.length === 0 || nl[0] === undefined)
// console.log(out, nl.length, ...nl);
// drop dup points (all deltas are 0)
if (!arc && !(dx || dy || dz || da)) {
@ -408,7 +409,7 @@ export function cam_export(print, online) {
// collect tool info to add to header
let toolz = {}, ctool;
const isOffset = offset && (offset.x || offset.y);
const isOffset = offset && (offset.x || offset.y || offset.z);
// remap points as necessary for origins, offsets, inversions
print.output.forEach(layer => {
if (!Array.isArray(layer)) {
@ -427,8 +428,9 @@ export function cam_export(print, online) {
// ensure not point is modified twice
point.mod = 1;
if (isOffset) {
point.x += offset.x;
point.y += offset.y;
point.x += offset.x ?? 0;
point.y += offset.y ?? 0;
point.z -= offset.z ?? 0;
}
if (scale) {
point.x *= scale.x;

View file

@ -97,6 +97,7 @@ export function menu() {
camConventional: newBoolean(LANG.ou_conv_s, onBooleanClick, {title:LANG.ou_conv_l}),
camEaseDown: newBoolean(LANG.cr_ease_s, onBooleanClick, {title:LANG.cr_ease_l}),
camDepthFirst: newBoolean(LANG.ou_depf_s, onBooleanClick, {title:LANG.ou_depf_l}),
camInnerFirst: newBoolean(LANG.ou_inrf_s, onBooleanClick, {title:LANG.ou_inrf_l}),
camToolInit: newBoolean(LANG.ou_toin_s, onBooleanClick, {title:LANG.ou_toin_l}),
separator: newBlank({ class:"set-sep", driven }),
camFirstZMax: newBoolean(LANG.ou_z1st_s, onBooleanClick, {title:LANG.ou_z1st_l}),

View file

@ -100,6 +100,11 @@ class OpOutline extends CamOp {
// 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);
@ -225,7 +230,7 @@ class OpOutline extends CamOp {
let easeDown = process.camEaseDown;
let toolDiam = this.toolDiam;
let cutdir = op.ov_conv;//process.camConventional;
let cutdir = op.ov_conv;
let depthFirst = process.camDepthFirst;
let depthData = [];
@ -251,9 +256,9 @@ class OpOutline extends CamOp {
});
// set cut direction on outer polys
POLY.setWinding(t, cutdir);
POLY.setWinding(t, !cutdir);
// set cut direction on inner polys
POLY.setWinding(c, !cutdir);
POLY.setWinding(c, cutdir);
if (depthFirst) {
depthData.push(polys);
@ -269,9 +274,11 @@ class OpOutline extends CamOp {
let polyLast;
// console.log({order, orderPolys});
printPoint = poly2polyEmit(orderPolys, printPoint, function(poly, index, count) {
polyLast = polyEmit(poly, index, count, polyLast);
}, { swapdir: false });
}, {
swapdir: false,
weight: process.camInnerFirst
});
})
newLayer();
}

View file

@ -5,7 +5,7 @@ import { Tool } from './tool.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlice } from '../../core/slice.js';
import { newPoint } from '../../../geo/point.js';
import { util as base_util } from '../../../geo/base.js';
import { util } from '../../../geo/base.js';
class OpRegister extends CamOp {
constructor(state, op) {
@ -88,7 +88,7 @@ class OpRegister extends CamOp {
step = -step;
}
}
for (let z of base.util.lerp(startZ, endZ, op.down)) {
for (let z of util.lerp(startZ, endZ, op.down)) {
let slice = newSlice(z);
addSlices(slice);
sliceOut.push(slice);

View file

@ -32,11 +32,12 @@ class OpRough extends CamOp {
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);
// clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part) {
if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone();
let step = toolDiam * op.step;
let inset = roughStock ?
@ -55,9 +56,16 @@ class OpRough extends CamOp {
for (let z = zstart; zsteps > 0; zsteps--) {
let slice = newSlice();
slice.z = z;
slice.camLines = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
let polys = POLY.setZ(facing.clone(true), slice.z + roughLeaveZ);
if (tabs) {
tabs.forEach(tab => {
tab.off = POLY.expand([tab.poly], toolDiam / 2).flat();
});
polys = cutTabs(tabs, polys, slice.z);
}
slice.camLines = polys;
slice.output()
.setLayer("face", {face: color, line: color})
.setLayer("roughing", {face: color, line: color})
.addPolys(slice.camLines);
addSlices(slice);
camFaces.push(slice);
@ -251,6 +259,12 @@ class OpRough extends CamOp {
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;
@ -302,7 +316,9 @@ class OpRough extends CamOp {
// set winding specified in output
POLY.setWinding(level, cutdir, false);
poly2polyEmit(level, printPoint, (poly, index, count) => {
printPoint = polyEmit(poly, index, count, printPoint);
printPoint = polyEmit(poly, index, count, printPoint, {cutFromLast: true});
}, {
weight: process.camInnerFirst
});
newLayer();
}

View file

@ -38,7 +38,7 @@ class OpTrace extends CamOp {
let cutdir = ov_conv;
let polys = [];
let reContour = false;
let canRecontour = offset !== 'none' && down === 0;
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);
@ -57,6 +57,7 @@ class OpTrace extends CamOp {
let max = Math.max(...zs);
if (max - min > 0.0001 && canRecontour) {
reContour = true;
down = 0;
}
}
if (false) newSliceOut(0).output()
@ -89,6 +90,7 @@ class OpTrace extends CamOp {
if (reContour) {
state.contourPolys(widget, slice.camLines);
}
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
slice.output()
.setLayer("trace follow", {line: color}, false)
.addPolys(slice.camLines)
@ -144,7 +146,7 @@ class OpTrace extends CamOp {
} else {
slice.camLines = POLY.flatten(slice.camLines, null, true);
}
POLY.setWinding(slice.camLines, cutdir, false);
POLY.setWinding(slice.camLines, offset === "outside" ? !cutdir : cutdir, false);
if (debug && shadow) slice.output()
.setLayer("trace shadow", {line: 0xff8811}, false)
.addPolys(shadow)

View file

@ -85,6 +85,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
easeDown = process.camEaseDown,
easeAngle = process.camEaseAngle,
depthFirst = process.camDepthFirst,
innerFirst = process.camInnerFirst,
engageFactor = process.camFullEngage,
arcTolerance = process.camArcTolerance,
arcRes = toRadians(process.camArcResolution),
@ -243,7 +244,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
break;
}
}
camOut(point.clone().setZ(zmax_outer));
camOut(point.clone().setZ(zmax_outer), 0);
points.forEach(function (point, index) {
camOut(point, 1);
if (index > 0 && index < points.length - 1) {
@ -331,7 +332,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* @param {number} opts.moveLen typically = tool diameter used to trigger terrain detection
* @param {number} opts.factor speed scale factor
*/
function camOut(point, emit, opts) {
function camOut(point, emit = 1, opts) {
// console.trace({point, emit, opts})
let {
center = {},
@ -392,8 +393,18 @@ export function prepEach(widget, settings, print, firstPoint, update) {
let deltaXY = lastPoint.distTo2D(point),
deltaZ = point.z - lastPoint.z,
absDeltaZ = Math.abs(deltaZ),
hasDelta = deltaXY >= 0.001 && absDeltaZ >= 0,
isMove = emit == 0;
// when rapid pluge could cut thru stock, rapid to just above stock
// then continue plunge as a plunge cut
if (deltaZ < 0 && lastPoint.z > stockz && point.z < stockz && emit === 0) {
// console.log('detected plunge cut as rapid move', lastPoint.z, stockz, point.z);
layerPush(point.clone().setZ(stockz + 1), 0, 0, tool);
// change to cutting move for remainder of plunge
emit = 1;
}
// drop points too close together
if (!isLathe && !isArc && deltaXY < 0.001 && point.z === lastPoint.z) {
// console.trace(["drop dup",lastPoint,point]);
@ -401,7 +412,7 @@ export function prepEach(widget, settings, print, firstPoint, update) {
}
// convert short planar moves to cuts in some cases
if (!isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
if (hasDelta && !isRough && !isArc && isMove && deltaXY <= moveLen && deltaZ <= 0 && !lasering) {
let iscontour = tolerance > 0;
let isflat = absDeltaZ < 0.001;
// restrict this to contouring
@ -455,6 +466,11 @@ export function prepEach(widget, settings, print, firstPoint, update) {
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) {
lastPoint = layerPush(point.clone().setZ(stockz + 1), 0, 0, tool);
emit = 1;
}
}
} else if (isRough && deltaZ < 0) {
layerPush(point.clone().setZ(lastPoint.z), 0, 0, tool);
@ -717,9 +733,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
function emitTrace(slice) {
let { tool, rate, plunge } = slice.camTrace;
setTool(tool, rate, plunge);
let traceTool = new Tool(settings, tool);
let traceToolDiam = traceTool.fluteDiameter();
printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, { swapdir: false });
printPoint = poly2polyEmit(slice.camLines, printPoint, polyEmit, {
swapdir: false,
weight: innerFirst
});
newLayer();
}
@ -736,10 +753,15 @@ export function prepEach(widget, settings, print, firstPoint, update) {
* @param {number} index - unused
* @param {number} count - 1 to set engage factor
* @param {Point} fromPoint - the point to rapid move from
* @param {Object} camOutOpts - optional parameters to pass to camOut
* @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
*/
function polyEmit(poly, index, count, fromPoint) {
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
@ -775,7 +797,8 @@ export function prepEach(widget, settings, print, firstPoint, update) {
// if(offset == 0) console.log("forEachPoint",point,pidx,points)
// console.log({pointA, pointB, indexA, indexB,startIndex})
if (indexA == startIndex) {
camOut(pointA.clone(), 0, { factor: engageFactor });
//if cutFromLast is true, emit a 1 for a cutting move
camOut(pointA.clone(), cutFromLast ? 1 : 0, { factor: engageFactor });
// if first point, move to and call export function
if (arcEnabled) arcQ.push(pointA);
}
@ -845,7 +868,6 @@ export function prepEach(widget, settings, print, firstPoint, update) {
radFault = Math.abs(angle) > arcRes
if (radFault) {
// if so, remove first point
console.log("secondary radfault,",structuredClone(arcQ),{angle,arcRes,a,b})
camOut(arcQ.shift(), 1)
}
@ -1062,7 +1084,10 @@ export function prepEach(widget, settings, print, firstPoint, update) {
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, dir, ease);
fromPoint = depthOutlinePath(fromPoint, depth + 1, levels, radius, emitter, !dir, ease);
return fromPoint;
}, { weight: false, swapdir: false });
}, {
weight: innerFirst,
swapdir: false
});
return start;
}
@ -1098,10 +1123,10 @@ function getZClearPath(terrain, x1, y1, x2, y2, z, zadd, off, over) {
}
}
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 p1 = newPoint(x1, y1);
let p2 = newPoint(x2, y2);
let int = data.tops.map(p => p.intersections(p1, p2, true)).flat();
if (int.length) {
maxz = Math.max(maxz, data.z + zadd + over);

View file

@ -4,6 +4,7 @@ import { base, util } from '../../../geo/base.js';
import { newLine } from '../../../geo/line.js';
import { newPoint } from '../../../geo/point.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newSlopeFromAngle } from '../../../geo/slope.js';
import { polygons as POLY } from '../../../geo/polygons.js';
import { setSliceTracker } from '../../core/slice.js';
import { ops as OPS } from './ops.js';
@ -86,6 +87,8 @@ export async function cam_slice(settings, widget, onupdate, ondone) {
bottom_z, bottom_cut
}, 3);
// console.log({ track, bounds, stock, workarea });
return structuredClone(workarea);
};
@ -383,11 +386,11 @@ export function addDogbones(poly, dist, reverse) {
let bdiff = ((adiff < 0 ? (180 - adiff) : (180 + adiff)) / 2) + 180;
if (!open || (i > 1 && i < length)) {
if (isCW && adiff > 45) {
let newa = base.newSlopeFromAngle(lastsl.angle + bdiff);
let newa = newSlopeFromAngle(lastsl.angle + bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
} else if (!isCW && adiff < -45) {
let newa = base.newSlopeFromAngle(lastsl.angle - bdiff);
let newa = newSlopeFromAngle(lastsl.angle - bdiff);
newpts.push(lastpt.projectOnSlope(newa, dist));
newpts.push(lastpt.clone());
}
@ -657,9 +660,9 @@ export async function holes(settings, widget, individual, rec, onProgress) {
// keeps the z bottom relative to the part when z align changes
zBottom = isIndexed ? camZBottom : camZBottom - zbOff;
let slicerOpts = { flatoff: 0.001 }
let slicerOpts = { flatoff: 0.001 };
let slicer = new cam_slicer(widget, slicerOpts);
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat()
let zFlats = Object.keys(slicer.zFlat).map(Number).map(z => [z, z - 0.002]).flat();
precision = Math.max(0, precision)
let intervals = (precision == 0) ? [] : slicer.interval(
@ -669,11 +672,11 @@ export async function holes(settings, widget, individual, rec, onProgress) {
}
)
let zees = [...zFlats, ...intervals]
let zees = [...zFlats, ...intervals];
let indices = [...new Set(zees
.map(kv => parseFloat(kv).round(5))
.filter(z => z !== null)
)]
)];
let centerDiff = diam * 0.1,
area = (diam / 2) * (diam / 2) * Math.PI,
circles = [],
@ -689,19 +692,20 @@ 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 = computeShadowAt(widget, slice.z, 0);
let inner = top.inner;
if (!inner) { //no holes
continue;
}
for (let poly of inner) {
if (poly.points.length < 7) continue;
let center = poly.calcCircleCenter();
center.area = poly.area();
center.overlapping = [center]
center.overlapping = [center];
center.depth = 0;
// console.log("center",center)
if (poly.circularity() < 0.985) {
if (poly.circularity() < 0.98) {
// if not circular, don't add to holes
continue;
}
@ -712,7 +716,7 @@ export async function holes(settings, widget, individual, rec, onProgress) {
}
let overlap = false;
for (let [i, circle] of circles.entries()) {
let dist = circle.distTo2D(center)
let dist = circle.distTo2D(center);
// //if on the same xy point,
if (dist <= centerDiff) {
@ -726,19 +730,19 @@ export async function holes(settings, widget, individual, rec, onProgress) {
if (!overlap) circles.push(center);
}
}
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles")
onProgress(0.5 + (i / slices.length * 0.25), "recognize circles");
}
let drills = []
let drills = [];
for (let [i, c] of circles.entries()) {
let overlapping = c.overlapping
let overlapping = c.overlapping;
let last = overlapping.shift();
while (overlapping.length) {
let circ = overlapping.shift();
let aveArea = (circ.area + last.area) / 2;
let areaDelta = Math.abs(circ.area - last.area)
let areaDelta = Math.abs(circ.area - last.area);
if (areaDelta < aveArea * 0.05) { // if area delta less than 5% of average area
//keep top circle selected
last.depth = last.z - circ.z;
@ -748,24 +752,25 @@ export async function holes(settings, widget, individual, rec, onProgress) {
last = circ;
}
}
if (last.depth != 0) drills.push(last) //add last circle
onProgress(0.75 + (i / circles.length * 0.25), "assemble holes")
if (last.depth != 0) drills.push(last); // add last circle
onProgress(0.75 + (i / circles.length * 0.25), "assemble holes");
}
drills.forEach(h => {
if (rec.fromTop) {
// set z top if selected
h.depth += wztop - h.z
h.z = wztop
h.depth += wztop - h.z;
h.z = wztop;
}
delete h.overlapping //for encoding
h.diam = toolDiam // for mesh generation
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); //for same size selection
delete h.overlapping; //for encoding
h.diam = toolDiam; // for mesh generation
h.selected = (!individual && Math.abs(h.area - area) <= area * 0.05); // for same size selection
})
// console.log("unfiltered circles",circles)
// console.log("drills",drills)
drills = drills.filter(drill => drill.depth > 0)
widget.shadowedDrills = shadowedDrills
console.log("unfiltered circles",circles);
console.log("drills",drills);
drills = drills.filter(drill => drill.depth > 0);
widget.shadowedDrills = shadowedDrills;
widget.drills = drills;
return drills;
}

View file

@ -4,6 +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';
const DEG2RAD = Math.PI / 180;
@ -44,7 +45,7 @@ function selectTool(tool) {
ui.toolMetric.checked = tool.metric;
ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
if (tool === 'tapermill') {
ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle(
ui.toolTaperAngle.value = calcTaperAngle(
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
).round(1);
} else if(tool === 'drill'){
@ -208,16 +209,15 @@ export function updateTool(ev) {
selectedTool.metric = ui.toolMetric.checked;
selectedTool.type = toolNames[ui.toolType.selectedIndex];
if (selectedTool.type === 'tapermill') {
const CAM = kiri.driver.CAM;
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value);
const len = CAM.calcTaperLength(rad, angle * DEG2RAD);
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 = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1);
ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1);
}
} else {
ui.toolTaperAngle.value = 0;
@ -318,7 +318,7 @@ function _showTools() {
return;
}
const record = {
version: kiri.version,
version: api.version,
tools: api.conf.get().tools,
time: Date.now()
};

View file

@ -1,7 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js';
import { fdm_slice } from './slice.js';
import { fdm_slice, sliceAll } from './slice.js';
import { fdm_prepare } from './prepare.js';
import { fdm_export } from './export.js';
@ -39,6 +39,7 @@ export const FDM = {
extrudePerMM,
extrudeMM,
slice: fdm_slice,
sliceAll: sliceAll,
prepare: fdm_prepare,
export: fdm_export
};

View file

@ -2,7 +2,7 @@
import '../../../ext/jspoly.js';
import { base } from '../../../geo/base.js';
import { newPolygon } from '../../../geo/polygon.js';
import { newPolygon, Polygon } from '../../../geo/polygon.js';
import { polygons as POLY, fillArea } from '../../../geo/polygons.js';
import { getRangeParameters } from './driver.js';
import { slicer } from '../../../geo/slicer.js';
@ -163,25 +163,47 @@ function thin_type_2(params) {
}
function thin_type_3(params) {
// offsetN usually = extrusion width (nozzle diameter)
let { z, top, count, offsetN, last, gaps } = params;
top.thin_fill = [];
top.fill_sparse = [];
// produce trace from outside of poly inward no more than max inset
let { noodle, remain } = top.poly.noodle(offsetN * count);
top.shells = noodle;
// top.shells = noodle;
top.gaps = last = remain;
let thin = top.thin_fill;
let scale = 5000;
let thin = top.thin_fill = [];
let sparse = top.fill_sparse = [];
let scale = 1000;
let minR = offsetN / 2;
let maxR = minR * 1.5;
let minSpur = 0; // offsetN * 3;
let minSpur = offsetN * 2;
let showNoodle = false;
let mergeChains = true;
let interpolateShortSpur = true;
let showChainRawPoints = false;
let showChainInterpPoints = true;
let showMidline = true;
let showMidPoints = false;
let showMidNormals = false;
let showMidRadii = true;
let showNexuses = false;
let pointMap = new Map();
let lineMap = new Map();
function pointDist(p0, p1) {
return Math.hypot(p0.x-p1.x, p0.y-p1.y);
}
function point2key(point) {
return `${(point.x*100)|0}|${(point.y*100)|0}`;
}
// convert point to deduplicated / unique point record
function pointToRec(point) {
let key = `${(point.x*100)|0}|${(point.y*100)|0}`;
let key = point2key(point);
let rec = pointMap.get(key);
if (!rec) {
rec = {
@ -196,6 +218,7 @@ function thin_type_3(params) {
return rec;
}
// convert line to unique line record. matches reversed and duplicate lines
function pointsToLine(p0, p1) {
p0 = pointToRec(p0);
p1 = pointToRec(p1);
@ -219,6 +242,9 @@ function thin_type_3(params) {
let chains = [];
// for each standalone noodle, construct medial axis
// map medial axis points into de-duplicated segments
// which will be re-connected into chains
for (let poly of noodle) {
// scale the polygon (including inners) so the medial axis code works properly
poly = poly.clone(true).scale({ x:scale, y:scale, z:scale });
@ -231,69 +257,84 @@ function thin_type_3(params) {
for (let { point0, point1 } of ma) {
pointsToLine(point0, point1);
}
for (let seg of lineMap.values()) {
let { p0, p1 } = seg;
let len = seg.len = Math.hypot(p0.x-p1.x, p0.y-p1.y);
// filter out lines where both points' radii are under the
// minR threshold. shorten and fixup lines where minR falls
// somewhere along the gradient between ends.
if (p0.r < minR || p1.r < minR) {
if (p0.r > minR && len > minSpur) {
// move p1 toward p0 until minR met
let pct = (minR - p1.r) / (p0.r - p1.r);
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
p1.x -= dx * pct;
p1.y -= dy * pct;
p1.r = minR;
pointMap.delete(p1.key);
} else if (p1.r > minR && len > minSpur) {
// move p0 toward p1 until minR met
let pct = (minR - p0.r) / (p1.r - p0.r);
let dx = p0.x - p1.x;
let dy = p0.y - p1.y;
p0.x -= dx * pct;
p0.y -= dy * pct;
p0.r = minR;
pointMap.delete(p0.key);
} else {
lineMap.delete(seg.key);
continue;
}
}
// connect segments into chains
for (let seg of lineMap.values()) {
let { p0, p1 } = seg;
let len = seg.len = pointDist(p0, p1);
// filter out lines where both points' radii are under the
// minR threshold. shorten and fixup lines where minR falls
// somewhere along the gradient between ends.
if (interpolateShortSpur)
if (p0.r < minR || p1.r < minR) {
if (p0.r > minR && len > minSpur) {
// move p1 toward p0 until minR met
let pct = (minR - p1.r) / (p0.r - p1.r);
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
p1.x -= dx * pct;
p1.y -= dy * pct;
p1.r = minR;
pointMap.delete(p1.key);
} else if (p1.r > minR && len > minSpur) {
// move p0 toward p1 until minR met
let pct = (minR - p0.r) / (p1.r - p0.r);
let dx = p0.x - p1.x;
let dy = p0.y - p1.y;
p0.x -= dx * pct;
p0.y -= dy * pct;
p0.r = minR;
pointMap.delete(p0.key);
} else {
// both points eliminated
pointMap.delete(p0.key);
pointMap.delete(p1.key);
lineMap.delete(seg.key);
continue;
}
// for each line segment either add it to a chain or start a new chain
let add;
for (let chain of chains) {
if (p0.count === 2 && p0.key === chain[0].key) {
chain.splice(0,0,{...p1, len});
} else if (p0.count === 2 && p0.key === chain.peek().key) {
chain.peek().len = len;
chain.push({...p1});
add = true;
} else if (p1.count === 2 && p1.key === chain[0].key) {
chain.splice(0,0,{...p0, len});
add = true;
} else if (p1.count === 2 && p1.key === chain.peek().key) {
chain.peek().len = len;
chain.push({...p0});
add = true;
}
if (add) {
chain.total += len;
break;
}
}
// for each line segment either add it to a chain or start a new chain
let add;
for (let chain of chains) {
if (p0.count === 2 && p0 === chain[0]) {
// prefix chain with p1
chain.splice(0,0,p1);
add = true;
} else if (p0.count === 2 && p0 === chain.peek()) {
// append chain with p1
chain.push(p1);
add = true;
} else if (p1.count === 2 && p1 === chain[0]) {
// prefix chain with p0
chain.splice(0,0,p0);
add = true;
} else if (p1.count === 2 && p1 === chain.peek()) {
// append chain with p0
chain.push(p0);
add = true;
}
if (!add) {
chains.push([ {...p0 ,len}, {...p1} ]);
chains.peek().total = len;
if (add) {
chain.total += len;
break;
}
}
if (!add) {
chains.push([ p0, p1 ]);
chains.peek().total = len;
}
}
let showMid = false;
let showCross = false;
let showRad = true;
// todo: merge chains at next of 3 or more preferencing
// merging longer chains. then shorten remaining nexus chain
// intersections by minR
// render inset "noodle"
if (showNoodle) {
top.shells.appendAll(noodle);
}
// divide a segment/redius into 1 or more equal subsegments
function div(cr) {
if (cr <= maxR) {
return [ cr ];
@ -311,71 +352,249 @@ function thin_type_3(params) {
}
}
// add chains to thinwall output for visualization
if (showRad) {
top.shells = [];
}
for (let chain of chains.filter(c => c.total >= minR)) {
// chain reduction by merging short segments
let end = chain[0];
let nuchain = [ end ];
for (let point of chain.slice(1)) {
if (point.len === undefined || point.len + end.len >= minR) {
nuchain.push(point);
end = point;
} else {
end.len += point.len;
// filter chains that are too short from consideration
chains = chains.filter(c => c.total >= minR);
chains.sort((c1,c2) => c2.total - c1.total);
function merge1chain() {
let clen = chains.length;
for (let i=0; i<clen; i++) {
let ci = chains[i];
if (!ci) continue;
for (let j=i+1; j<clen; j++) {
let cj = chains[j];
if (!cj) continue;
if (ci[0] === cj[0]) {
ci.reverse().appendAll(cj)
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci.peek() === cj.peek()) {
ci.appendAll(cj.reverse());
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci.peek() === cj[0]) {
ci.appendAll(cj);
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci[0] === cj.peek()) {
cj.appendAll(ci);
cj.total += ci.total;
ci.merged = true;
chains[i] = undefined;
return true;
}
}
}
chain = nuchain;
// emit chain with subdivision for long segments
for (let i=0; i<chain.length-1; i++) {
// medial axis segment
if (showMid) {
thin.push(new Point(chain[i].x, chain[i].y));
thin.push(new Point(chain[i+1].x, chain[i+1].y));
return false;
}
// map chains to nexus via endpoints
let nexus = {};
function addEp(point, chain) {
let key = point.key;
let rec = nexus[key];
if (!rec) rec = nexus[key] = { point, chains: [] };
if (rec.chains.indexOf(chain) < 0) rec.chains.push(chain);
}
// since chains are sorted longest to shortest,
// they will appear in the nexus record longest to shortest
for (let chain of chains) {
addEp(chain[0], chain);
addEp(chain.peek(), chain);
}
// connect chains end to end following nexus branches that result
// in the longest final merged chain (open or closed)
// map chain endpoints to nexus points
while (mergeChains && merge1chain()) ;
// filter out null chains that were merged into other chains
chains = chains.filter(chain => chain);
// filter dup points from connecting ends
chains = chains.map(chain => chain.filter((v, i) => i === 0 || v !== chain[i - 1]));
// mark closed chains
// todo use exact point match by eliminating slice above
// then later removing repeated points in the resulting chain from merges
for (let chain of chains) {
chain.closed = (chain[0] === chain.peek());
console.log(chain.closed ? 'closed' : 'open');
if (chain.closed) chain.pop();
}
// shorten chain length by minR distance by popping or moving points
function shorten(chain) {
for (let rem=0, i=chain.length-1; i>=1 && rem < minR; i--) {
let c1 = chain[i];
let c2 = chain[i-1];
let d = pointDist(c1,c2);
if (rem + d <= minR) {
chain.pop();
rem += d;
} else {
let diff = minR - rem;
// move c1 toward c2
let pct = diff / d;
let dx = c1.x - c2.x;
let dy = c1.y - c2.y;
chain[i] = {
x: c1.x - dx * pct,
y: c1.y - dy * pct,
r: c1.r
};
rem = 0;
break;
}
// compute medial axis segment cross section
}
}
// shorten chains terminating at nexus by midR
for (let rec of Object.values(nexus)) {
let { point } = rec;
rec.shorts = 0;
if (rec.chains.length < 3) continue;
for (let chain of chains.filter(c => !c.closed)) {
if (pointDist(chain[0], point) < minR) {
chain.reverse();
shorten(chain);
chain.reverse();
rec.shorts++;
} else if (chain.peek() === point) {
shorten(chain);
rec.shorts++;
}
}
}
if (showNexuses)
for (let rec of Object.values(nexus)) {
let { point } = rec;
top.shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts));
}
function renderPointNormal(pt, ndx, ndy) {
if (showMidNormals) {
let xo = ndx * pt.r;
let yo = ndy * pt.r;
thin.push(new Point(pt.x + yo, pt.y - xo));
thin.push(new Point(pt.x - yo, pt.y + xo));
}
if (showMidRadii) {
const Sx = pt.x + ndy * pt.r;
const Sy = pt.y - ndx * pt.r;
const circ = top.shells;
const pop = div(pt.r);
let acc = 0;
for (let r of pop) {
const t = acc + r;
const px = Sx + -ndy * t;
const py = Sy + ndx * t;
circ.push(newPolygon().centerCircle({x:px, y:py}, r, 10));
acc += r * 2;
}
}
}
// gather point offsets into shells
let zi = z;
for (let chain of chains) {
// zi += 0.1;
if (showChainRawPoints)
for (let pt of chain) {
top.shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10));
}
// draw medial axis chain
let { closed, length } = chain;
let term = closed ? length : length - 1;
let segs = [];
for (let i=0; i<term; i++) {
let p0 = chain[i];
let p1 = chain[i+1];
let p1 = chain[(i+1) % length];
let len = pointDist(p0, p1);
// medial axis segment
if (showMidline) {
thin.push(new Point(p0.x, p0.y, zi));
thin.push(new Point(p1.x, p1.y, zi));
}
// compute medial axis segment normal
let offs = len <= offsetN ? [ 0 ] : base.util.lerp(0, len, offsetN, true);
let step = 1 / offs.length;
let dr = (p1.r - p0.r);
let dx = (p1.x - p0.x);
let dy = (p1.y - p0.y);
let ndx = dx / p0.len;
let ndy = dy / p0.len;
let offs = p0.len <= offsetN ? [ 0.5 ] : base.util.lerp(0, p0.len, offsetN, true);
let indx = 0;
let step = 1 / offs.length;
// interpolate across the length of the chain segment
for (let off of offs) {
let inc = (indx++ * step);
let cr = p0.r + dr * inc;
let cx = p0.x + dx * inc;
let cy = p0.y + dy * inc;
let xo = ndx * cr;
let yo = ndy * cr;
if (showCross) {
thin.push(new Point(cx + yo, cy - xo));
thin.push(new Point(cx - yo, cy + xo));
let ndx = dx / len;
let ndy = dy / len;
segs.push({ p0, p1, offs, step, dr, dx, dy, ndx, ndy });
}
console.log({ segs });
// compute chain subdivisions
let slen = segs.length;
for (let si=0; si<slen; si++) {
let seg = segs[si];
let segp = segs[(si - 1 + slen) % slen];
let { p0, p1, offs, step, dr, dx, dy, ndx, ndy } = seg;
let first = si === 0;
let last = si === slen - 1;
let mid = !(first || last);
// for length 2, first and last segment are the same
if (showMidPoints) {
if (first) {
// todo: handle closed
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
}
if (showRad) {
let circ = top.shells;
let pop = div(cr);
let sx = cx + yo;
let sy = cy - xo;
let m = 1;
for (let r of pop) {
sx -= (r/cr) * yo * m;
sy += (r/cr) * xo * m;
circ.push(newPolygon().centerCircle({ x:sx, y:sy }, r, 10));
m = 2;
if (mid) {
// mid segments
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
}
if (last) {
// todo: handle closed
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
if (closed) {
console.log('closed');
} else {
top.shells.push(newPolygon().centerCircle(p1, 0.08, 10));
}
// console.log(pop);
}
}
{
// todo: handle closed
if (first) {
renderPointNormal(p0, ndx, ndy);
}
if (mid) {
renderPointNormal(p0, ((ndx + segp.ndx)/2), ((ndy + segp.ndy)/2));
}
if (last) {
renderPointNormal(p1, ndx, ndy);
}
}
// interpolate across the length of the chain segment
if (showChainInterpPoints)
for (let indx = 1; indx < offs.length; indx++) {
const inc = indx * step;
const cr = p0.r + dr * inc;
const cx = p0.x + dx * inc;
const cy = p0.y + dy * inc;
const xo = ndx * cr;
const yo = ndy * cr;
if (showMidPoints) {
top.shells.push(newPolygon().centerCircle({ x:cx, y:cy }, 0.05, 10));
}
renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy);
}
}
}
POLY.setZ([...thin, ...top.shells], z);
// POLY.setZ([...top.shells], z);
return { last, gaps };
}