diff --git a/notes.md b/notes.md index 5efbac1a..b7e7a9a1 100644 --- a/notes.md +++ b/notes.md @@ -31,27 +31,21 @@ * `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 +* `F` new parameter to cap output flow rate (on printer device) +* `F` new parameters for bridging speed / bridge fan control +* `F` new base paremeter for hole and perimeter compensation +* `F` new parameters for inner / outer perimeter compensation * `F` convert ranges to z offsets while continuing to show layer # * `F` support pillar top/bottom should conform to part * `F` more explicit line width control with ranges and min/max adaptive * `F` test outlining solid projected areas (internally) * `F` gradient infill https://www.youtube.com/watch?v=hq53gsYREHU&feature=emb_logo -* `F` feather sharp tips by reducing extrusion in area of overlap * `F` segment large flat areas on first layer to mitigate peeling * `F` option to support interior bridges when 0% infill * `F` calculate filament use per extruder per print * `F` expand internal supporting flats / solids before projection (threshold) -* `P` reduce fan speed, feed rate, and extrusion factor for bridges * `P` auto purge pillars when quick layers are detected for extra cooling -* `P` extruder + filament max flow rate cap in planner * `P` solid fill the tops of supports for down facing flats # FDM - BELT @@ -91,8 +85,8 @@ * `F` ease-in and ease-out especially on tab cut-out start/stop * `F` maintain several part orientations + op chains in a single profile +* `P` outer outside corners as arc moves * `P` improve parser - do not require spaced tokens and support implied G0 / G1 -* `P` outer corner arc moves * `P` log Z interpolation for contour XYZ moves * `P` option to start with the smallest poly by area on layer change * `P` redo all path route / planning in prepare to account for terrain before camOut @@ -106,11 +100,12 @@ * subfolder parameter for dropped files * auto-kick check box in preferences - # Mesh:Tool * add undo/redo * add TextGeometry +* https://threejs.org/docs/#examples/en/geometries/TextGeometry +* https://dustinpfister.github.io/2023/07/05/threejs-text-geometry/ * font to path with https://github.com/paulzi/svg-text-to-path * click to repair normals based on a known good (selected) face * send to Kiri:Moto workspace (or update model vertices in place) @@ -122,3 +117,7 @@ * fix mirror to work with groups (just models currently) * add analyze results dialog * remove/hide auto-repair function + +# Other + +* preferred icons https://icons.getbootstrap.com/ diff --git a/package.json b/package.json index 5e9bdcc9..82e76bb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grid-apps", - "version": "4.3.0", + "version": "4.3.1", "description": "grid.space 3d slicing & modeling tools", "author": "Stewart Allen ", "license": "MIT", diff --git a/src/geo/base.js b/src/geo/base.js index d7ea18bf..5406d5ab 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -91,11 +91,11 @@ function area2(p1, p2, p3) { } function isCloseTo(v1, v2, dist) { - return Math.abs(v1 - v2) <= (dist || base.config.precision_merge); + return Math.abs(v1 - v2) <= (dist ?? base.config.precision_merge); } -function inCloseRange(val, min, max) { - return (isCloseTo(val, min) || val >= min) && (isCloseTo(val, max) || val <= max); +function inCloseRange(val, min, max, precisiom) { + return (isCloseTo(val, min, precisiom) || val >= min) && (isCloseTo(val, max, precisiom) || val <= max); } /** @@ -232,7 +232,7 @@ function intersect(p1, p2, p3, p4, test, parallelok) { /** * used by {@link rayIntersect} and {@link Polygon.trace} */ -function intersectRayLine(ro, s1, p1, p2, infinite) { +function intersectRayLine(ro, s1, p1, p2) { let keys = base.key, p1x = ro.x, p1y = ro.y, @@ -259,11 +259,11 @@ function intersectRayLine(ro, s1, p1, p2, infinite) { a = n1 / d; b = n2 / d; - if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) { + if (inCloseRange(b, 0, 1, 0.00) && a >= 0) { let ip = newPoint( p1x + (a * s1x), p1y + (a * s1y), - p2.z || ro.z, + p2.z ?? ro.z, keys.NONE ); ip.dist = a; diff --git a/src/geo/point.js b/src/geo/point.js index a29b98c4..add35a8c 100644 --- a/src/geo/point.js +++ b/src/geo/point.js @@ -114,11 +114,16 @@ class Point { return this.key; } - clone() { + clone(keys) { let p = newPoint(this.x, this.y, this.z, this._key); if (this.a !== undefined) { p.a = this.a; } + if (keys) { + for (let key of keys) { + p[key] = this[key]; + } + } return p; } diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 846515f9..ddce7094 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -1104,6 +1104,18 @@ export class Polygon { return this.first().isEqual(this.last()); } + closeIf(dist = 1) { + let closeDist = this.first().distTo2D(this.last()); + if (closeDist < 0.001) { + this.points.pop(); + return this.setClosed(); + } else if (closeDist <= dist) { + return this.setClosed(); + } else { + return this.setOpen(); + } + } + fixClosed() { if (this.appearsClosed()) { this.points.pop(); @@ -1311,15 +1323,15 @@ export class Polygon { pp2.pos = ip2; } }); - list.sort(function(p1, p2) { - return util.distSq(lp1, p1) - util.distSq(lp1, p2); - }); if (deep && this.inner) { this.inner.forEach(p => { let ints = p.intersections(lp1, lp2); if (ints) list.appendAll(ints); }); } + list.sort(function(p1, p2) { + return util.distSq(lp1, p1) - util.distSq(lp1, p2); + }); return list; } @@ -1380,6 +1392,23 @@ export class Polygon { return poly; } + /** + * emit the shortest poly that connects two points on a closed CW poly + * because traveling around the 'end point' may yield a shorter route. + * if the poly is open, use emitSegment() + */ + emitShortestSegment(i1, i2) { + if (this.open) { + return this.emitSegment(i1, i2); + } + let start = i1.p2.pos, + end = i2.p1.pos, + points = this.points, + seg0 = new Polygon([ i1, ...points.slice(start, end + 1), i2 ]).setOpen(), + seg1 = new Polygon([ i2, ...points.slice(end +1 ), ...points.slice(0, start), i1 ]).setOpen(); + return seg0.perimeter() < seg1.perimeter() ? seg0 : seg1; + } + /** * @param {Polygon} poly * @param {number} [tolerance] @@ -2042,13 +2071,9 @@ export class Polygon { // use expand / deflate technique instead if (opt.pump) { - let p2 = POLY.offset([this], opt.pump, { - z - }); + let p2 = POLY.offset([this], opt.pump, { z }); if (p2) { - p2 = POLY.offset(p2, -opt.pump, { - z - }); + p2 = POLY.offset(p2, -opt.pump, { z }); return p2; } return null; @@ -2316,9 +2341,12 @@ export class Polygon { return obj; } - // split long straight lines into segments no longer than max - // and return a new polygon - segment(max = 1) { + // split long straight lines into segments no longer than `max` + // and return a new polygon. optionally `mark` points with their + // derived segment start point (hinting for thin walls), `wrap` + // the poly first point by appending to the end (adaptive walls), + // or stopping segmentation at `maxoff` + segment(max = 1, mark = false, wrap = false, maxoff = Infinity) { const newp = []; const points = this.points; const length = points.length; @@ -2330,9 +2358,14 @@ export class Polygon { const dy = p2.y - p1.y; const dl = Math.sqrt(dx * dx + dy * dy); newp.push(p1); - if (dl < max) { + if (mark) p1.segment = p1; + // if segment shorter than max (delta) or + // maxoff is exhausted (only segmenting up to some length) + // then skip sub-segmenting the current segment + if (dl < max || maxoff < 0) { continue; } + maxoff -= dl; const div = dl / max; const fit = div | 0; const add = fit - 1; @@ -2344,10 +2377,15 @@ export class Polygon { newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2)); ox += ix; oy += iy; + // mark new point with first point of originating segment + if (mark) newp.peek().segment = p1; } } if (newp.length > length) { - return newPolygon().addPoints(newp.map(p => p.clone())).setOpenValue(this.open); + if (wrap) { + newp.push(newp[0]); + } + return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(this.open); } return this; } diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 6d4d8128..1d5c5212 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -645,7 +645,7 @@ export function offset(polys, dist, opts = {}) { for (let poly of polys) { // convert to clipper format poly = poly.toClipper(); - if (clean) poly = CleanPolygons(poly, config.clipperClean); + if (clean) poly = CleanPolygons(poly, opts.cleanDist ?? config.clipperClean); if (simple) poly = SimplifyPolygons(poly, fill); coff.AddPaths(poly, join, type); } @@ -871,6 +871,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) { * @returns {Point[]} */ export function rayIntersect(start, slope, polygons, for_fill) { + throw "deprecated"; let i = 0, flat = [], points = [], diff --git a/src/kiri/core/codec.js b/src/kiri/core/codec.js index 06c9f896..3547fb6f 100644 --- a/src/kiri/core/codec.js +++ b/src/kiri/core/codec.js @@ -312,7 +312,7 @@ Polygon.prototype.encode = function(state) { state.poly[this.id] = this; - return { + let enc = { type: TYPE.POLY, id: this.id, array: encodePointArray(this.points, state, this.z), @@ -322,6 +322,20 @@ Polygon.prototype.encode = function(state) { color: this.color, open: this.open }; + + // encode extended point info for tagged polys + if (this._epk) { + let epk = enc.epk = {}; + for (let key of this._epk) { + let arr = epk[key] = []; + let pts = this.points; + for (let i=0, l=pts.length; i 0) { this.lastPoint = this.startPoint; - // TODO: revisit seek to origin as the first move - // addOutput(output, startPoint, 0, undefined, tool); } output.appendAll(input); } @@ -98,6 +96,7 @@ class Print { * @param {Array} output - the array to print to * @param {Object} [options] - optional parameters * @param {boolean} [options.ccw] - set the polygon to be counter-clockwise + * @param {boolean} [options.scarf] - scarf seam permitted * @param {number} [options.extrude] - extrude factor for the polygon * @param {number} [options.rate] - print speed in mm/s * @param {number} [options.coast] - distance to coast at the end of the polygon @@ -119,15 +118,16 @@ class Print { const { settings } = scope; const { process } = settings; - let shortDist = process.outputShortDistance, - shellMult = numOrDefault(options.extrude, process.outputShellMult), + let shellMult = numOrDefault(options.extrude, process.outputShellMult), printSpeed = options.rate || process.outputFeedrate, moveSpeed = process.outputSeekrate, minSpeed = process.outputMinSpeed, + nozzleSize = options.nozzleSize, coastDist = options.coast || 0, closest = options.simple ? poly.first() : poly.findClosestPointTo(startPoint), perimeter = poly.perimeter(), close = !options.open, + scarf = !poly.open ? (options.scarf ?? 0) : false, tool = options.tool, last = startPoint, first = true; @@ -137,10 +137,74 @@ class Print { printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly); } - poly.forEachPoint((point, pos, points, count) => { - if (first) { + // if not starting at first point in poly, rotate to move start to index = 0 + let pp = poly.points; + if (closest.index > 0) { + let cio = pp.indexOf(closest.point); + pp = poly.points = [ ...pp.slice(cio), ...pp.slice(0, cio) ]; + } + + // scarf sanity checks + if (scarf) { + // cancel scarf for thin wall polys + if (pp.filter(p => p.skip || p.moved).length) { + scarf = 0; + } else { + // cancel scarf if any point.z differs + let z0 = pp[0].z; + let zd = 0; + for (let p of pp) zd += Math.abs(p.z - z0); + if (zd) scarf = 0; + } + // console.log({ scarf }); + } + + // when creating scarf seams, segment poly up to seam length + // create array of step up points at start of poly with increasing z + // and increasing shellMult and then append the same points on the back + // end of the poly with fixed z and decreasing shellMult + if (scarf) { + let epz = Math.max(...poly.points.map(p => p.z)); + let spz = startPoint.z; + poly = poly.segment(options.nozzleSize ?? 0.4, false, false, scarf * 2); + pp = poly.points; + let lp, sp = []; + for (let p of pp) { + let d = lp?.distTo2D(p) ?? 0; + sp.push(lp = p); + scarf -= d; + if (scarf <= 0) break; + } + let fcs = 1.0; // flow compensation seam + let fco = (1 / sp.length) * 0.0; // flow compensation offset (- half step) + let zd = (epz - spz) / sp.length; + let zi = 1; + for (let p of sp) { + p.z -= zd * (sp.length - zi); + p.moved = (((zi++) / sp.length) * fcs) - 1 - fco; + } + let esp = sp.map(p => p.clone()); // ending scarf points + for (let p of esp) { + p.z = epz; + p.moved = (((--zi) / esp.length) * fcs) - 1 - fco; + } + pp.push(...esp); + scarf = true; + } + + // scarf manages its own close point + if (close && !scarf) { + pp.push(pp[0]); + } + + let lpo; + for (let point of pp) { + if (point.skip && lpo?.skip) { + scope.addOutput(output, point, 0, moveSpeed, tool); + } else if (first) { + // if (point.skip) console.log({ skip: point }); if (options.onfirst) { - options.onfirst(point); + options.onfirst(point, output); } // move to first output point on poly let out = scope.addOutput(output, point, 0, moveSpeed, tool); @@ -150,7 +214,8 @@ class Print { first = false; } else { let seglen = last.distTo2D(point); - if (coastDist && shellMult && perimeter - seglen <= coastDist) { + // cancel coast when using scarf seam + if (!scarf && coastDist && shellMult && perimeter - seglen <= coastDist) { let delta = perimeter - coastDist; let offset = seglen - delta; let offPoint = last.offsetPointFrom(point, offset) @@ -158,10 +223,13 @@ class Print { shellMult = 0; } perimeter -= seglen; - scope.addOutput(output, point, shellMult, printSpeed, tool); + // increase mult by % of point moved relative to nozzle radius + let multOut = shellMult + (point.moved ?? 0); + // to increase shellMult when point.inc set for collapsed points + scope.addOutput(output, point, multOut, printSpeed, tool); } - last = point; - }, close, closest.index); + last = lpo = point; + } this.lastPoly = poly; @@ -169,6 +237,14 @@ class Print { } constReplace(str, consts, start, pad, short) { + function tryeval(str) { + try { + return eval(`{ ${str} }`) + } catch (e) { + console.log({ eval_error: e, str }); + return str; + } + } let cs = str.indexOf("{", start || 0), ce = str.indexOf("}", cs), tok, nutok, nustr; @@ -192,8 +268,7 @@ class Print { } eva.push(`function range(a,b) { return (a + (layer / layers) * (b-a)).round(4) }`); eva.push(`try {( ${tok} )} catch (e) {console.log(e);0}`); - let scr = eva.join(''); - let evl = eval(`{ ${scr} }`); + let evl = tryeval(eva.join('')); nutok = evl; if (pad === 666) { return evl; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 0567d53d..1c92f077 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -181,8 +181,8 @@ async function path(levels, update, opts = {}) { return; } - if (lastOut) { // if last outpoint is defined - + // if last outpoint is defined + if (lastOut) { if (arrowAll || lastOut.emit !== out.emit) { heads.push({p1: lastOutPoint, p2: outPoint}); } @@ -192,7 +192,7 @@ async function path(levels, update, opts = {}) { // 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 + 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(); @@ -200,17 +200,13 @@ async function path(levels, update, opts = {}) { 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 - // 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/settings.js b/src/kiri/core/settings.js index 1a5b65b0..5efa6a30 100644 --- a/src/kiri/core/settings.js +++ b/src/kiri/core/settings.js @@ -286,7 +286,7 @@ function updateFieldsFromSettings(setrec, uirec = api.ui, opt = {}) { function updateExtruderFields(device) { const { ui } = api; - const { LANG } = api.const; + const { current: LANG } = api.const.LANG; if (device.extruders && device.extruders[device.internal]) { updateFieldsFromSettings(device.extruders[device.internal]); ui.extruder.innerHTML = diff --git a/src/kiri/core/slice.js b/src/kiri/core/slice.js index 934f7394..d684d046 100644 --- a/src/kiri/core/slice.js +++ b/src/kiri/core/slice.js @@ -144,7 +144,9 @@ class Slice { } else { // create top object from object bundle passed back by slicePost() let top = new Top(data.poly); - top.thin_fill = data.thin_fill ? data.thin_fill.map(p => newPoint(p.x,p.y,p.z)) : undefined; + top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z)); + top.thin_wall = data.thin_wall; + top.thin_sort = data.thin_sort; top.fill_lines = data.fill_lines; top.fill_sparse = data.fill_sparse; top.fill_off = data.fill_off; diff --git a/src/kiri/core/widget.js b/src/kiri/core/widget.js index 3e340433..783598ca 100644 --- a/src/kiri/core/widget.js +++ b/src/kiri/core/widget.js @@ -360,6 +360,10 @@ class Widget { this.setOpacity(opacity ?? 1); } + refreshVisualState() { + this.setVisualState(this.getVisualState()); + } + restoreVisualState() { if (this.cache.vizstate) { this.setVisualState(this.cache.vizstate); @@ -613,6 +617,7 @@ class Widget { this.roto = []; this.center(); this.setModified(); + this.refreshVisualState(); } mirror() { diff --git a/src/kiri/mode/cam/cl-helical.js b/src/kiri/mode/cam/cl-helical.js index 6894f0a6..f8102e94 100644 --- a/src/kiri/mode/cam/cl-helical.js +++ b/src/kiri/mode/cam/cl-helical.js @@ -5,59 +5,59 @@ import { clearPops, env } from "./client.js"; let alert, lastWidget; export let helicalOn = false; export function selectHelical() { - if (helicalOn) { - return helicalDone(); - } - clearPops(); - alert = api.show.alert("analyzing surfaces...", 1000); - let cylinders = env.poppedRec.cylinders; + if (helicalOn) { + return helicalDone(); + } + clearPops(); + alert = api.show.alert("analyzing surfaces...", 1000); + let cylinders = env.poppedRec.cylinders; - CAM.surface_prep(false, () => { - api.hide.alert(alert); - alert = api.show.alert("[esc] cancels surface selection"); - for (let [wid, arr] of Object.entries(cylinders)) { - let widget = api.widgets.forid(wid); - if ( widget && arr?.length ) - for (let faceid of arr) { - CAM.cylinderToggle(widget, faceid, (faceids) => { - // cylinders[widget.id] = faceids; - }); + CAM.surface_prep(false, () => { + api.hide.alert(alert); + alert = api.show.alert("[esc] cancels surface selection"); + for (let [wid, arr] of Object.entries(cylinders)) { + let widget = api.widgets.forid(wid); + if (widget && arr?.length) + for (let faceid of arr) { + CAM.cylinderToggle(widget, faceid, (faceids) => { + // cylinders[widget.id] = faceids; + }); + } + } + }); + helicalOn = env.hoveredOp; + helicalOn.classList.add("editing"); + api.feature.on_mouse_up = (obj, ev) => { + let { face } = obj; + let min = Math.min(face.a, face.b, face.c); + let faceid = min / 3; + let widget = (lastWidget = obj.object.widget); + CAM.cylinderToggle(widget, faceid, ({ faces, error }) => { + if (error) { + api.show.alert(error, 3000); + console.log(error); + return; + } + cylinders[widget.id] = faces; + }); + }; +}; + +export function helicalDone() { + if (!(helicalOn && env.poppedRec && env.poppedRec.cylinders)) { + return; + } + let { cylinders } = env.poppedRec; + for (let wid of Object.keys(cylinders)) { + let widget = api.widgets.forid(wid); + if (widget) { + CAM.cylinderClear(widget); + } else { + delete cylinders[wid]; } } - }); - helicalOn = env.hoveredOp; - helicalOn.classList.add("editing"); - api.feature.on_mouse_up = (obj, ev) => { - let { face } = obj; - let min = Math.min(face.a, face.b, face.c); - let faceid = min / 3; - let widget = (lastWidget = obj.object.widget); - CAM.cylinderToggle(widget, faceid, ({ faces, error }) => { - if ( error ) { - api.show.alert(error, 3000); - console.log( error ); - return; - } - cylinders[widget.id] = faces; - }); - }; -}; - -export function helicalDone(){ - if (!( helicalOn && env.poppedRec && env.poppedRec.cylinders )) { - return; - } - let { cylinders } = env.poppedRec; - for (let wid of Object.keys(cylinders)) { - let widget = api.widgets.forid(wid); - if (widget) { - CAM.cylinderClear(widget); - } else { - delete cylinders[wid]; - } - } - api.hide.alert(alert); - api.feature.on_mouse_up = undefined; - helicalOn.classList.remove("editing"); - helicalOn = false; + api.hide.alert(alert); + api.feature.on_mouse_up = undefined; + helicalOn.classList.remove("editing"); + helicalOn = false; }; diff --git a/src/kiri/mode/cam/cl-origin.js b/src/kiri/mode/cam/cl-origin.js index 5b63f8ac..c694f078 100644 --- a/src/kiri/mode/cam/cl-origin.js +++ b/src/kiri/mode/cam/cl-origin.js @@ -25,6 +25,16 @@ export function originSelect() { env.hover = pointHover; env.hoverUp = pointHoverUp; + // less precise point key for hi-res meshes that leads to dups + function updateKey(pt) { + pt._key = [ + ((pt.x * 1000) | 0), + ((pt.y * 1000) | 0), + ((pt.z * 1000) | 0) + ].join(' '); + return pt; + } + CAM.traces((ids) => { api.hide.alert(alert); alert = api.show.alert("select origin
[esc] to cancel", 1000); @@ -53,14 +63,14 @@ export function originSelect() { }); for (let poly of polys) { if (poly.circularity() >= 0.99) { - let center = poly.calcCircleCenter(); + let center = updateKey(poly.calcCircleCenter()); if (points[center.key]) { continue; } addpoint(widget, center); } else if (poly.isOpen()) { - incpoint(widget, poly.first()); - incpoint(widget, poly.last()); + incpoint(widget, updateKey(poly.first())); + incpoint(widget, updateKey(poly.last())); } } }); diff --git a/src/kiri/mode/cam/op-helical.js b/src/kiri/mode/cam/op-helical.js index 82fc7dbc..a8f19e78 100644 --- a/src/kiri/mode/cam/op-helical.js +++ b/src/kiri/mode/cam/op-helical.js @@ -58,7 +58,7 @@ export class OpHelical extends CamOp { } let { zmin, zmax, diam, center, interior, faces } = res; // console.log({ zmin, zmax, diam, center, interior }); - let radius = diam >> 1; + let radius = diam /2; zmax = fromTop ? stock.z: zmax; let radAdd, @@ -138,14 +138,17 @@ export class OpHelical extends CamOp { ); // console.log({down,toBottom,ofFull,ofCircle,startAngle,finalAngle,finalEnd}); - //circle down to final point - poly.addPoints( - arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , { - clockwise, - radius, - center, - }) - ); + //if not already very close to final point + if( ofCircle >= 0.001 ){ + //circle down to final point + poly.addPoints( + arcToPath(startPoint.setZ(currentZ), finalEnd, numSegs , { + clockwise, + radius, + center, + }) + ); + } //if starting at the bottom, reverse poly if ( reverse ){ @@ -169,9 +172,9 @@ export class OpHelical extends CamOp { let lineOut = radius - entryOffset; let centerOffset = (entryOffset**2 + 2*entryOffset*lineOut) / (2*(entryOffset + lineOut)); //add entry points - entry.push(... + entry.push( newPoint(0,-lineOut,0), - arcToPath( + ...arcToPath( newPoint(0,-lineOut,0), newPoint(radius,0,0), 128, @@ -186,12 +189,15 @@ export class OpHelical extends CamOp { } } //for each point + //rotate by start angle + // add to hole center + let invert = ((clockwise && !reverse ) || (!clockwise && reverse) ) ? -1 : 1 + let entryAngle = reverse ? finalAngle : startAngle entry = entry.map(p => { - //rotate by start angle - // add to hole center // TODO: there's totally a way to do this with a matrix - p.y *= clockwise ? 1 : -1 - p.rotate(startAngle) + // logical XOR + p.y *= invert + p.rotate(entryAngle) return p.add(entryCenter) }) //append to start of poly diff --git a/src/kiri/mode/cam/slice.js b/src/kiri/mode/cam/slice.js index cf77b042..2c9f454f 100644 --- a/src/kiri/mode/cam/slice.js +++ b/src/kiri/mode/cam/slice.js @@ -132,15 +132,17 @@ export async function cam_slice(settings, widget, onupdate, ondone) { } if (stock.x && stock.y && stock.z && !isIndexed) { - if (stock.x + 0.00001 < bounds.max.x - bounds.min.x) { + let maxDelta = 1e-3; + + if (stock.x + maxDelta < (bounds.max.x - bounds.min.x)) { return error('stock X too small for part. resize stock or use offset stock'); } - if (stock.y + 0.00001 < bounds.max.y - bounds.min.y) { + if (stock.y + maxDelta < (bounds.max.y - bounds.min.y)) { return error('stock Y too small for part. resize stock or use offset stock'); } - if (stock.z + 0.00001 < bounds.max.z - bounds.min.z) { + if (stock.z + maxDelta < (bounds.max.z - bounds.min.z)) { return error('stock Z too small for part. resize stock or use offset stock'); } } @@ -510,8 +512,7 @@ export async function traces(settings, widget) { widget.traces = polylines; return true; -}; - +} /** * Returns an array of arrays of perpindicular triangles in the mesh. @@ -523,110 +524,109 @@ export async function traces(settings, widget) { * @param {object} opts - options object * @return {array} - array of arrays of triangle data */ -export async function cylinders(settings, widget, opts){ - - let {} = opts ?? {} - - let {array:verts} = widget.mesh.geometry.attributes.position - let perpTriangles = [] +export async function cylinders(settings, widget, opts) { + let { } = opts ?? {}; + let { array: verts } = widget.mesh.geometry.attributes.position; + let perpTriangles = []; //iterate over all triangles - for(let i = 0; i < verts.length; i+=9){ - let a = [verts[i], verts[i+1], verts[i+2]], - b = [verts[i+3], verts[i+4], verts[i+5]], - c = [verts[i+6], verts[i+7], verts[i+8]]; + for (let i = 0; i < verts.length; i += 9) { + let a = [verts[i], verts[i + 1], verts[i + 2]], + b = [verts[i + 3], verts[i + 4], verts[i + 5]], + c = [verts[i + 6], verts[i + 7], verts[i + 8]]; //calculate normal - let normal = new THREE.Vector3(a[0]-b[0], a[1]-b[1], a[2]-b[2]) - .cross(new THREE.Vector3(b[0]-c[0], b[1]-c[1], b[2]-c[2])) - .normalize() + let normal = new THREE.Vector3(a[0] - b[0], a[1] - b[1], a[2] - b[2]) + .cross(new THREE.Vector3(b[0] - c[0], b[1] - c[1], b[2] - c[2])) + .normalize(); // if perpindicular normal, and at least 2 Zs are the same - if(normal.z.round(5) == 0 && ! (a[2] != b[2] && b[2] != c[2])){ + if (normal.z.round(5) == 0 && !(a[2] != b[2] && b[2] != c[2])) { let minZ = Math.min(a[2], b[2], c[2]), maxZ = Math.max(a[2], b[2], c[2]); - if(minZ == maxZ){ // all Zs are the same indicated malformed geometry + if (minZ == maxZ) { // all Zs are the same indicated malformed geometry continue; } - perpTriangles.push({...[a,b,c], normal,minZ,maxZ,i:index}) + perpTriangles.push({ ...[a, b, c], normal, minZ, maxZ, i: index }); } } //map where zmax, zmin -> triangleData - let cylinderTriangles = new Map() - for(let t of perpTriangles){ - let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}` - if(!cylinderTriangles.has(hash)){ - cylinderTriangles.set(hash,[]) + let cylinderTriangles = new Map(); + for (let t of perpTriangles) { + let hash = `${t.minZ.round(5)},${t.maxZ.round(5)}`; + if (!cylinderTriangles.has(hash)) { + cylinderTriangles.set(hash, []); } - cylinderTriangles.get(hash).push(t) + cylinderTriangles.get(hash).push(t); } - - return Array.from(cylinderTriangles.values()) + + return Array.from(cylinderTriangles.values()); } - - /** - * Find all triangles that are part of the same cylinder as the given triangle - * @param {Widget} widget - widget object - * @param {number} face - index of face in triangle vertex data - * @return {array} - array of all triangle data that belong to the same cylinder - * @requires surface_prep must be called first - */ +/** + * Find all triangles that are part of the same cylinder as the given triangle + * @param {Widget} widget - widget object + * @param {number} face - index of face in triangle vertex data + * @return {array} - array of all triangle data that belong to the same cylinder + * @requires surface_prep must be called first + */ export function cylinder_find (widget, face){ CAM.surface_prep(widget,false); return widget.tool.findCylinderSurface(face); } -export function cylinder_poly_find(widget, face){ +export function cylinder_poly_find(widget, face) { let faces = cylinder_find(widget, face); let vert = widget.getGeoVertices({ unroll: true, translate: true }).map(v => v.round(4)); - let slicer = new cam_slicer(widget,{}) - let opts = { - dedup:false, - edges: false, - over:true - } - let firstOffset = faces[0]*9 - let [x1,y1,z1,x2,y2,z2,x3,y3,z3]= Array.from(vert.subarray(firstOffset,firstOffset+9)) - let zs = [z1,z2,z3].map(z => z.round(5)) + let slicer = new cam_slicer(widget, {}); + let firstOffset = faces[0] * 9; + let [x1, y1, z1, x2, y2, z2, x3, y3, z3] = Array.from(vert.subarray(firstOffset, firstOffset + 9)); + let zs = [z1, z2, z3].map(z => z.round(5)); let zmin = Math.min(...zs); let zmax = Math.max(...zs); - let zmid = (zmin+zmax)/2 - let cylVerts = new Float32Array(faces.length*9) - + let zmid = (zmin + zmax) / 2; + let cylVerts = new Float32Array(faces.length * 9); + let opts = { + dedup: false, + edges: false, + over: true + }; + let i = 0 //for each cylinder face index - for(let index of faces){ - let off = index*9 + for (let index of faces) { + let off = index * 9; //copy the 9 triangle vertices floats in - for(let j = 0;j < 9;j++){ - cylVerts[i] = vert[off+j] - i++ + for (let j = 0; j < 9; j++) { + cylVerts[i] = vert[off + j]; + i++; } } + //slice the poly at the midpoint - let poly = slicer.sliceZ(zmid, cylVerts,opts)?.polys?.at(0) - if(!poly) throw "slicing returned no poly" + let poly = slicer.sliceZ(zmid, cylVerts, opts)?.polys?.at(0); + if (!poly) throw "slicing returned no poly"; + let circular = poly.circularity() > 0.98; + //throw error if not circular - if(!circular) throw "faces must be circular" - + if (!circular) throw "faces must be circular"; + //calculate circle properties let area = poly.area(), - diam = Math.sqrt(area / Math.PI)* 2, + diam = Math.sqrt(area / Math.PI) * 2, center = poly.calcCircleCenter(); - - //find direction using normal to center point + //find direction using normal to center point let point = newPoint(x1, y1, z1), delta = point.setZ(center.z).sub(center), - normal = new THREE.Vector3(x1-x2, y1-y2, z1-z2) - .cross(new THREE.Vector3(x2-x3, y2-y3, z2-z3)), + normal = new THREE.Vector3(x1 - x2, y1 - y2, z1 - z2) + .cross(new THREE.Vector3(x2 - x3, y2 - y3, z2 - z3)), dotProd = normal.dot(delta), - interior = dotProd < 0 + interior = dotProd < 0 - return {faces, zmin, zmax, poly, circular, area, diam, center,interior} + return { faces, zmin, zmax, poly, circular, area, diam, center, interior } } /** diff --git a/src/kiri/mode/fdm/client.js b/src/kiri/mode/fdm/client.js index a7471a5d..7bb6d5cf 100644 --- a/src/kiri/mode/fdm/client.js +++ b/src/kiri/mode/fdm/client.js @@ -426,7 +426,7 @@ export function init() { let idn = new Raycaster(rp, dn) .intersectObjects(targets, false) .filter(t => !t.object.pillar); - if (idn.length && idn.length % 2 === 0) { + if (idn.length) { iw = iup[0].object.widget || iw; let wa = api.widgets.annotate(iw.id); let ws = (wa.support = wa.support || []); @@ -471,10 +471,11 @@ export function init() { iw = null; } p1 = point; + // looking up by default (assumes platform intersect) let dir = new Vector3(0,1,0) let ray = new Raycaster(point, dir); let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0; - // when on object, project down on downward faces + // when intersecting underside of object, project down if (int && int.face && int.face.normal.z < -0.1) { dir.y = -1; } @@ -482,7 +483,7 @@ export function init() { let i2 = ray.intersectObjects(targets, false) .filter(t => !t.object.pillar) // eliminate other pillars .filter(i => i.distance > 0.1); // false matches close to origin of ray - if (i2.length && i2.length % 2 === 0) { + if (i2.length) { p2 = i2[0].point; iw = i2[0].object.widget || iw; let p1y = Math.max(0, p1.y); diff --git a/src/kiri/mode/fdm/export.js b/src/kiri/mode/fdm/export.js index 8481bdc0..b6768e4b 100644 --- a/src/kiri/mode/fdm/export.js +++ b/src/kiri/mode/fdm/export.js @@ -59,7 +59,8 @@ export function fdm_export(print, online, ondone, ondebug) { retSpeed = process.outputRetractSpeed * 60 || 1, // range retDwell = process.outputRetractDwell || 0, // range timeDwell = retDwell / 1000, - arcDist = isBelt ? 0 : (process.arcTolerance || 0), + scarfZ = process.outputScarfLength ? true : false, + arcDist = isBelt || scarfZ ? 0 : (process.arcTolerance || 0), arcMin = 1, arcRes = 20, arcDev = 0.5, @@ -111,7 +112,11 @@ export function fdm_export(print, online, ondone, ondebug) { progress: 0, nozzle: tool, tool: tool, - model_labels: model_labels.sort().join(',') + oid: '', + oname: '', + model_labels: model_labels.sort().join(','), + total_time: Math.ceil(print.total_time / 60), + remain_time: Math.ceil(print.total_time / 60), }, pidx, path, out, speedMMM, emitMM, emitPerMM, lastp, laste, dist, lines = 0, @@ -525,6 +530,7 @@ export function fdm_export(print, online, ondone, ondebug) { // retract before first move retract(); + subst.remain_time = Math.ceil(print.total_time / 60); while (layer < layers.length) { path = layers[layer]; layerno = path.slice.index; @@ -598,7 +604,7 @@ export function fdm_export(print, online, ondone, ondebug) { } // move Z to layer height - if (layer > 0 || !isBelt) { + if (layer === 0 || ((layer > 0 || !isBelt) && !scarfZ)) { moveTo({z:zpos}, seekMMM); } @@ -620,6 +626,8 @@ export function fdm_export(print, online, ondone, ondebug) { let b64 = encodeBitOffset(off); append(`; start object id: ${out.widget.track.grid_id}`); isBambu && append(`M624 ${b64}`); + subst.oid = out.widget.track.grid_id; + subst.oname = out.widget.meta.file; } cwidget = out.widget; } @@ -779,12 +787,20 @@ export function fdm_export(print, online, ondone, ondebug) { } else { // emitMM = emitPerMM * out.emit * dist; emitMM = extrudeMM(dist, emitPerMM, out.emit); - moveTo({x:x, y:y, e:emitMM}, speedMMM); + if (scarfZ) { + moveTo({x, y, z:z + path.height/2, e:emitMM}, speedMMM); + } else { + moveTo({x, y, e:emitMM}, speedMMM); + } emitted += emitMM; } } else { drainQ(); - moveTo({x:x, y:y}, seekMMM); + if (scarfZ) { + moveTo({x, y, z:z + path.height/2}, speedMMM); + } else { + moveTo({x, y}, seekMMM); + } // 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 @@ -830,11 +846,13 @@ export function fdm_export(print, online, ondone, ondebug) { append(`M808`); } drainQ(); + + print.total_time -= path.print_time; + subst.remain_time = Math.ceil(print.total_time / 60); } function emitQrec(rec) { let {e, x, y, dist, emitPerMM, speedMMM} = rec; - // emitMM = emitPerMM * e * dist; emitMM = extrudeMM(dist, emitPerMM, e); moveTo({x:x, y:y, e:emitMM}, speedMMM); emitted += emitMM; @@ -958,7 +976,6 @@ export function fdm_export(print, online, ondone, ondebug) { } return true; - } if (inloops) { diff --git a/src/kiri/mode/fdm/init-menu.js b/src/kiri/mode/fdm/init-menu.js index bb34b97f..fc0506c1 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -74,6 +74,8 @@ export function menu() { sliceBottomLayers: newInput(LANG.sl_lbot_s, { title:LANG.sl_lbot_l, convert:toInt }), separator: newBlank({ class:"set-sep", driven }), sliceAdaptive: newBoolean(LANG.ad_adap_s, onBooleanClick, { title: LANG.ad_adap_l }), + outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), + sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), _____: newGroup(LANG.sw_menu, $('fdm-walls'), { modes:FDM, driven, hideable, separator, group:"fdm-walls" }), sliceShells: newInput(LANG.sl_shel_s, { title:LANG.sl_shel_l, convert:toFloat }), sliceLineWidth: newInput(LANG.sl_line_s, { title:LANG.sl_line_l, convert:toFloat, bound:bound(0,5) }), @@ -82,23 +84,25 @@ export function menu() { sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"), outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}), sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}), + _____: newGroup(LANG.fs_menu, $('fdm-solid'), { modes:FDM, driven, hideable, separator, group:"fdm-solid" }), + sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), + sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}), + sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), + sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), _____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }), sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"), sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}), sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}), - sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}), separator: newBlank({ class:"set-sep", driven }), sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}), sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}), - separator: newBlank({ class:"set-sep", driven }), - sliceFillGrow: newInput(LANG.fi_grow_s, {title:LANG.fi_grow_l, convert:toFloat}), - sliceFillAngle: newInput(LANG.fi_angl_s, {title:LANG.fi_angl_l, convert:toFloat}), _____: newGroup(LANG.fh_menu, $('fdm-heat'), { modes:FDM, driven, hideable, separator, group:"fdm-heat" }), outputTemp: newInput(LANG.ou_nozl_s, {title:LANG.ou_nozl_l, convert:toInt}), outputBedTemp: newInput(LANG.ou_bedd_s, {title:LANG.ou_bedd_l, convert:toInt}), _____: newGroup(LANG.fc_menu, $('fdm-cool'), { modes:FDM, driven, hideable, separator, group:"fdm-cool" }), outputFanLayer: newInput(LANG.ou_fanl_s, { title:LANG.ou_fanl_l, convert:toInt, bound:bound(0,255) }), outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), + outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }), _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), sliceSupportDensity: newInput(LANG.sp_dens_s, {title:LANG.sp_dens_l, convert:toFloat, bound:bound(0.0,1.0)}), @@ -164,23 +168,21 @@ export function menu() { outputRetractSpeed: newInput(LANG.ad_rrat_s, {title:LANG.ad_rrat_l, convert:toInt}), outputRetractWipe: newInput(LANG.ad_wpln_s, {title:LANG.ad_wpln_l, convert:toFloat, bound:bound(0.0,10)}), separator: newBlank({ class:"set-sep", driven }), - sliceLayerStart: newSelect(LANG.sl_strt_s, {title:LANG.sl_strt_l}, "start"), - outputLayerRetract: newBoolean(LANG.ad_lret_s, onBooleanClick, {title:LANG.ad_lret_l}), outputAvoidGaps: newBoolean(LANG.ad_agap_s, onBooleanClick, {title:LANG.ad_agap_l}), separator: newBlank({ class:"set-sep", driven, show:isBelt }), outputBeltFirst: newBoolean(LANG.ad_lbir_s, onBooleanClick, {title:LANG.ad_lbir_l, show:isBelt}), _____: newGroup(LANG.ad_menu, $('fdm-expert'), { modes:FDM, driven, hideable, separator, group:"fdm-xprt" }), sliceAngle: newInput(LANG.sl_angl_s, {title:LANG.sl_angl_l, convert:toFloat, show:isBelt}), + antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toFloat, bound:bound(0,3)}), + arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), outputRetractDwell: newInput(LANG.ad_rdwl_s, {title:LANG.ad_rdwl_l, convert:toInt}), - sliceSolidMinArea: newInput(LANG.ad_msol_s, {title:LANG.ad_msol_l, convert:toFloat}), outputMinSpeed: newInput(LANG.ad_mins_s, {title:LANG.ad_mins_l, convert:toFloat, bound:bound(1,200)}), + outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), + outputScarfLength: newInput(LANG.ad_scar_s, {title:LANG.ad_scar_l, convert:toFloat, bound:bound(0,1000)}), outputShortPoly: newInput(LANG.ad_spol_s, {title:LANG.ad_spol_l, convert:toFloat, bound:bound(0,10000)}), outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}), zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}), - arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}), - antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}), outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}), - outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}), fdmRanges: $('fdm-ranges'), diff --git a/src/kiri/mode/fdm/post.js b/src/kiri/mode/fdm/post.js index a2222402..25cd368e 100644 --- a/src/kiri/mode/fdm/post.js +++ b/src/kiri/mode/fdm/post.js @@ -7,6 +7,7 @@ import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { getRangeParameters } from './driver.js'; import { slicer } from '../../../geo/slicer.js'; import { Point } from '../../../geo/point.js'; +import { Slope } from '../../../geo/slope.js'; slicer.slicePost.FDM = slicePost; @@ -20,7 +21,26 @@ export function slicePost(data, options) { const { useAssembly, post_args, zIndexes } = options; const { process, isSynth, vaseMode } = post_args; const { shellOffset, fillOffset, clipOffset } = post_args; - data.tops = POLY.nest(groups); + + if (options.pump) { + let nugroups = []; + let pump = shellOffset / 10; + for (let p of groups) { + let retop = POLY.offset([ p ], -pump, { + join: ClipperLib.JoinType.jtRound, + arc: (1/pump) * 4 + }); + retop = POLY.offset(retop, pump, { + join: ClipperLib.JoinType.jtRound, + arc: (1/pump) * 4 + }); + nugroups.push(...retop); + } + data.tops = POLY.nest(nugroups); + } else { + data.tops = POLY.nest(groups); + } + if (isSynth) { const process = post_args.process; if (process.sliceSupportGrow > 0) { @@ -69,7 +89,10 @@ export function slicePost(data, options) { }; function offset_default(params) { - let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params; + let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps, thin } = params; + + let first; + // standard wall offsetting strategy POLY.offset( top_poly, @@ -81,6 +104,7 @@ function offset_default(params) { outs: top.shells, flat: true, call: (polys, onCount) => { + first = first || polys; last = polys; // mark each poly with depth (offset #) starting at 0 for (let p of polys) { @@ -99,6 +123,79 @@ function offset_default(params) { } } ); + + if (!thin) { + return { last, gaps }; + } + + const deltaBig = 160; + const deltSmall = 20; + + // look for close points on adjacent segmented polys and merge + let test_polys = first !== last ? [ ...first, ...last ] : last; + let test_flats = POLY.flatten(test_polys ?? []); + + // pre-compute segment angles + for (let poly of test_flats) + poly.forEachSegment((p0, p1) => { + p0.angle = new Slope(p0, p1); + }); + + // segment each poly to be tested + let thin_test = test_flats.map(poly => { + return { + poly, + points: poly.segment(offset1, true).points + } + }); + + for (let pi=0, pil=thin_test.length; pi { + let keep = (last_seg !== p.segment || p.moved || p.skip); + last_seg = p.segment; + return keep; + }); + } + } + return { last, gaps }; } @@ -164,31 +261,189 @@ function thin_type_2(params) { function thin_type_3(params) { // offsetN usually = extrusion width (nozzle diameter) - let { z, top, count, offsetN, last, gaps } = params; + let { z, top, count, offsetN } = params; // produce trace from outside of poly inward no more than max inset - let { noodle, remain } = top.poly.noodle(offsetN * count); + let noodleWidth = offsetN * count; + let { noodle, remain } = top.poly.noodle(noodleWidth); + for (let n of POLY.flatten(noodle).sort((a,b) => a.depth - b.depth)) { + n.shell = [1,-1][Math.floor(n.depth / 2)]; + // console.log('map', n.depth, n.shell, n.length, n.area().round(3)); + } - // top.shells = noodle; - top.gaps = last = remain; + // re-expand inner offset so fill offsets align properly with expectation + remain = POLY.offset(remain, offsetN / 2); - let thin = top.thin_fill = []; - let sparse = top.fill_sparse = []; + let debugNoodle = false; + let debugRemain = false; + let debugExtrusion = false; + + let shells = top.shells; // only used for debug + let traces = top.thin_wall = []; + + let midR = offsetN * 0.5; // nominal extrusion width + let minR = midR * 0.75; // smallest interior fill or single wall width + let maxR = midR * 1.5; // max double wall extrusion width + let maxF = midR * 2; // max interior extrusion width + + let lines = []; + let polys = []; + + // show inset "noodle" + if (debugNoodle) polys.push(...noodle); + + outer: for (let i=0; i b.area() - a.area())) + trace_noodle( + [ n ], + noodleWidth, + minR, + i === 0 ? midR : midR * 1.25, + i === 0 ? maxR : maxF, + { + shell: n.shell, + lines, + polys, + remain: next, + traces, + brute: false, + showChainIntersect: false + }); + + // show remaining noodle after single trace + if (debugRemain) polys.push(...POLY.setZ(POLY.flatten(next), z)); + + noodle = next; + } + + // console.log({ lines, polys, trace }); + + // show extrusion + let zo = z + 0.1; + if (debugExtrusion) + for (let trace of traces) + for (let point of trace) { + polys.push(newPolygon().centerCircle(point, point.r, 12).setZ(zo = zo + 0.005)); + } + + if (!debugExtrusion) + POLY.setZ([...lines, ...polys], z); + + if (lines.length) top.thin_fill = lines; + if (polys.length) shells.push(...polys); + + // create shell depth annotation that will survive serialization + top.thin_sort = traces.map(t => t.shell); + + // remove point.segment markers for codec encoding between minion and worker + for (let trace of traces) { + for (let pt of trace) { + delete pt.s; + } + } + + // console.log({ traces: traces.map(t => [t.length, t.shell]) }); + // console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces, polys }); + return { traces, last: remain }; +} + +// trace a single extrusion line around the inside of the noodle poly +function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) { + let { + brute, + showInset, + showChainIntersect, + minArea = midR * midR, + shellStep = 1 / 100, // diameter step as % of nozzle + lines = [], + polys = [], + remain = [], + traces = [], + shell = 0 + } = opt; + + // increment shell with direction (1=outside, -1=inside) + let shellAdd = shell > 0 ? 1 : -1; + let sstep = 1 / shellStep; + let insets = []; let scale = 1000; - let minR = offsetN / 2; - let maxR = minR * 1.5; - let minSpur = offsetN * 2; + let dstep = minR / 4; + let dtotl = 0; - 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 inset = POLY.offset(noodle, -dstep, { + join: ClipperLib.JoinType.jtRound, + arc: (1/dstep) * 4 + }); + + while (inset && inset.length) { + dtotl += dstep; + for (let p of POLY.flatten(inset, [])) { + p.dtotl = dtotl; + } + insets.push(inset); + if (showInset) { + polys.appendAll(inset); + } + if (!brute) { + break; + } + inset = POLY.offset(inset, -dstep, { + minArea: 0 + }); + } + + // project from first inset and find greated offset intersection. + // this examples show how to brute force create a medial axis from + // micro stepping insets. the same intersect technique used with + // medial axis can be applied to this structure to produce an + // equivalent output. not as computationally efficient, but much simpler. + // if (brute) { + // let { intersectRayLine } = base.util; + // let tests = insets.slice(1); + // let source = POLY.flatten(insets[0], []); + // for (let poly of source) { + // // segment the smallest inset and test against all other insets + // poly.segment(minR).forEachSegment((p1, p2) => { + // let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; + // let len = pointDist(p1, p2); + // let dx = (p1.x - p2.x) / len; + // let dy = (p1.y - p2.y) / len; + // let np2 = { x: np1.x + dy * 10, y: np1.y - dx * 10 }; + // let max = { dtotl: poly.dtotl, dist: Infinity, maxd: Infinity }; + // let term = false; + // outer: for (let test of tests) { + // for (let tpoly of POLY.flatten(test, [])) { + // tpoly.forEachSegment((tp1, tp2) => { + // let int = intersectRayLine(np1, { dx: dy, dy: -dx }, tp1, tp2); + // if (int && int.dist > int.p1.poly.dtotl) { + // return; + // } + // if (int && int.dist <= max.maxd && int.p1.poly.dtotl > max.dtotl) { + // max.dtotl = int.p1.poly.dtotl; + // max.dist = int.dist; + // max.int = int; + // } else if (int && int.dist <= max.maxd && int.p1.poly.dtotl === max.dtotl && int.dist < max.dist) { + // max.dtotl = int.p1.poly.dtotl; + // max.dist = int.dist; + // max.int = int; + // } + // }); + // if (term) { + // break outer; + // } + // } + // } + // if (max.int) { + // lines.push(new Point(np1.x, np1.y)); + // lines.push(new Point(max.int.x, max.int.y)); + // } + // }); + // } + // return { lines, polys }; + // } let pointMap = new Map(); let lineMap = new Map(); @@ -240,11 +495,8 @@ function thin_type_3(params) { return rec; } - 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 }); @@ -253,349 +505,174 @@ function thin_type_3(params) { // construct medial axis lines. this appears to produce duplicate // reverse segments in a subset of cases let ma = JSPoly.construct_medial_axis(out, inr); - // dedup the points so we can track them uniquely (for chain intersections) + if (ma.length === 0) throw('cannot construct medial axis'); + // dedup the points so we can track them uniquely for (let { point0, point1 } of ma) { pointsToLine(point0, point1); } } - // 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 === 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) { - chain.total += len; - break; - } - } - if (!add) { - chains.push([ p0, p1 ]); - chains.peek().total = len; - } - } - - // 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 ]; } else if (cr <= maxR * 2) { - return [ cr / 2, cr / 2 ]; + let rem = (cr - midR * 2); + if (rem < minR) { + return [ cr / 2, cr / 2 ]; + } + return [ midR, rem, midR ]; } else if (cr <= maxR * 3) { - let rem = cr - minR * 2; - if (rem < maxR) { - return [ minR, rem, minR ]; - } else { - return [ minR, rem / 2, rem / 2, minR ]; + let rem = (cr - midR * 2); + if (rem < minR) { + return [ cr / 3, cr / 3, cr / 3 ]; } + return [ midR, rem, midR ]; } else { - return [ minR, ...div(cr - minR * 2), minR ]; + return [ midR, cr - midR * 2, midR ]; } } - // filter chains that are too short from consideration - chains = chains.filter(c => c.total >= minR); - chains.sort((c1,c2) => c2.total - c1.total); + // medial axis segments for comparison + let segs = [...lineMap.values()]; - 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; - } - } - } - - // 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 0 ? (poly.parent ? shellAdd : 0) : (poly.parent ? 0 : shellAdd); + let ltpo; + let trace = []; + trace.shell = shell + sadd; + poly.segment(minR, true).forEachSegment((p1, p2) => { + let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 }; + let len = pointDist(p1, p2); + let dx = (p1.x - p2.x) / len; + let dy = (p1.y - p2.y) / len; + let min = { dist: Infinity }; + for (let seg of segs) { + let { p0, p1 } = seg; + let int = intersectRayLine(np1, { dx: dy, dy: -dx }, p0, p1); + if (int?.dist < min.dist && int?.dist < noodleWidth) { + min = int; + min.seg = seg; } } + if (min.p1) { + let mp1 = min.seg; + if (showChainIntersect) { + lines.push(new Point(np1.x, np1.y)); + lines.push(new Point(min.x, min.y)); + } + let mr = min.dist + dstep; + if (mr < minR) { + nupoly.push(new Point(min.x, min.y)); + return; + } + // use intersection off and divide into extrusion lanes + let pop = div(mr); + let odd = (pop.length % 2 === 1); + let len = Math.ceil(pop.length / 2); + // check if segment was claimed by a different originating segment + // for odd wall counts so that they're not emitted twice + // todo: failing for noodle width single wall on circular objects + // update claim system to eliminate overlapping close singles + // and/or extend segment definition to curve within angle deviation + // instead of strict point/point segments + if (odd && mp1.claimed && mp1.claimed !== p1.segment) len--; + if (len === 0) { + nupoly.push(new Point(min.x, min.y)); + return; + } + let npo; + if (pop.length === 1) { + // let rad_step = ((pop[0] * sstep) | 0) / sstep; + // for single wall place it exactly on medial axis + nupoly.push(new Point(min.x, min.y)); + npo = { x: min.x, y: min.y, r: mr, s: p1.segment }; + } else { + // radius rounded to nearest step + let rad_step = ((pop[0] * sstep) | 0) / sstep; + // otherwise use first division + // compensate for minimal inset + let off = -dstep + rad_step; + npo = { + x: np1.x + dy * off, + y: np1.y - dx * off, + r: rad_step, + s: p1.segment + }; + // trace inset by full diameter + nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); + } + let lpo = trace.peek(); + // when outputting the same segment at the same radius + // it's safe to drop the last point since path output will connect them + if (trace.length > 1 && lpo.s === npo.s && lpo.r === npo.r) { + trace.pop(); + } + trace.push(npo); + if (odd && !mp1.claimed) { + mp1.claimed = p1.segment; + } + // detect trace jump and start new trace + if (ltpo && pointDist(ltpo, trace.peek()) > maxR) { + // start new trace removing jump point + // and adding that to the new trace + traces.push(trace); + let nutrace = [ trace.pop() ]; + nutrace.shell = trace.shell; + // close last trace if endpoints near enough + if (pointDist(ltpo, trace[0] <= maxR)) { + trace.push(trace[0]); + } + trace = nutrace; + } + // update last trace point out + ltpo = trace.peek(); + } + }); + // repeat first point of trace (if close enough to be closed) + if (trace[0] && pointDist(trace[0], trace.peek()) <= maxR) { + trace.push(trace[0]); } - { - // 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); + if (trace.length) { + traces.push(trace); } } + + // subtract inner extrusion offsets from parent extrusion offset + // this can result in more top level polys and must be processed + // iteratively. + let setA = [ parent ]; + for (let setB of inner) { + setA = POLY.subtract(setA, [ setB ], []); + } + + // clean and re-nest the resulting polygon soup + // subtraction cleans pre-subtract but not post and these + // outlines can have sharp unprintable interiors + let ret = POLY.nest( + POLY.flatten(setA, [], true).map(p => { + return p.clean().simplify(); + }).flat().map(p => { + p.shell = shell + shellAdd; + return p; + }) + ).filter(p => p.area() >= minArea); + + remain.push(...ret); } - POLY.setZ([...thin, ...top.shells], z); - // POLY.setZ([...top.shells], z); - - return { last, gaps }; + return { lines, polys, remain, traces }; } /** @@ -638,22 +715,32 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = { if (dist < 1) p.open = false; } }); let ret = { last, gaps }; - switch (opt.thinType) { - case "type 1": + let ttype = opt.thinType; + switch (ttype) { + case "legacy 1": ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset }); break; - case "type 2": + case "legacy 2": ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm }); break; - case "type 3": - ret = thin_type_3({ z, top, count, top_poly, offsetN }); - break; + case "adaptive": + try { + ret = thin_type_3({ z, top, count, offsetN }); + break; + } catch (e) { + console.log('adaptive layer failed. falling back to basic.', e); + ttype = "basic"; + } + case "basic": default: - ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm }); + ret = offset_default({ + z, top, count, top_poly, offset1, offsetN, wasm, + thin: ttype === 'basic' + }); break; } - last = ret.last || last; - gaps = ret.gaps || gaps; + last = ret.last ?? last; + gaps = ret.gaps ?? gaps; } } else { // no shells, just infill, is permitted diff --git a/src/kiri/mode/fdm/prepare.js b/src/kiri/mode/fdm/prepare.js index c7a90638..8d560b43 100644 --- a/src/kiri/mode/fdm/prepare.js +++ b/src/kiri/mode/fdm/prepare.js @@ -3,7 +3,7 @@ import { base, util } from '../../../geo/base.js'; import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js'; import { newBounds } from '../../../geo/bounds.js'; -import { newPoint } from '../../../geo/point.js'; +import { newPoint, Point } from '../../../geo/point.js'; import { newPolygon, Polygon } from '../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { newPrint } from '../../core/print.js'; @@ -41,6 +41,7 @@ export async function fdm_prepare(widgets, settings, update) { zneg = 0, zoff = 0, zmin = 0, + zmax = 0, shield, output = [], layerout = []; @@ -141,6 +142,7 @@ export async function fdm_prepare(widgets, settings, update) { }); layerout.z = zoff + height; layerout.height = height; + layerout.print_time = printPoint.layer_time; output.append(layerout); layerout = []; @@ -195,7 +197,7 @@ export async function fdm_prepare(widgets, settings, update) { }); }); - print.addPrintPoints(preout, layerout, null); + print.addPrintPoints(preout, layerout); if (preout.length) { // retract between brims and print @@ -224,6 +226,7 @@ export async function fdm_prepare(widgets, settings, update) { } for (let slice of widget.slices) { zmin = Math.min(zmin, slice.z); + zmax = Math.max(zmax, slice.z); if (!slice.supports) { continue; } @@ -469,6 +472,9 @@ export async function fdm_prepare(widgets, settings, update) { let lastExt; let lastOut; + print.zmax = zmax; + print.total_time = 0; + // walk cake layers bottom up for (let layer of cake) { // track purge blocks generated for each layer @@ -570,6 +576,8 @@ export async function fdm_prepare(widgets, settings, update) { } } ); + layerout.print_time = printPoint.layer_time; + print.total_time += printPoint.layer_time; print.setWidget(null); lastOut = slice; @@ -796,6 +804,8 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { process = opt.params || settings.process, originCenter = device.originCenter || bedRound, extruder = parseInt(slice.extruder || 0), + scarfLength = process.outputScarfLength || 0, + minLayerTime = process.outputMinLayerTime || 0, nozzleSize = process.sliceLineWidth || device.extruders[extruder].extNozzle, firstLayer = (opt.first || false) && !opt.support, thinWall = nozzleSize * (opt.thinWall || 1.75), @@ -805,10 +815,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { shellOrder = {"out-in":-1,"in-out":1,"alternate":-2}[process.sliceShellOrder] || -1, sparseMult = process.outputSparseMult, coastDist = process.outputCoastDist || 0, - finishSpeed = opt.speed || process.outputFinishrate, firstShellSpeed = process.firstLayerRate, firstFillSpeed = process.firstLayerFillRate, firstPrintMult = process.firstLayerPrintMult, + finishSpeed = firstLayer ? firstShellSpeed : (opt.speed || process.outputFinishrate), printSpeed = opt.speed || (firstLayer ? firstShellSpeed : process.outputFeedrate), fillSpeed = opt.speed || opt.fillSpeed || (firstLayer ? firstFillSpeed || firstShellSpeed : process.outputFeedrate), infillSpeed = process.sliceFillRate || opt.infillSpeed || fillSpeed || printSpeed, @@ -839,10 +849,21 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { shellOrder = -shellOrder; } + // update fill speed when solids detected if (slice.finishSolids) { fillSpeed = process.sliceSolidRate || finishSpeed; } + // on flats layers also slow down shell print speed + if (slice.isFlatsLayer) { + printSpeed = finishSpeed || printSpeed; + } + + // override: adapt bridge layer speed + if (slice.isBridgeLayer) { + fillSpeed /= 2; + } + // apply first layer extrusion multipliers if (firstLayer) { fillMult *= firstPrintMult; @@ -873,7 +894,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // return true if move path from p1 to p2 intersects a // top and a path around the top (inside print) was not found - function intersectsTop(p1, p2) { + function requiresRetract(p1, p2) { if (slice.index < 0) { return false; } @@ -898,15 +919,20 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (dbug === slice.index) console.log(slice.index, {p1, p2, d: p1.distTo2D(p2)}); let ints = []; + let intk = new Set(); let tops = slice.topRouteFlat(); for (let poly of tops) { poly.forEachSegment((s1, s2) => { let ip = util.intersect(p1,p2,s1,s2,base.key.SEGINT); - if (ip) { + if (ip) ip._key = undefined; + if (ip && !intk.has(ip.key)) { ints.push({ip, poly}); + intk.add(ip.key); + ip.p1.poly.hits = (ip.p1.poly.hits ?? 0) + 1; } }); } + ints = ints.filter(i => i.poly.hits === 2); // no intersections if (ints.length === 0) { @@ -1021,8 +1047,8 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { return false; } - // solid infill - function outputTraces(poly, opt = {}) { + // output shell wall + function outputTraces(poly, opt = {}, traceNo = 0) { if (!poly) return; if (Array.isArray(poly)) { if (opt.sort) { @@ -1055,30 +1081,32 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { if (next) { last = next; polys.remove(next); - outputTraces(next, opt); + outputTraces(next, opt, traceNo++); } else { last = null; } } } else { outputOrderClosest(poly, function(next) { - outputTraces(next, opt); + outputTraces(next, opt, traceNo++); }, null); } } else { let finishShell = poly.depth === 0 && !firstLayer; + let scarf = (slice.index > 0 && traceNo === 0 && z - startPoint?.z > 1e-3) ? scarfLength : 0; startPoint = print.polyPrintPath(poly, startPoint, preout, { - ccw: opt.shell && process.outputAlternating && slice.index % 2, - tool: extruder, - rate: finishShell ? finishSpeed : printSpeed, accel: finishShell, - wipe: process.outputWipeDistance || 0, + ccw: opt.shell && process.outputAlternating && slice.index % 2, coast: firstLayer ? 0 : coastDist, extrude: numOrDefault(opt.extrude, shellMult), - onfirst: function(firstPoint) { + nozzleSize, + rate: finishShell ? finishSpeed : printSpeed, + scarf, + tool: extruder, + onfirst: (firstPoint, preout) => { let from = seedPoint || startPoint; if (from.distTo2D(firstPoint) > retractDist) { - if (intersectsTop(from, firstPoint)) { + if (requiresRetract(from, firstPoint)) { retract(); } } @@ -1089,6 +1117,59 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } + function minThinDist(np, trace) { + return Math.min( + Math.hypot(np.x - trace[0].x, np.y - trace[0].y), + Math.hypot(np.x - trace.peek().x, np.y - trace.peek().y), + ); + } + + function outputAdaptiveWalls(thin, sort) { + // restore thin shell order annotation and sort on it + thin.forEach((a,i) => a.shell = sort[i] * shellOrder); + thin.forEach(t => t.shell = t.shell < 0 ? Math.abs(t.shell) + 0.5 : t.shell); + thin.sort((a,b) => a.shell - b.shell); + let np = print.lastPoint; + // perform nearest poly to poly algorithm on shells + for (;;) { + let min = { dist: Infinity }; + for (let trace of thin) { + if (!min.trace) { + min.trace = trace; + } else if (np && min.trace.shell === trace.shell) { + let dist = minThinDist(np, trace); + if (dist < min.dist) min = { dist, trace }; + } + } + if (!min.trace) { + startPoint = np; + return; + } + let { trace } = min; + let fp = trace[0]; + let lp = trace.peek(); + // reverse trace if far end closer + if (np && Math.hypot(np.x - lp.x, np.y - lp.y) < Math.hypot(np.x - fp.x, np.y - fp.y)) { + trace.reverse(); + } + // remove emitted trace + thin = thin.filter(t => t !== trace); + // retract if moving > retractDist + if (np && np.distTo2D(new Point(trace[0].x, trace[0].y)) > retractDist) { + retract(); + } + let outputSpeed = trace.shell === 1 || trace.shell === 1.5 ? finishSpeed : printSpeed; + for (let pt of trace) { + np = new Point(pt.x, pt.y, z); + if (pt === trace[0]) { + print.addOutput(preout, np, 0, moveSpeed); + } else { + print.addOutput(preout, np, ((pt.r * 2) / nozzleSize) * shellMult, outputSpeed); + } + } + } + } + /** * output sparse infill * @param {Polygon[]} polys @@ -1107,7 +1188,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { poly.forEachPoint((p, i) => { let dist = lp.distTo2D(p); let rdst = dist > retractDist; - let itop = rdst && intersectsTop(lp,p); + let itop = rdst && requiresRetract(lp,p); let emit = extrude; // retract if dist trigger and crosses a slice top polygon if (i === 0) { @@ -1151,7 +1232,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } let first = points[0]; let last = first; - if (startPoint && startPoint.distTo2D(first) > thinWall && intersectsTop(startPoint, first)) { + if (startPoint && startPoint.distTo2D(first) > thinWall && requiresRetract(startPoint, first)) { retract(); } print.addOutput(preout, first, 0, moveSpeed, extruder); @@ -1185,10 +1266,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { start = 0, skip = false, lastIndex = -1, - raft = opt.raft || false, + raft = opt.raft ?? false, flow = opt.flow || 1, - near = opt.near || (antiBacklash ? false : true), - fast = opt.fast || false, // support infill only! + near = opt.near ?? (antiBacklash ? false : true), + fast = opt.fast ?? false, // support infill only! fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow, thinDist = near ? thinWall : thinWall; @@ -1300,7 +1381,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { print.addOutput(preout, p2, fill, fillSpeed, extruder); } else { // retract if dist trigger or crosses a slice top polygon - if (!fast && dist > retractDist && (zhop || intersectsTop(startPoint, p1))) { + if (!fast && dist > retractDist && (zhop || requiresRetract(startPoint, p1))) { retract(); } @@ -1409,7 +1490,15 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } // control of layer start point + if (!lastTop) switch (process.sliceLayerStart) { + case "spiral": { + let center = top.poly.bounds.center(); + let angle = (Math.PI * 2) * (slice.z / print.zmax); + let x = center.x + Math.cos(angle)*10000; + let y = center.y + Math.sin(angle)*10000; + startPoint = newPoint(x,y,startPoint.z); + } break; case "last": break; case "random": @@ -1444,6 +1533,9 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { // raft if (top.traces) outputTraces(top.traces); + // thin wall v3 + if (top.thin_wall) outputAdaptiveWalls(top.thin_wall, top.thin_sort); + // innermost shells let inner = next.innerShells() || []; let shells = next.shells || []; @@ -1492,13 +1584,38 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) { } } + // adjust layer output speeds (by slowing them down) + // if layer print time is less than minimum (required for cooling) + let lp, td = 0, tt = 0; + for (let p of preout) { + if (!p.emit) continue; + let mms = p.speed ?? 1; + let dst = lp?.distTo2D(p.point) ?? 0; + if (dst) { + tt += (dst/mms); + td += dst; + } + lp = p.point; + } + if (minLayerTime && tt < minLayerTime) { + let fact = tt / minLayerTime; + for (let p of preout) { + if (p.emit) p.speed *= fact; + } + } + // offset print points for (let i=0; i slices.length - topLayers - 1; + let isTopBase = isTop && slice.index === slices.length - topLayers; let isDense = range.sliceFillSparse > 0.995; let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; @@ -505,6 +506,13 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let fillSpace = fillSpacing * spaceMult * solidWidth; doSolidLayerFill(slice, fillSpace, sliceFillAngle); } + if (slice.index === slices.length - 1) { + slice.isFlatsLayer = true; + } + if (isTopBase) { + // mark the first top solid supporting layer as a bridge + slice.isBridgeLayer = true; + } sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "solid layers"); @@ -997,31 +1005,45 @@ function doRender(slice, isSynth, params, opt = {}) { .setLayer('part', Color.part) .addPolys([top.poly]); - output + if (top.shells?.length) output .setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) .addPolys(top.shells || [], vopt({ offset, height, clean: true })); - output + if (isThin && top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(a => a.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z))).flat(), vopt({ offset, height })); + + if (!isThin && top.thin_wall?.length) output + .setLayer("walls", Color.thin) + .addPolys(top.thin_wall.map(a => + a.length === 1 ? + newPolygon().centerCircle(a[0], a[0].r/2, 12) : + newPolygon(a.map(p => newPoint(p.x, p.y))) + .setZ(slice.z) + .closeIf(height) + ), vopt({ offset, height })); + + if (top.fill_lines?.length) output .setLayer("solid fill", isSynth ? Color.support : Color.fill) .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); - if (!(slice.belt && slice.belt.anchor)) output + if (!(slice.belt?.anchor)) output .setLayer("sparse fill", Color.infill) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - if (slice.belt && slice.belt.anchor) output + if (slice.belt?.anchor) output .setLayer("anchor", Color.anchor) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) - if (top.thin_fill) output + if (top.thin_fill?.length) output .setLayer("thin fill", Color.thin) .addLines(top.thin_fill, vopt({ offset, height })); - if (top.gaps && devel) output + if (top.gaps?.length && devel) output .setLayer("gaps", Color.gaps) .addPolys(top.gaps, vopt({ offset, height, thin: true })); - if (isThin && devel && top.fill_off && top.fill_off.length) { + if (isThin && devel && top.fill_off?.length) { slice.output() .setLayer('fill inset', Color.inset) .addPolys(top.fill_off); @@ -1359,8 +1381,9 @@ function addSolidFills(slice, polys) { export function projectFlats(slice, count, expand) { if (!slice.down || !slice.flats) return; // these flats are marked for finishing print speed - if (slice.flats.length) slice.finishSolids = true; - if (slice && slice.flats && slice.flats.length) { + if (slice.flats?.length) { + slice.finishSolids = true; + slice.isFlatsLayer = true; const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; projectSolid(slice, flats, count, false, true); } @@ -1372,7 +1395,10 @@ export function projectFlats(slice, count, expand) { export function projectBridges(slice, count) { if (!slice.up || !slice.bridges) return; // these flats are marked for finishing print speed - if (slice.bridges.length) slice.finishSolids = true; + if (slice.bridges?.length) { + slice.finishSolids = true; + slice.isBridgeLayer = true; + } projectSolid(slice, slice.bridges, count, true, true); }; @@ -1732,6 +1758,9 @@ function fillSupportPolys(args) { * @returns {*} */ function projectSolid(slice, polys, count, up, first) { + if (slice && !up && count === 1) { + slice.isBridgeLayer = true; + } if (!slice || count <= 0) { return; } diff --git a/src/mesh/tool.js b/src/mesh/tool.js index 41db2f8b..bc80aa6a 100644 --- a/src/mesh/tool.js +++ b/src/mesh/tool.js @@ -241,13 +241,12 @@ class MeshTool { }; } -/** - * Retrieves the adjacent faces for a given face index. - * - * @param {number} face - The index of the face for which adjacent faces are to be found. - * @returns {number[]} An array of face indices that are adjacent to the given face. - */ - + /** + * Retrieves the adjacent faces for a given face index. + * + * @param {number} face - The index of the face for which adjacent faces are to be found. + * @returns {number[]} An array of face indices that are adjacent to the given face. + */ getAdjacentFaces(face) { const { faces, sides } = this.getIndex(); const foff = face * 6; @@ -319,31 +318,31 @@ class MeshTool { * 3. Filtering faces with normals perpendicular to Z axis * 4. Recursively adding adjacent faces with the same Z values and normal perpendicular to Z axis */ - findCylinderSurface(face){ - face = Number(face) - - const radianTolerance = 8 * (Math.PI/180); + findCylinderSurface(face) { + face = Number(face); + const radianTolerance = 8 * (Math.PI / 180); const vertexTolerance = 1e-4; - //helper function - const dissimilar = (a,b,c) => Math.abs(a-b) > vertexTolerance && - Math.abs(b-c) > vertexTolerance && - Math.abs(a-c) > vertexTolerance; + + // helper function + const dissimilar = (a, b, c) => Math.abs(a - b) > vertexTolerance && + Math.abs(b - c) > vertexTolerance && + Math.abs(a - c) > vertexTolerance; let found = {}, out = [face], - {vertices} = this, - {faces} = this.getIndex(), - faceOffset = face*9, - zs = [2,5,8].map(i => vertices[faceOffset+i].round(5)), //get zs from face vertices + { vertices } = this, + { faces } = this.getIndex(), + faceOffset = face * 9, + zs = [2, 5, 8].map(i => vertices[faceOffset + i].round(5)), //get zs from face vertices lowZ = Math.min(zs), highZ = Math.max(zs), checked = {}, check = [face]; - - //check for errors, and throw if found - if(dissimilar(...zs)){ + + // check for errors, and throw if found + if (dissimilar(...zs)) { throw `face must have only 2 Z values. found: ${zs.join(", ")}` - }else if(Math.abs( faces[face * 6 + 2] > 1e-6 )){ + } else if (Math.abs(faces[face * 6 + 2] > 1e-6)) { throw "face's normal must be perpendicular to Z axis" } @@ -352,11 +351,12 @@ class MeshTool { const face = check.shift(); const froot = face * 6; const vroot = face * 9 - + // filter faces to those perpendicular to +z axis - if ( Math.abs( faces[froot + 2] > 1e-6 )) continue; - //filter faces with verts don't share the z values of the first face - if( + if (Math.abs(faces[froot + 2] > 1e-6)) continue; + + // filter faces with verts don't share the z values of the first face + if ( !(vertices[vroot + 2] !== lowZ || vertices[vroot + 2] !== highZ) && !(vertices[vroot + 5] !== lowZ || vertices[vroot + 5] !== highZ) && !(vertices[vroot + 8] !== lowZ || vertices[vroot + 8] !== highZ) @@ -369,7 +369,7 @@ class MeshTool { } const aroot = f * 6; let sum = 0; - for (let i=0; i<3; i++) { + for (let i = 0; i < 3; i++) { sum += Math.pow(faces[froot + i] - faces[aroot + i], 2); } const fn = Math.sqrt(sum); @@ -381,6 +381,7 @@ class MeshTool { } } } + return out; } diff --git a/src/moto/license.js b/src/moto/license.js index ae5ed98c..21ca7569 100644 --- a/src/moto/license.js +++ b/src/moto/license.js @@ -3,9 +3,9 @@ const terms = { COPYRIGHT: "Copyright (C) Stewart Allen - All Rights Reserved", LICENSE: "See the license.md file included with the source distribution", - VERSION: "4.3.0" + VERSION: "4.3.1" }; -export const beta = 1; +export const beta = 0; export const license = terms; export const version = terms.VERSION; diff --git a/web/kiri/index.css b/web/kiri/index.css index bb06d38e..5b39165b 100644 --- a/web/kiri/index.css +++ b/web/kiri/index.css @@ -1636,8 +1636,10 @@ details[open] summary::after { background-color: rgba(255,255,255,0.5); position: absolute; z-index: 30; - bottom: -20px; + bottom: calc(-4px - 1em); right: 0; + height: 1em; + aspect-ratio: 1 / 1 !important; } .mod-x svg { aspect-ratio: 1 / 1 !important; diff --git a/web/kiri/index.html b/web/kiri/index.html index 91431e16..a72a261a 100644 --- a/web/kiri/index.html +++ b/web/kiri/index.html @@ -337,6 +337,7 @@
+
diff --git a/web/kiri/lang/en.js b/web/kiri/lang/en.js index fdaa35b3..451bcadb 100644 --- a/web/kiri/lang/en.js +++ b/web/kiri/lang/en.js @@ -332,14 +332,14 @@ self.lang['en-us'] = { se_save: "save", // FDM LAYERS - sl_menu: "paths", + sl_menu: "layers", sl_angl_s: "slice angle", sl_angl_l: ["slice angle bias in degrees"], sl_lahi_s: "layer height", sl_lahi_l: ["height of each slice","layer in millimeters"], ad_minl_s: "layer minimum", ad_minl_l: ["adaptive min layer height","in millimeters","must be non-zero"], - ad_adap_s: "adaptive", + ad_adap_s: "adaptive height", ad_adap_l: ["use adaptive layer heights","with 'layer height' as max","and 'layer min' as the min"], sl_ltop_s: "top layers", sl_ltop_l: ["number of solid layers at the top of the print or on flat areas facing up"], @@ -360,8 +360,17 @@ self.lang['en-us'] = { sl_line_l: ["extrusion width in millimeters","0 = nozzle diameter"], sl_ordr_s: "shell order", sl_ordr_l: ["output shell order","inside to outside","or outside to inside","affects surface quality"], - sl_strt_s: "layer start", - sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + sl_strt_s: "start point", + sl_strt_l: ["layer starting point","last = where last layer ended","center = part center","origin = device origin","random = yolo"], + + // FDM SOLID + fs_menu: "solid fill", + fi_over_s: "shell overlap", + fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], + fi_angl_s: "start angle", + fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], + fi_grow_s: "area expand", + fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], // FDM FILL fi_menu: "sparse fill", @@ -370,12 +379,6 @@ self.lang['en-us'] = { fi_pcnt_l: ["fill density values","0.0 - 1.0"], fi_rept_s: "fill repeat", fi_rept_l: ["number of times to repeat","unchanged fill layers"], - fi_angl_s: "solid angle", - fi_angl_l: ["starting angle in degrees","90 degrees added to ","each following layer","applies only to solid layers"], - fi_grow_s: "solid expand", - fi_grow_l: ["expand projected solids","can help with solid areas","over sparse infill areas","units in millimeters"], - fi_over_s: "fill overlap", - fi_over_l: ["overlap with shell and other fill","for improved extrusion bonding","as fraction of nozzle diameter","0.0 - 2.0"], // fi_rate_s: "print speed", fi_rate_l: ["extrusion speed for infill","set to 0 to use default","output print speeds"], @@ -762,8 +765,10 @@ self.lang['en-us'] = { ou_exml_l: ["extrusion multiplier","0.0 - 2.0"], ou_fanl_s: "fan on layer", ou_fanl_l: ["layer # to turn on fan","layers start at 0"], - ou_fans_s: "fan speed", + ou_fans_s: "fan on speed", ou_fans_l: "0 - 255", + ou_layt_s: "min layer time", + ou_layt_l: ["minimum allowable layer print time in seconds. printing will be slowed if the layer would outerwise complete in fewer seconds.","0 = disabled"], // OUTPUT CAM ou_toll_s: "precision", @@ -845,7 +850,7 @@ self.lang['en-us'] = { // FDM ADVANCED ad_menu: "expert", - ad_rdst_s: "retract dist", + ad_rdst_s: "retract distance", ad_rdst_l: ["amount to retract filament","for long moves. in millimeters"], ad_rrat_s: "retract rate", ad_rrat_l: ["speed of filament","retraction in mm/s"], @@ -855,15 +860,17 @@ self.lang['en-us'] = { ad_rdwl_l: ["time between re-engaging","filament and movement","in milliseconds"], ad_scst_s: "shell coast", ad_scst_l: ["non-printing end","of perimeter shells","in millimeters"], - ad_msol_s: "min solid", + ad_msol_s: "min area", ad_msol_l: ["minimum area (mm^2)","required to keep solid","must be > 0.1"], ad_mins_s: "min speed", ad_mins_l: ["minimum speed","for short segments"], + ad_scar_s: "scarf length", + ad_scar_l: ["ease up length for scarf joint. disables arc detection","0 = disabled"], ad_spol_s: "short path", ad_spol_l: ["polygons shorter than this","will have their print speed","scaled down to min speed","in millimeters"], ad_arct_s: "arc tolerance", - ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","in millimeters","0 to disable"], - ad_zhop_s: "z hop dist", + ad_arct_l: ["convert faceted lines to arcs","center point drift tolerance","when matching arc points","consider values around 0.15","value in millimeters","0 or scarf disables this"], + ad_zhop_s: "z hop distance", ad_zhop_l: ["amount to raise z","on retraction moves","in millimeters","0 to disable"], ad_abkl_s: "anti-backlash", ad_abkl_l: ["for better flat surface finish","use micro-movements to cancel","backlash in solid layer output","in millimeters","0 to disable","if your firmware has M425","put that in the gcode header","and leave this as 0"], @@ -873,8 +880,8 @@ self.lang['en-us'] = { ad_altr_l: ["alternate shell winding order","clockwise / counter-clockwise","may reduce warping in thin areas","and help with belt edge adhesion"], ad_zint_s: "interleave z", ad_zint_l: ["interleave Z heights with odd number shells"], - ad_lret_s: "layer retract", - ad_lret_l: ["force filament retraction","between layers"], + ad_lret_s: "force retract", + ad_lret_l: ["force filament retraction between layers"], ad_agap_s: "avoid gaps", ad_agap_l: ["route around gaps rather","than perform a retract","when possible"], ad_play_s: "polish layers", diff --git a/web/kiri/manifest.json b/web/kiri/manifest.json index ce4522be..051f5f4c 100644 --- a/web/kiri/manifest.json +++ b/web/kiri/manifest.json @@ -1,5 +1,5 @@ { - "name": "Kiri:Moto 4.3.0", + "name": "Kiri:Moto 4.3.1", "short_name": "Kiri:Moto", "description": "Slicer for 3D printers, CNC mills, laser cutters and more", "start_url": "/kiri/",