diff --git a/package.json b/package.json index 32fe787e..a9ee6147 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..806fbd9b 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(); @@ -2042,13 +2054,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; @@ -2318,7 +2326,7 @@ export class Polygon { // split long straight lines into segments no longer than max // and return a new polygon - segment(max = 1) { + segment(max = 1, mark, wrap) { const newp = []; const points = this.points; const length = points.length; @@ -2330,6 +2338,7 @@ export class Polygon { const dy = p2.y - p1.y; const dl = Math.sqrt(dx * dx + dy * dy); newp.push(p1); + if (mark) p1.segment = p1; if (dl < max) { continue; } @@ -2344,10 +2353,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/conf.js b/src/kiri/core/conf.js index d8f38f30..3153c01f 100644 --- a/src/kiri/core/conf.js +++ b/src/kiri/core/conf.js @@ -949,7 +949,9 @@ export const conf = { hidden: { "fdm-base": true, "fdm-cool": true, + "fdm-heat": true, "fdm-fill": true, + "fdm-solid": true, "fdm-supp": true, "fdm-xprt": true }, diff --git a/src/kiri/core/consts.js b/src/kiri/core/consts.js index a332391e..fa73787f 100644 --- a/src/kiri/core/consts.js +++ b/src/kiri/core/consts.js @@ -115,9 +115,10 @@ const LISTS = { ], thin: [ { name: "off" }, - { name: "type 1" }, - { name: "type 2" }, - { name: "type 3" } + { name: "basic" }, + { name: "adaptive" }, + { name: "legacy 1" }, + { name: "legacy 2" } ] }; diff --git a/src/kiri/core/print.js b/src/kiri/core/print.js index d0487b52..c343540b 100644 --- a/src/kiri/core/print.js +++ b/src/kiri/core/print.js @@ -137,8 +137,12 @@ class Print { printSpeed = minSpeed + (printSpeed - minSpeed) * (perimeter / process.outputShortPoly); } + let lpo; poly.forEachPoint((point, pos, points, count) => { - if (first) { + 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); } @@ -158,9 +162,12 @@ 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; + last = lpo = point; }, close, closest.index); this.lastPoly = poly; diff --git a/src/kiri/core/render.js b/src/kiri/core/render.js index 0567d53d..a322958d 100644 --- a/src/kiri/core/render.js +++ b/src/kiri/core/render.js @@ -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(); @@ -210,7 +210,6 @@ async function path(levels, update, opts = {}) { } 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/init-menu.js b/src/kiri/mode/fdm/init-menu.js index bb34b97f..5320c367 100644 --- a/src/kiri/mode/fdm/init-menu.js +++ b/src/kiri/mode/fdm/init-menu.js @@ -82,17 +82,18 @@ 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}), @@ -171,16 +172,15 @@ export function menu() { 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)}), 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..34458f49 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; @@ -69,7 +70,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 +85,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 +104,78 @@ 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 deltSmall && diff < deltaBig) continue; + // merge points marking point offset and skip + let mid = p0.midPointTo(p1); + let inc = p0.distTo2D(mid) / offset1; + moved = p0.moved = p1.moved = inc; + p1.skip = true; + p0.x = p1.x = mid.x; + p0.y = p1.y = mid.y; + } + } + } + if (moved) { + prec0.moved = prec1.moved = true; + } + } + } + + for (let rec of thin_test) { + if (rec.moved) { + let last_seg; + rec.poly.points = rec.points.filter((p,i) => { + let keep = (last_seg !== p.segment || p.moved || p.skip); + last_seg = p.segment; + return keep; + }); + } + } + return { last, gaps }; } @@ -164,31 +241,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 +475,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 +485,173 @@ 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) + // 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 }; } /** @@ -639,21 +695,25 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = { } }); let ret = { last, gaps }; switch (opt.thinType) { - case "type 1": + 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 }); + case "adaptive": + ret = thin_type_3({ z, top, count, offsetN }); break; + 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: opt.thinType === '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..bd676ec2 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'; @@ -805,10 +805,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 +839,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; @@ -1089,6 +1100,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 @@ -1185,10 +1249,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; @@ -1444,6 +1508,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 || []; diff --git a/src/kiri/mode/fdm/slice.js b/src/kiri/mode/fdm/slice.js index cec07284..f0bc89e2 100644 --- a/src/kiri/mode/fdm/slice.js +++ b/src/kiri/mode/fdm/slice.js @@ -498,6 +498,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) { let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; let isBottom = slice.index < bottomLayers; let isTop = topLayers && slice.index > 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..a9b61cf5 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"], @@ -363,6 +363,15 @@ self.lang['en-us'] = { sl_strt_s: "layer start", sl_strt_l: ["layer starting point","last = last layer end","center = part center","origin = device origin"], + // 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", fi_type: "fill type", @@ -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"], @@ -855,7 +858,7 @@ 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"], 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/",