/** Copyright Stewart Allen -- All Rights Reserved */ import '../../../ext/jspoly.js'; import { base } from '../../../geo/base.js'; import { newPolygon, Polygon } from '../../../geo/polygon.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { getRangeParameters } from './driver.js'; import { slicer } from '../../../geo/slicer.js'; import { Point } from '../../../geo/point.js'; slicer.slicePost.FDM = slicePost; /** * may run in minion or worker context. do not create objects * that will not quickly encode in threaded mode. add to existing * data object. return is ignored. */ export function slicePost(data, options) { const { z, lines, groups } = data; const { useAssembly, post_args, zIndexes } = options; const { process, isSynth, vaseMode } = post_args; const { shellOffset, fillOffset, clipOffset } = post_args; data.tops = POLY.nest(groups); if (isSynth) { const process = post_args.process; if (process.sliceSupportGrow > 0) { // expand synth supports here so they can be clipped later data.tops = POLY.expand(data.tops, process.sliceSupportGrow, data.z, []); } // do not shell synth widgets because // they will be clipped against peers later // which requires shelling post-clip // but we still need to generate tops delete data.groups; return; } const index = zIndexes.indexOf(z); const range = getRangeParameters(process, index); // calculate fractional shells let shellFrac = (range.sliceShells - (range.sliceShells | 0)); let sliceShells = range.sliceShells | 0; if (shellFrac) { let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; let v2 = 1 - v1; let parts = Math.round(v2/v1) + 1; let rem = index % parts; let trg = shellFrac > 0.5 ? 1 : parts - 1; sliceShells += rem >= trg ? 1 : 0; } const isFirst = index === 0; const height = process.sliceHeight; const spaceMult = isFirst ? process.firstLayerLineMult || 1 : 1; const count = isSynth ? 1 : sliceShells; const offset = shellOffset * spaceMult; const fillOff = fillOffset * spaceMult; const thinType = isSynth ? undefined : process.sliceDetectThin; const nutops = []; // co-locate shell processing with top generation in slicer for (let top of data.tops) { nutops.push(doTopShells(z, top, count, offset/2, offset, fillOff, { thinType, vaseMode, useAssembly })); } data.clip = clipOffset ? POLY.offset(nutops.map(t => t.simple), clipOffset) : undefined; data.tops = nutops; delete data.groups; }; function offset_default(params) { let { z, top, count, top_poly, offset1, offsetN, wasm, last, gaps } = params; // standard wall offsetting strategy POLY.offset( top_poly, [-offset1, -offsetN], { z, wasm, count, outs: top.shells, flat: true, call: (polys, onCount) => { last = polys; // mark each poly with depth (offset #) starting at 0 for (let p of polys) { p.depth = count - onCount; if (p.fill_off) p.fill_off.forEach(function(pi) { // use negative offset for inners pi.depth = -(count - onCount); }); // mark inner depth to match parent if (p.inner) { for (let pi of p.inner) { pi.depth = p.depth; } } } } } ); return { last, gaps }; } function thin_type_1(params) { let { z, top, count, top_poly, offsetN, last, gaps } = params; top.thin_fill = []; top.fill_sparse = []; let layers = POLY.inset(top_poly, offsetN, count, z, true); last = layers.last().mid; top.shells = layers.map(r => r.mid).flat(); top.gaps = layers.map(r => r.gap).flat(); let off = offsetN; let min = off * 0.75; let max = off * 4; for (let poly of layers.map(r => r.gap).flat()) { let centers = poly.centers(off/2, z, min, max, {lines:false}); top.fill_sparse.appendAll(centers); } return { last, gaps }; } function thin_type_2(params) { let { z, top, count, top_poly, offset1, offsetN, last, gaps, wasm } = params; top.gaps = gaps; top.thin_fill = []; let oso = {z, count, gaps: [], outs: [], minArea: 0.05, wasm}; POLY.offset(top_poly, [-offset1, -offsetN], oso); oso.outs.forEach((polys, i) => { polys.forEach(p => { p.depth = i; if (p.fill_off) { p.fill_off.forEach(pi => pi.depth = i); } if (p.inner) { for (let pi of p.inner) { pi.depth = p.depth; } } top.shells.push(p); }); last = polys; }); // slice.solids.trimmed = slice.solids.trimmed || []; oso.gaps.forEach((polys, i) => { let off = (i == 0 ? offset1 : offsetN); polys = POLY.offset(polys, -off * 0.8, {z, minArea: 0, wasm}); top.thin_fill.appendAll(cullIntersections( fillArea(polys, 45, off/2, [], 0.01, off*2), fillArea(polys, 135, off/2, [], 0.01, off*2), )); gaps.push(...polys); }); return { last, gaps }; } function thin_type_3(params) { // offsetN usually = extrusion width (nozzle diameter) let { z, top, count, offsetN, last, gaps } = params; // produce trace from outside of poly inward no more than max inset let noodleWidth = offsetN * count; let { noodle,} = top.poly.noodle(noodleWidth); // render inset "noodle" // shells.appendAll(noodle); let thin = top.thin_fill = []; let shells = top.shells; let sparse = top.fill_sparse = []; 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 = []; let trace = []; for (let i=0; i { 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(); function pointDist(p0, p1) { return Math.hypot(p0.x-p1.x, p0.y-p1.y); } function point2key(point) { return `${(point.x*100)|0}|${(point.y*100)|0}`; } // convert point to deduplicated / unique point record function pointToRec(point) { let key = point2key(point); let rec = pointMap.get(key); if (!rec) { rec = { key, count: 0, x: point.x/scale, y: point.y/scale, r: point.radius/scale, }; pointMap.set(key, rec); } return rec; } // convert line to unique line record. matches reversed and duplicate lines function pointsToLine(p0, p1) { p0 = pointToRec(p0); p1 = pointToRec(p1); let key = p0.key > p1.key ? `${p1.key}:${p0.key}` : `${p0.key}:${p1.key}`; let rec = lineMap.get(key); if (rec) { rec.count++; } else { rec = { p0, p1, key, count: 1, }; p0.count++; p1.count++; lineMap.set(key, rec); } return rec; } let nusegs = []; // for each standalone noodle, construct medial axis // map medial axis points into de-duplicated segments 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 }); let out = poly.points.map(p => ({ x: p.x|0, y: p.y|0 })); let inr = (poly.inner ?? []).map(p => p.points.map(p => ({ x: p.x|0, y: p.y|0 }))); // 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) for (let { point0, point1 } of ma) { pointsToLine(point0, point1); } } // break down long segments for (let seg of lineMap.values()) { let { p0, p1 } = seg; let len = seg.len = pointDist(p0, p1); // add to nusegs if (len <= minR) { nusegs.push({ p0, p1 }); if (showMedialAxis) { lines.push(new Point(p0.x, p0.y), new Point(p1.x, p1.y)); } } else { let steps = Math.ceil(len / minR) + 1; let dx = (p1.x - p0.x) / steps; let dy = (p1.y - p0.y) / steps; for (let i=0; i { 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 nusegs) { 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; } 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 if (odd && mp1.claimed) len--; if (len === 0) { nupoly.push(new Point(min.x, min.y)); return; } let op; if (pop.length === 1) { nupoly.push(new Point(min.x, min.y)); // for single wall place it exactly on medial axis trace.push(op = { x: min.x, y: min.y, r: mr }) } else { let off = -dstep; // otherwise use divisions for (let i=0; i<1; i++) { off += (pop[i] * (i ? 2 : 1)); trace.push(op = { x: np1.x + dy * off, y: np1.y - dx * off, r: pop[0] }) } off += dstep / 2; nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2)); } if (odd && !mp1.claimed) { mp1.claimed = true; } } }); } // 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() ).filter(p => p.area() >= minArea); remain.push(...ret); } return { lines, polys, remain, trace }; } /** * may run in minion or worker context. performs shell offsetting * including thin wall detection when enabled */ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { // pretend we're a top object in minions if (!top.poly) { top = { poly: top }; } // add simple (low rez poly) where less accuracy is OK top.simple = top.poly.simple(); let wasm = opt.useAssembly; let top_poly = [ top.poly ]; if (opt.vaseMode) { // remove top poly inners in vase mode top.poly = top.poly.clone(false); } top.shells = []; // strategies output shells here top.fill_off = []; // offset from innermost shell for infill top.fill_lines = []; // fill lines inside fill_off let last = [], gaps = []; if (count) { // permit offset of 0 for laser and drag knife if (offset1 === 0 && count === 1) { last = top_poly.clone(true); top.shells = last; } else { // heal top open polygons if the ends are close (benchy tilt test) top_poly.forEach(p => { if (p.open) { let dist = p.first().distTo2D(p.last()); if (dist < 1) p.open = false; } }); let ret = { last, gaps }; switch (opt.thinType) { case "type 1": ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset }); break; case "type 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; default: ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm }); break; } last = ret.last || last; gaps = ret.gaps || gaps; } } else { // no shells, just infill, is permitted last = [top.poly]; } // generate fill offset poly set from last offset to top.fill_off if (fillOffset && last.length > 0) { // if gaps present, remove that area from fill inset if (gaps.length) { let nulast = []; POLY.subtract(last, gaps, nulast, null, z); last = nulast; } last.forEach(function(inner) { POLY.offset([inner], -fillOffset, {outs: top.fill_off, flat: true, z}); }); } // for diffing top.last = last; // top.last_simple = last.map(p => p.clean(true, undefined, config.clipper / 10)); return top; } /** * given an array of arrays of points (lines), eliminate intersections * between groups, then return a unified array of shortest non-intersects. */ function cullIntersections() { function toLines(pts) { let lns = []; for (let i=0, il=pts.length; i t); if (aOa.length < 1) return; let aa = toLines(aOa.shift()); while (aOa.length) { let bb = toLines(aOa.shift()); loop: for (let i=0, il=aa.length; i !l.del).concat(bb.filter(l => !l.del)); } let good = []; for (let i=0, il=aa.length; i 2 ? good : []; }