diff --git a/bin/esbuild.config.mjs b/bin/esbuild.config.mjs index 4881ab00..0cb973bb 100644 --- a/bin/esbuild.config.mjs +++ b/bin/esbuild.config.mjs @@ -12,7 +12,9 @@ const MESH_OUTFILE = 'src/pack/mesh-main.js'; const MESH_EXTRAS = [ ]; const KIRI_OUTFILE = 'src/pack/kiri-main.js'; -const KIRI_EXTRAS = [ ]; +const KIRI_EXTRAS = [ + 'src/ext/base64.js' +]; async function appendExtraModules(extras, outfile, minify = false) { const files = await glob(extras); diff --git a/notes.md b/notes.md index a7036e77..5efbac1a 100644 --- a/notes.md +++ b/notes.md @@ -31,6 +31,10 @@ * `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder purge blocks fail to generate properly for rafts +* `F` min layer time +* `F` separate bridging speed / fan control +* `F` elephant foot compensation +* `F` inner / outer perimeter compensation * `F` better thin-wall (like arachne) * `F` add scarf seams * `F` add 'random' and 'seam' start point options diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 38957f46..846515f9 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -258,9 +258,10 @@ export class Polygon { // of the polygon with the noodle removed (for the next pass) noodle(width) { let clone = this.clone(true); - let ins = clone.offset(width); + let ins = clone.offset(width) ?? []; + let remain = ins.clone(true); let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true)); - return { noodle: nood, remain: ins }; + return { noodle: nood, remain }; } // generate center crossing point cloud diff --git a/src/kiri/core/init.js b/src/kiri/core/init.js index 36a68da0..02dd0a55 100644 --- a/src/kiri/core/init.js +++ b/src/kiri/core/init.js @@ -1323,7 +1323,8 @@ function init_one() { yr = ((tl && yc) || (!tl && yt) ? ra : 1), zr = ((tl && zc) || (!tl && zt) ? ra : 1); // prevent null scale - if (xr * yr * zr === 0) { + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); return; } selection.scale(xr,yr,zr); @@ -1350,7 +1351,11 @@ function init_one() { xr = ((tl && xc) || (!tl && xt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1), zr = ((tl && zc) || (!tl && zt) ? ra : 1); - + // prevent null scale + if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) { + api.alerts.show('invalid scale value'); + return; + } selection.scale(xr,yr,zr); ui.scaleX.was = ui.scaleX.value = xv * xr; ui.scaleY.was = ui.scaleY.value = yv * yr; @@ -1362,6 +1367,7 @@ function init_one() { e.target.value = val; } + // on enter but not on blur space.event.onEnterKey([ ui.scaleX, selectionScale, ui.scaleY, selectionScale, @@ -1369,6 +1375,9 @@ function init_one() { ui.sizeX, selectionSize, ui.sizeY, selectionSize, ui.sizeZ, selectionSize, + ]); + // on enter and blur + space.event.onEnterKey([ ui.toolName, updateTool, ui.toolNum, updateTool, ui.toolFluteDiam, updateTool, diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index 848229f3..d0487b52 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -362,14 +362,11 @@ class Print { // if( axis == 'I' || axis == "J") return if (abs) { pos[axis] = val; - if (axis == "X") point.x = factor * pos.X + xoff.x + off.x else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz - - } else { - mov[axis] = val; + // mov[axis] = val; pos[axis] += val; } // console.log("position updated",structuredClone(pos)) @@ -393,7 +390,6 @@ class Print { center = center.add(prevPoint); center.setZ((prevPoint.z+point.z)/2+dz); } - return { center, point, @@ -402,10 +398,8 @@ class Print { } function outputPoint(point,lastP,emit,{center,arcPoints,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)) { newlayer = false; let dz = point.z - seq.z; diff --git a/src/kiri/mode/cam/cl-tab.js b/src/kiri/mode/cam/cl-tab.js index 8237065e..0cda4f12 100644 --- a/src/kiri/mode/cam/cl-tab.js +++ b/src/kiri/mode/cam/cl-tab.js @@ -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 diff --git a/src/kiri/mode/cam/op-outline.js b/src/kiri/mode/cam/op-outline.js index 270f4c64..c9b7e275 100644 --- a/src/kiri/mode/cam/op-outline.js +++ b/src/kiri/mode/cam/op-outline.js @@ -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); diff --git a/src/kiri/mode/cam/op-rough.js b/src/kiri/mode/cam/op-rough.js index 341f56d8..beb24827 100644 --- a/src/kiri/mode/cam/op-rough.js +++ b/src/kiri/mode/cam/op-rough.js @@ -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 ? @@ -258,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; diff --git a/src/kiri/mode/cam/op-trace.js b/src/kiri/mode/cam/op-trace.js index 7b759316..52481169 100644 --- a/src/kiri/mode/cam/op-trace.js +++ b/src/kiri/mode/cam/op-trace.js @@ -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() diff --git a/src/kiri/mode/cam/tools.js b/src/kiri/mode/cam/tools.js index 9552fccb..8c6b43d2 100644 --- a/src/kiri/mode/cam/tools.js +++ b/src/kiri/mode/cam/tools.js @@ -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; diff --git a/src/kiri/mode/fdm/driver-be.js b/src/kiri/mode/fdm/driver-be.js index 397670e2..f1164acc 100644 --- a/src/kiri/mode/fdm/driver-be.js +++ b/src/kiri/mode/fdm/driver-be.js @@ -1,7 +1,7 @@ /** Copyright Stewart Allen -- 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 }; diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index bbc088fc..a2222402 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -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 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 { client.clear(); client.sync([this.widget]);