/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; // dep: geo.base // dep: geo.slicer // dep: geo.polygons // dep: kiri.utils // dep: kiri.consts // dep: kiri-mode.fdm.driver // dep: kiri-mode.fdm.post // use: kiri-mode.fdm.fill // use: ext.clip2 // use: add.three gapp.register("kiri-mode.fdm.slice", [], (root, exports) => { const { base, kiri, noop } = root; const { consts, driver, fill, fill_fixed, newSlice, utils } = kiri; const { config, polygons, util, newPoint } = base; const { fillArea } = polygons; const { FDM } = driver; const { doTopShells, getRangeParameters } = FDM; const POLY = polygons, tracker = util.pwait, lopacity = 0.6, opacity = 1, fat = 1.5, COLOR = { anchor: { check: 0x999933, face: 0x999933, line: 0x999933, opacity, lopacity, fat }, shell: { check: 0x0077bb, face: 0x0077bb, line: 0x0077bb, opacity, lopacity, fat }, fill: { check: 0x00bb77, face: 0x00bb77, line: 0x00bb77, opacity, lopacity, fat }, infill: { check: 0x3322bb, face: 0x3322bb, line: 0x3322bb, opacity, lopacity, fat }, support: { check: 0xaa5533, face: 0xaa5533, line: 0xaa5533, opacity, lopacity, fat }, gaps: { check: 0xaa3366, face: 0xaa3366, line: 0xaa3366, opacity, lopacity, fat } }, PROTO = Object.clone(COLOR), profile = false, profileStart = profile ? console.profile : noop, profileEnd = profile ? console.profileEnd : noop, debug = false; let isThin = false, // force line rendering isFlat = false, // force flat rendering offset = 0; // poly line generation offsets function vopt(opt) { if (opt) { if (isFlat) { opt.flat = true; opt.outline = true; return opt; } if (isThin) return null; } return opt; } FDM.sliceAll = function(settings, onupdate) { // future home of brim and anchor generation let widgets = Object.values(kiri.worker.cache) .filter(w => !w.meta.disabled) .sort((a,b) => { return a.slices[0].z - b.slices[0].z }); // assign grid_id which can be embedded in gcode and // used by the controller to cancel objects during print let { bounds } = settings; for (let widget of widgets) { let { pos, box } = widget.track; // calculate top/left coordinate for widget // relative to bounding box for all widgets let tl = { x: Math.round((pos.x - box.w/2 - bounds.min.x) / 10) + 1, y: Math.round((pos.y - box.h/2 - bounds.min.y) / 10) + 1 }; widget.track.grid_id = tl.x * 100 + tl.y; } // count extruders used let ext = []; for (let w of widgets) { if (w.anno && w.anno.extruder >= 0) { let e = w.anno.extruder; if (ext.indexOf(e) < 0) { ext.push(e); } } } // sort widgets by first slice Z widgets.sort((a,b) => { return a.slices[0].z - b.slices[0].z; }); // give first widget a pass since it should have the anchor widgets.shift(); // remove anchor slices from other widgets (only with multi-material) if (ext.length > 1) { for (let w of widgets) { w.slices = w.slices.filter(s => s.index >= 0); } } }; /** * DRIVER SLICE CONTRACT * * Given a widget and settings object, call functions necessary to produce * slices and then the computations using those slices. This function is * designed to run client or server-side and provides all output via * callback functions. * * @param {Object} settings * @param {Widget} Widget * @param {Function} onupdate (called with % complete and optional message) * @param {Function} ondone (called when complete with an array of Slice objects) */ FDM.slice = function(settings, widget, onupdate, ondone) { let render = settings.render !== false, { process, device, controller } = settings, isBelt = device.bedBelt, isBrick = controller.devel && process.sliceZInterleave, isSynth = widget.track.synth, isSupport = widget.track.support, useAssembly = controller.assembly, isConcurrent = controller.threaded && kiri.minions.concurrent, topLayers = process.sliceTopLayers || 0, bottomLayers = process.sliceBottomLayers || 0, vaseMode = process.sliceFillType === 'vase' && !isSynth, metadata = widget.anno, maxtruder = Math.max(0, device.extruders.length - 1), extruder = Math.min(maxtruder, parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0)), sliceHeight = process.sliceHeight, sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle, fillOffsetMult = 1.0 - bound(process.sliceFillOverlap, 0, 0.8), shellOffset = lineWidth, fillSpacing = lineWidth, fillOffset = lineWidth * fillOffsetMult, clipOffset = process.sliceSupportOffset, sliceFillAngle = process.sliceFillAngle, supportDensity = process.sliceSupportDensity; // override globals used by vopt() isFlat = controller.lineType === "flat"; isThin = !isFlat && controller.lineType === "line"; offset = lineWidth / 2; // allow overriding support fill auto angle algorithm // also causes support fill to be aligned on start boundaries // best with angles that are a multiple of 90 degrees if (process.sliceSupportFill >= 0) { // yes, an ugly hack to allow it to pass through old code paths process.sliceSupportFill += 1000; } if (isFlat) { Object.values(COLOR).forEach(color => { color.flat = true; color.line = 1 color.opacity = 1; }); } else { Object.keys(COLOR).forEach(key => { const color = COLOR[key]; const proto = PROTO[key] color.flat = proto.flat; color.line = proto.line; color.opacity = proto.opacity; }); } if (!(sliceHeight > 0 && sliceHeight < 100)) { return ondone("invalid slice height"); } if (!(lineWidth >= 0.01 && lineWidth < 100)) { return ondone("invalid nozzle size"); } const sliceMinHeight = process.sliceAdaptive && process.sliceMinHeight > 0 ? Math.min(process.sliceMinHeight, sliceHeight) : 0; if (sliceHeightBase <= 0) { console.log("invalid first layer height < slice height"); console.log("reverting to min valid slice height"); sliceHeightBase = sliceMinHeight || sliceHeight; } let bounds = widget.getBoundingBox(); let points = widget.getPoints(); let indices = []; let heights = []; let healed = false; // handle z cutting (floor method) and base flattening let zPress = isBelt ? process.firstLayerFlatten || 0 : 0; let zCut = widget.track.zcut || 0; let { belt } = widget; if (zCut || zPress) { for (let p of points) { if (!p._z) { p._z = p.z; if (zPress) { if (isBelt) { let zd = (belt.slope * p.z) - p.y; if (zd > 0 && zd <= zPress) { p.y += zd * belt.cosf; p.z -= zd * belt.sinf; } } else { if (p.z <= zPress) p.z = 0; } } if (zCut && !isBelt) { p.z -= zCut; } } } } base.slice(points, { debug: process.xray, xray: process.xray, zMin: bounds.min.z, zMax: bounds.max.z - zCut, // support/synth usually has overlapping boxes union: controller.healMesh || isSynth, indices: process.indices || process.xray, useAssembly, post: 'FDM', post_args: { shellOffset, fillOffset, clipOffset, lineWidth, vaseMode, isSynth, process, }, // z index generator zGen(zopt) { if (process.xray) { return zopt.zIndexes; } let { zMin, zMax } = zopt; let h1 = sliceHeight; let h0 = sliceHeightBase || h1; let hm = sliceMinHeight || 0; let h = h0; let z = h0; let zi = indices; // indices let zh = heights; // heights if (hm) { // adaptive increments based on z indices (var map to legacy code) let zIncFirst = h0; let zInc = h1; let zIncMin = hm; let zHeights = heights; let zIndexes = indices; let zOrdered = Object.values(zopt.zIndexes).map(v => parseFloat(v)); // console.log('adaptive slicing', zIncMin, ':', zInc, 'from', zMin, 'to', zMax); let zPos = zIncFirst, zOI = 0, zDelta, zDivMin, zDivMax, zStep, nextZ, lzp = zPos; // adaptive slice height // first slice/height is fixed from base zHeights.push(zIncFirst); zIndexes.push(zIncFirst); // console.log({zIncFirst, zOrdered}) while (zPos < zMax && zOI < zOrdered.length) { nextZ = zOrdered[zOI++]; if (zPos >= nextZ) { // console.log('skip',{zPos},'>=',{nextZ}); continue; } zDelta = nextZ - zPos; if (zDelta < zIncMin) { // console.log('skip',{zDelta},'<',{zIncMin}); continue; } zDivMin = Math.floor(zDelta / zIncMin); zDivMax = Math.floor(zDelta / zInc); if (zDivMax && zDivMax <= zDivMin) { if (zDelta % zInc > 0.01) zDivMax++; zStep = zDelta / zDivMax; // console.log(`--- zDivMax <= zDivMin ---`, zStep, zDelta % zInc) } else { zStep = zDelta; } // console.log({nextZ, zPos, zDelta, zStep, zDivMin, zDivMax}) while (zPos < nextZ) { zHeights.push(zStep); zIndexes.push(zPos + zStep); zPos += zStep; // console.log({D: zPos - lzp, zPos}) // lzp = zPos; } } // console.log({zIndexes, zHeights}); } else { // simple based + fixed increment while (true) { // reduce slice position by half layer height let realz = (z - (h / 2)).round(3); if (realz > zMax) { break; } zh.push(h); zi.push(realz); h = h1; z += h; } } return zi; }, // slicer function (worker local or minion distributed) slicer(z, points, opts) { // opts.debug = opts.debug || isSynth; return (isConcurrent ? kiri.minions.sliceZ : base.sliceZ)(z, points, opts); }, onupdate(update) { return onupdate(0.0 + update * 0.5) } }).then((output) => { // post process slices and re-incorporate missing meta-data return output.slices.map(data => { let { z, clip, lines, groups, changes } = data; if (!data.tops) return null; let slice = newSlice(z).addTops(data.tops); slice.index = indices.indexOf(z); slice.height = heights[slice.index]; slice.clips = clip; // do not warn on merging supports if (changes && !isSynth) { healed = true; slice.changes = changes; if (self.debug) { console.log('slice healed', slice.index, slice.z, changes); } } if (process.xray) { slice.index = process.xrayi.shift(); slice.lines = lines; slice.groups = groups; slice.xray = process.xray; } return slice; }).filter(s => s); }).then(slices => { return onSliceDone(slices); }).then(ondone); // shadow used to clip supports in non-belt mode async function doShadow(slices) { if (widget.shadow) { return; } let root = widget.group[0]; if (root.shadow) { widget.shadow = root.shadow; return; } // console.log({ doShadow: widget, slices }); // create shadow for clipping supports let alltops = widget.group .filter(w => !w.track.synth) // no supports in shadow .map(w => w.slices).flat() .map(s => s.tops).flat().map(t => t.simple); let shadow = isConcurrent ? await kiri.minions.union(alltops, 0.1) : POLY.union(alltops, 0.1, true); // expand shadow when requested (support clipping) if (process.sliceSupportExtra) { shadow = POLY.offset(shadow, process.sliceSupportExtra); } widget.shadow = root.shadow = POLY.setZ(shadow, 0); // slices[0].output() // .setLayer('shadow', { line: 0xff0000, check: 0xff0000 }) // .addPolys(shadow); } async function onSliceDone(slices) { // alert non-manifold parts if (healed) { onupdate(null, null, "part may not be manifold"); } // remove all empty slices above part but leave below // for multi-part (multi-extruder) setups where the void is ok // also reverse because slicing occurs bottom-up let found = false; slices = slices.reverse().filter(slice => { if (slice.tops.length) { return found = true; } else { return found; } }).reverse(); // connect slices into linked list for island/bridge projections for (let i=1; i 0.1) { continue; } let clipto = pslice.clips; if (pslice.supportOutline) { // merge support outlines into slice clips which // should only happen when automatic and manual // supports are used together clipto.appendAll(pslice.supportOutline); } let ntops = []; POLY.subtract(tops, clipto, ntops, null, slice.z, 0); tops = ntops; } // trim to group's shadow if not in belt mode if (!isBelt) { tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z); } } slice.tops = []; for (let t of tops) { slice.addTop(t); } doShells(slice, 1, shellOffset / 2); } } // calculate % complete and call onupdate() function doupdate(index, from, to, msg) { trackupdate(index / slices.length, from, to, msg); } function trackupdate(pct, from, to, msg) { onupdate(0.5 + (from + (pct * (to - from))) * 0.5, msg); } // for each slice, performe a function and call doupdate() function forSlices(from, to, fn, msg) { slices.forEach(slice => { fn(slice); doupdate(slice.index, from, to, msg) }); } // do not hint polygon fill longer than a max span length config.hint_len_max = util.sqr(process.sliceBridgeMax); // reset for solids, support projections // and other annotations slices.forEach(slice => { slice.widget = widget; slice.extruder = extruder; slice.solids = []; }); // just the top/bottom special solid layers or range defined solid layers forSlices(0.15, 0.2, slice => { let range = slice.params; let spaceMult = slice.index === 0 ? process.firstLayerLineMult || 1 : 1; let isBottom = slice.index < bottomLayers; let isTop = topLayers && slice.index > slices.length - topLayers - 1; let isDense = range.sliceFillSparse > 0.995; let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; if (solidWidth) { let fillSpace = fillSpacing * spaceMult * solidWidth; doSolidLayerFill(slice, fillSpace, sliceFillAngle); } sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "solid layers"); // add lead in anchor when specified in belt mode (but not for synths) if (isBelt && !isSynth) { let { cosf, slope } = widget.belt; // find adjusted zero point from slices let smin = Infinity; for (let slice of slices) { let miny = Infinity; for (let poly of slice.topPolys()) { let y = poly.bounds.maxy; let z = slice.z; // at 45 degrees, 1mm in Z is 1mm in Y let by = (slope * z) - y; if (by < miny) miny = by; if (by < smin) smin = by; } // mark slices with tops touching belt slice.belt = { miny, touch: miny.round(3) < sliceHeightBase }; } // find max width of first 5 layers for brim additions let start; let minx = Infinity, maxx = -Infinity; let peek = 0; for (let slice of slices) { if (slice.tops.length && peek++ < 5) { for (let poly of slice.topPolys()) { minx = Math.min(minx, poly.bounds.minx); maxx = Math.max(maxx, poly.bounds.maxx); } } // find first slice touching belt for start of anchor if (!start && slice.belt.touch) { start = slice; } } // ensure we start against a layer with shells while (start && start.up && start.topShells().length === 0) { start = start.up; } // if a brim applies, add that width to anchor let brim = getRangeParameters(process, 0).firstLayerBrim || 0; if (brim) { minx -= brim; maxx += brim; } // array of added top.fill_sparse arrays let adds = []; let step = sliceHeight; let anchorlen = (process.beltAnchor || process.firstLayerBeltLead) * cosf; while (anchorlen && start && anchorlen >= sliceHeight) { let addto = start.down; if (!addto) { addto = newSlice(start.z - step); addto.extruder = extruder; addto.belt = { }; addto.height = start.height; addto.up = start; start.down = addto; slices.splice(0,0,addto); } else if (!addto.belt) { console.log({addto_missing_belt: addto}); addto.belt = {}; } addto.index = -1; addto.belt.anchor = true; // this allows the anchor to print bi-directionally // by removing the forced start-point in print.js addto.belt.touch = false; let z = addto.z; let y = (slope * z) - smin - (lineWidth / 2); let splat = base.newPolygon().add(minx, y, z).add(maxx, y, z).setOpen(); let snew = addto.addTop(splat).fill_sparse = [ splat ]; adds.push(snew); start = addto; anchorlen -= (step * slope); } // add anchor bump let bump = process.firstLayerBeltBump; if (bump) { adds = adds.reverse().slice(1, adds.length - 1); let count = 1; for (let add of adds) { let poly = add[0]; let y = count++ * -start.height * 2; if (-y > bump) { count--; // break; } let first = poly.first(); // add up/over/down to anchor line (close = down) // which completes the bump perimeter poly.push(poly.last().add({x:0, y, z:0})); poly.push(poly.first().add({x:0, y, z:0})); poly.setClosed(); if (count > 2 && maxx - minx > 10) { // add vertical hatch lines insibe bump shell let mp = (maxx + minx) / 2; let dx = (maxx - minx - 2); dx = (Math.floor(dx / 3) * 3) / 2; let fy = first.y; let fz = first.z; let n2 = lineWidth / 2; for (let x = mp - dx; x <= mp + dx ; x += 3) { add.push( base.newPolygon().add(x, fy - n2, fz).add(x, fy + y + n2, fz).setOpen() ); } } } } // experimental emboss text on a flat underside of an object // in belt mode only if (process.pooch && self.OffscreenCanvas) { const { length, width, height, text } = process.pooch; let firstZ, firstI, lastZ, lastI, minX = 0, maxX = 0, maxY = 0; // locate suitable flat spot for (let slice of slices) { let { belt } = slice; if (!belt.touch) { continue; } let index = slice.index; let z = slice.z; outer: for (let poly of slice.topPolys()) { for (let i=0, p=poly.points, l=p.length; i 0 && p0y < 3 && Math.abs(p0y - p1y) < 0.01; let x_ok = Math.abs(width - Math.abs(p1.x - p0.x)) < 1 if (y_ok && x_ok) { if (i_ok) { firstZ = firstZ || z; firstI = firstI || index; lastZ = z; lastI = index; minX = Math.min(p0.x, p1.x); maxX = Math.max(p0.x, p1.x); maxY = p0y; break outer; } else { firstZ = lastI = 0; } } } } } let dy = Math.abs(height - maxY * (1 / Math.sqrt(2))); let dz = Math.abs(length - ((lastZ - firstZ) * Math.sqrt(2))); if (dy < 0.1 && dz < 1) { // console.log('FOUND', { firstI, lastI, minX, maxX, maxY }); let span = lastI - firstI - 2; // x = down the belt let tall = width * 2; let can = new self.OffscreenCanvas(span, tall); let ctx = can.getContext("2d"); ctx.scale(1.2, 1); ctx.font = '24px sans-serif'; // ctx.fillStyle = 'black'; ctx.textBaseline = "bottom"; ctx.fillText(text, 1, tall - 1); let img = ctx.getImageData(0, 0, span, tall).data.buffer; let rgb = new Uint32Array(img); // console.log({ img, rgb, p: rgb.filter(v => v) }); for (let x=0; x=0; y--) { let pix = rgb[y * span + x]; pix = ( ((pix >> 24) & 0xff) + ((pix >> 16) & 0xff) + ((pix >> 8) & 0xff) ) / 3; str += pix > 30 ? '*' : '-'; maxp = Math.max(maxp, pix); if (pix > 30) { if (start >= 0) { end = tall - y; } else { start = tall - y; } } else { if (start >= 0 && end > start) { lines.push({ start, end }); } start = end = undefined; } } console.log((x).toString().padStart(2,0),str,maxp | 0,lines); if (lines.length) { let slice = slices[firstI + x + 1]; let supps = slice.supports = slice.supports || []; let z = slice.z; let y = z - smin - (lineWidth / 2); for (let line of lines) { supps.push(base.newPolygon() .add(minX + line.start / 2, y, z) .add(minX + line.end / 2, y, z) .setOpen() ); } } } } } } // calculations only relevant when solid layers are used // layer boolean diffs need to be computed to find flat areas to fill // and overhangs that need to be supported. these are stored in flats // and bridges, projected up/down, and merged into an array of solids if (!vaseMode && !isSynth) { profileStart("delta"); forSlices(0.2, 0.34, slice => { let params = slice.params || process; let solidMinArea = params.sliceSolidMinArea; let sliceFillGrow = params.sliceFillGrow; doDiff(slice, { min: solidMinArea, grow: sliceFillGrow }); }, "layer deltas"); profileEnd(); profileStart("delta-project"); forSlices(0.34, 0.35, slice => { let params = slice.params || process; topLayers = params.sliceTopLayers || 0; bottomLayers = params.sliceBottomLayers || 0; if (topLayers) projectFlats(slice, topLayers); if (bottomLayers) projectBridges(slice, bottomLayers); }, "layer deltas"); profileEnd(); profileStart("solid-fill") let promises = isConcurrent ? [] : undefined; forSlices(0.35, promises ? 0.4 : 0.5, slice => { let params = slice.params || process; let first = slice.index === 0; let solidWidth = params.sliceFillWidth || 1; let spaceMult = first ? params.firstLayerLineMult || 1 : 1; let fillSpace = fillSpacing * spaceMult * solidWidth; let solidMinArea = params.sliceSolidMinArea; doSolidsFill(slice, fillSpace, sliceFillAngle, solidMinArea, promises); sliceFillAngle = (sliceFillAngle + 90.0) % 360; }, "fill solids"); // very last layer (top) is set to finish solid rate slices.last().finishSolids = true if (promises) { await tracker(promises, (i, t) => { trackupdate(i / t, 0.4, 0.5); }); } profileEnd(); } // for "real" objects, fill the remaining voids with sparse fill // sparse layers only present when non-vase mose and sparse % > 0 if (!isSynth && !vaseMode) { let lastType; let promises = isConcurrent ? [] : undefined; forSlices(0.5, promises ? 0.55 : 0.7, slice => { let params = slice.params || process; if (!params.sliceFillSparse) { return; } let newType = params.sliceFillType; doSparseLayerFill(slice, { settings, process, device, lineWidth, spacing: fillOffset, density: params.sliceFillSparse, bounds: widget.getBoundingBox(), height: sliceHeight, type: newType, cache: params._range !== true && lastType === newType, promises }); lastType = newType; }, "infill"); if (promises) { await tracker(promises, (i, t) => { trackupdate(i / t, 0.55, 0.7); }); } // back-fill slices marked for infill cloning for (let slice of slices) { if (slice._clone_sparse) { let tops = slice.tops; let down = slice.down.tops; for (let i=0; i p.cloneZ(slice.z)); } } } } else if (isSynth && isSupport) { // convert synth support widgets into support structure let outline = process.sliceSupportOutline || false; let promises = isConcurrent ? [] : undefined; let resolve = []; forSlices(0.5, promises ? 0.6 : 0.7, slice => { let params = slice.params || process; let density = params.sliceSupportDensity; let supports = slice.topShells(); slice.supports = supports; slice.tops = undefined; // remove outline leave only supports let polys = []; if (density) { if (!outline) { POLY.expand(supports, lineWidth, slice.z, polys); } else { POLY.expand(supports, -lineWidth/4, slice.z, polys); } } fillSupportPolys({ promises, polys, lineWidth, density, z: slice.z, isBelt, angle: process.sliceSupportFill }); resolve.push({ slice, polys }); }, "infill"); if (promises) { await tracker(promises, (i, t) => { trackupdate(i / t, 0.6, 0.7); }); } for (let rec of resolve) { let { slice, polys } = rec; rec.supports = polys; } } // auto support generation if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) { doShadow(slices); profileStart("support"); let promises = []; forSlices(0.7, 0.75, slice => { promises.push(doSupport(slice, process, widget.shadow, { })); }, "support"); await tracker(promises, (i, t) => { trackupdate(i / t, 0.75, 0.8); }); profileEnd(); } // fill all supports (auto and manual) // if (!isBelt && supportDensity) { if (supportDensity) { profileStart("support-fill"); let promises = false && isConcurrent ? [] : undefined; forSlices(0.8, promises ? 0.88 : 0.9, slice => { let params = slice.params || process; let density = params.sliceSupportDensity; doSupportFill({ promises, slice, lineWidth, density, minArea: process.sliceSupportArea, isBelt, angle: process.sliceSupportFill, outline: process.sliceSupportOutline !== false }); }, "support"); if (promises) { await tracker(promises, (i, t) => { trackupdate(i / t, 0.88, 0.9); }); } profileEnd(); } if (isBrick) { let indices = slices.map(s => s.index); let first = indices[1]; let last = indices[indices.length - 2]; let nu = []; for (let slice of slices) { if (slice.index < first || slice.index > last) { continue; } let nuSlice = slice.clone(); nuSlice.z -= slice.height / 2; if (slice.index === first) { nuSlice.z = slice.z - slice.height / 4; nuSlice.height = slice.height / 2; } else { nuSlice.height = slice.height; } nu.push(nuSlice); let ti = 0; for (let top of slice.tops) { let nuTop = nuSlice.tops[ti++]; nuTop.shells = []; top.shells = top.shells.filter((s,i) => { if (i % 2 === 0) { return true; } else { nuTop.shells.push(s); return false; } }); } if (slice.index === last) { let cap = nuSlice.clone(); cap.z += (slice.height * 0.75); cap.height = (slice.height / 2); nu.push(cap); cap.tops.forEach((top, i) => { top.shells = nuSlice.tops[i].shells.clone(); }); } } slices.appendAll(nu); slices.sort((a,b) => a.z - b.z); slices.forEach((s,i) => s.index = i); } // render if not explicitly disabled if (render) { forSlices(0.9, 1.0, slice => { let params = slice.params || process; doRender(slice, isSynth, params, controller.devel); }, "render"); } if (isBelt) { let bounds = base.newBounds(); let slice = slices[0]; if (slice.tops) { for (let top of slice.tops) { bounds.merge(top.poly.bounds); } } else if (slice.supports) { for (let poly of slice.supports) { bounds.merge(poly); } } widget.belt.miny = -bounds.miny; widget.belt.midy = (bounds.miny + bounds.maxy) / 2; } } } function connect_lines(lines, maxd = Infinity) { const newlines = []; let op2; let eo = 0; for (let i=0; i { if (isThin) output .setLayer('part', { line: 0x333333, check: 0x333333 }) .addPolys([top.poly]); output .setLayer(isSynth ? "support" : "shells", isSynth ? COLOR.support : COLOR.shell) .addPolys(top.shells || [], vopt({ offset, height, clean: true })); 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 .setLayer("sparse fill", COLOR.infill) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) if (slice.belt && slice.belt.anchor) output .setLayer("anchor", COLOR.anchor) .addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true })) if (top.thin_fill) output .setLayer("thin fill", COLOR.fill) .addLines(top.thin_fill, vopt({ offset, height })); if (top.gaps) output .setLayer("gaps", COLOR.gaps) .addPolys(top.gaps, vopt({ offset, height, thin: true })); if (isThin && devel && top.fill_off && top.fill_off.length) { slice.output() .setLayer('fill inset', { face: 0, line: 0xaaaaaa, check: 0xaaaaaa }) .addPolys(top.fill_off); // .setLayer('last', { face: 0, line: 0x008888, check: 0x008888 }) // .addPolys(top.last); } }); if (isThin && devel) { if (slice.solids && slice.solids.length) output .setLayer("solids", { face: 0xbbbb00, check: 0xbbbb00 }) .addAreas(slice.solids); if (slice.bridges && slice.bridges.length) output .setLayer("bridges", { face: 0x00cccc, line: 0x00cccc, check: 0x00cccc }) .addAreas(slice.bridges); if (slice.flats && slice.flats.length) output .setLayer("flats", { face: 0xaa00aa, line: 0xaa00aa, check: 0xaa00aa }) .addAreas(slice.flats); } if (slice.supports && params.sliceSupportOutline) output .setLayer("support", COLOR.support) .addPolys(slice.supports, vopt({ offset, height })); if (slice.supports) slice.supports.forEach(poly => { if (poly.fill) output .setLayer("support", COLOR.support) .addLines(poly.fill, vopt({ offset, height })); }); if (slice.xray) { const color = [ 0xff0000, 0x00aa00, 0x0000ff, 0xaaaa00, 0xff00ff, 0x0 ]; if (slice.lines) { slice.lines.forEach((line, i) => { const group = i % 5; slice.output().setLayer(`l${group}`, color[group]).addLine(line.p1, line.p2); }); } if (slice.groups) POLY.nest(slice.groups).forEach((poly, i) => { const group = i % 5; slice.addTop(poly); // slice.output().setLayer(`g${i}`, 0x888888).addPoly(poly); slice.output().setLayer(`g${i}`, color[group]).addPoly(poly); }); } // console.log(slice.index, slice.render.stats); } // shared with SLA driver and minions FDM.share = { doShells, doTopShells, doDiff, projectFlats, projectBridges }; /** * Compute offset shell polygons. For FDM, the first offset is usually half * of the nozzle width. Each subsequent offset is a full nozzle width. User * parameters control tweaks to these numbers to allow for better shell bonding. * The last shell generated is a "fillOffset" shell. Fill lines are clipped to * this polygon. Adjusting fillOffset controls bonding of infill to the shells. * * Most of this is done in slicePost() in FDM mode. now this is used by SLA, Laser * * @param {number} count * @param {number} offsetN * @param {number} fillOffset * @param {Obejct} options */ function doShells(slice, count, offset1, offsetN, fillOffset, opt = {}) { for (let top of slice.tops) { doTopShells(slice.z, top, count, offset1, offsetN, fillOffset, opt); } } /** * Create an entirely solid layer by filling all top polygons * with an alternating pattern. * * @param {number} linewidth * @param {number} angle * @param {number} density */ function doSolidLayerFill(slice, spacing, angle) { if (slice.xray) { return; } if (slice.tops.length === 0 || typeof(angle) != 'number') { slice.isSolidLayer = false; return; } slice.tops.forEach(function(top) { if (!top.fill_off) return; // missing for inner brick layers let lines = fillArea(top.fill_off, angle, spacing, null); top.fill_lines.appendAll(lines); }); slice.isSolidLayer = true; }; /** * Take output from pluggable sparse infill algorithm and clip to * the bounds of the top polygons and their inner solid areas. */ function doSparseLayerFill(slice, options = {}) { if (slice.xray) { return; } let process = options.process, spacing = options.spacing, // spacing space between fill lines density = options.density, // density of infill 0.0 - 1.0 bounds = options.bounds, // bounding box of widget height = options.height, // z layer height cache = !(options.cache === false), type = options.type || 'hex'; if (slice.tops.length === 0 || density === 0.0 || slice.isSolidLayer || slice.index < 0) { slice.isSparseFill = false; return; } let tops = slice.tops, down = slice.down, clib = self.ClipperLib, ctyp = clib.ClipType, ptyp = clib.PolyType, cfil = clib.PolyFillType, clip = new clib.Clipper(), ctre = new clib.PolyTree(), poly, polys = [], lines = [], line = [], solids = [], // callback passed to pluggable infill algorithm target = { // slice and slice property access slice: function() { return slice }, zIndex: function() { return slice.index }, zValue: function() { return slice.z }, // various option map access options: function() { return options }, lineWidth: function() { return options.lineWidth }, bounds: function() { return bounds }, zHeight: function() { return height }, offset: function() { return spacing }, density: function() { return density }, repeat: function() { return process.sliceFillRepeat }, // output functions emit: function(x,y) { if (isNaN(x)) { solids.push(x); } else { line.push(newPoint(x, y, slice.z)); slice.isSparseFill = true; } }, newline: function() { if (line.length > 0) { lines.push(line); line = []; } } }; // use specified fill type if (type && fill[type]) { fill[type](target); } else { console.log({missing_infill: type}); return; } // force emit of last line target.newline(); // prepare top infill structure for (let top of tops) { top.fill_sparse = top.fill_sparse || []; polys.appendAll(top.fill_off); polys.appendAll(top.solids); } // update fill fingerprint for this slice slice._fill_finger = POLY.fingerprint(polys); let skippable = cache && fill_fixed[type] ? true : false; let miss = false; // if the layer below has the same fingerprint, // we may be able to clone the infill instead of regenerating it if (skippable && slice.fingerprintSame(down)) { // the fill fingerprint can slightly different because of solid projections if (down._fill_finger && POLY.fingerprintCompare(slice._fill_finger, down._fill_finger)) { for (let i=0; i { top.fill_lines.push(p1, p2); }); } } if (fillable.length) { let lines = POLY.fillArea(fillable, angl, options.lineWidth); top.fill_lines.appendAll(lines); } } } } // if only solids were added and no lines to clip if (!sparse_clip) { return; } if (options.promises) { options.promises.push(kiri.minions.clip(slice, polys, lines)); return; } lines = lines.map(a => a.map(p => p.toClipper())); clip.AddPaths(lines, ptyp.ptSubject, false); clip.AddPaths(POLY.toClipper(polys), ptyp.ptClip, true); if (clip.Execute(ctyp.ctIntersection, ctre, cfil.pftNonZero, cfil.pftEvenOdd)) { for (let node of ctre.m_AllPolys) { poly = POLY.fromClipperNode(node, slice.z); for (let top of tops) { // use only polygons inside this top if (poly.isInside(top.poly)) { top.fill_sparse.push(poly); } } } } }; /** * Find difference between fill inset poly on two adjacent layers. * Used to calculate bridges, flats and then solid projections. * 'expand' is used for top offsets in SLA mode */ function doDiff(slice, options = {}) { const { sla, fakedown, grow, min } = options; if ((slice.index <= 0 && !fakedown) || slice.xray) { return; } const top = slice, down = slice.down || (fakedown ? newSlice(-1) : null), topInner = sla ? top.topPolys() : top.topInners(), downInner = sla ? down.topPolys() : down.topInners(), bridges = top.bridges = [], flats = down.flats = []; // skip diffing layers that are identical if (slice.fingerprintSame(down)) { top.bridges = bridges; down.flats = flats; return; } let newBridges = []; let newFlats = []; POLY.subtract(topInner, downInner, newBridges, newFlats, slice.z, min, { wasm: true }); newBridges = newBridges.filter(p => p.areaDeep() >= min); newFlats = newFlats.filter(p => p.areaDeep() >= min); if (grow > 0 && newBridges.length) { newBridges = POLY.offset(newBridges, grow); } if (grow > 0 && newFlats.length) { newFlats = POLY.offset(newFlats, grow); } bridges.appendAll(newBridges); flats.appendAll(newFlats); }; /** * * * @param {Polygon[]} polys */ function addSolidFills(slice, polys) { if (slice.solids) { slice.solids.appendAll(polys); } else if (polys && polys.length) { console.log({no_solids_in: slice, for: polys}) } }; /** * project bottom flats down */ 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) { const flats = expand ? POLY.expand(slice.flats, expand) : slice.flats; projectSolid(slice, flats, count, false, true); } }; /** * project top bridges up */ 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; projectSolid(slice, slice.bridges, count, true, true); }; /** * fill projected areas and store line data * @return {boolean} true if filled, false if not */ function doSolidsFill(slice, spacing, angle, minArea, fillQ) { let minarea = minArea || 1, tops = slice.tops, solids = slice.solids; if (!(tops && solids)) { return; } if (slice.isSolidLayer || slice.xray) { return; } let unioned = POLY.union(solids, undefined, true, { wasm: true }).flat(), isSLA = (spacing === undefined && angle === undefined); if (solids.length === 0) return false; if (unioned.length === 0) return false; let trims = [], inner = isSLA ? slice.topPolys() : slice.topFillOff(); // trim each solid to the inner bounds for (let p of unioned) { p.setZ(slice.z); for (let i of inner) { let masks = p.mask(i); if (masks && masks.length > 0) { trims.appendAll(masks); } } } // clear old solids and make array for new tops.forEach(top => { top.solids = [] }); // replace solids with merged and trimmed solids slice.solids = solids = trims; // parent each solid polygon inside the smallest bounding top let make_solid_layer = false; let tops_area = tops.length ? tops.map(top => top.poly.areaDeep()).reduce((a,i) => a+i) : 0; for (let solid of solids) { for (let top of tops) { let stop = []; if (top.poly.overlaps(solid)) { if (!solid.parent || solid.parent.area() > top.poly.area()) { if (solid.areaDeep() < minarea) { // console.log({i:slice.index,cull_solid:solid,area:solid.areaDeep()}); continue; } solid.parent = top.poly; top.solids.push(solid); stop.push(solid); } } // problematic for organic shapes with lots of big and small tops // the small tops tend to trigger entire layer fills. for now just // trip full solid layer if a single top area diff > 50% if (false && stop.length) { let top_area = top.poly.areaDeep(); let stop_area = stop.map(p => p.areaDeep()).reduce((a,v) => a + v); // if the solid area > 50% of the top area, make entire layer solid if (stop_area / tops_area > 0.5) { make_solid_layer = true; } } } } // if 50% of top is filled with solids, trigger layer conversion to solid // in future, this should be limited to a specific top, not entire layer if (make_solid_layer) { for (let top of tops) { top.solids = []; } doSolidLayerFill(slice, spacing, angle); return; } // for SLA to bypass line infill if (isSLA) { return true; } // create empty filled line array for each top for (let top of tops) { // synth belt anchor tops don't want fill if (!top.fill_lines) { continue; } const tofill = []; const angfill = []; const newfill = top.fill_lines = []; // determine fill orientation from top for (let solid of solids) { if (solid.parent === top.poly) { if (solid.fillang) { angfill.push(solid); } else { tofill.push(solid); } } } if (tofill.length > 0) { doFillArea(fillQ, tofill, angle, spacing, newfill); // top.fill_lines_norm = {angle:angle,spacing:spacing}; } if (angfill.length > 0) { top.fill_lines_ang = {spacing:spacing,list:[],poly:[]}; for (let af of angfill) { doFillArea(fillQ, [af], af.fillang.angle + 45, spacing, newfill); // top.fill_lines_ang.list.push(af.fillang.angle + 45); // top.fill_lines_ang.poly.push(af.clone()); } } } } function doFillArea(fillQ, polys, angle, spacing, output, minLen, maxLen) { if (fillQ) { fillQ.push(kiri.minions.fill(polys, angle, spacing, output, minLen, maxLen)); } else { POLY.fillArea(polys, angle, spacing, output, minLen, maxLen); } } /** * calculate external overhangs requiring support */ async function doSupport(slice, proc, shadow, opt = {}) { let maxBridge = proc.sliceSupportSpan || 5, minArea = proc.supportMinArea || 0.1, pillarSize = proc.sliceSupportSize, offset = proc.sliceSupportOffset || 0, gap = proc.sliceSupportGap, size = (pillarSize || 1), tops = slice.topPolys(), trimTo = tops; let traces = POLY.flatten(slice.topShells().clone(true)), fill = slice.topFill(), points = [], down = slice.down, down_tops = down ? down.topPolys() : null, down_traces = down ? POLY.flatten(down.topShells().clone(true)) : null; if (opt.exp && down_tops) { let points = down_tops.map(p => p.deepLength).reduce((a,v)=>a+v); if (points > 200) { // use de-rez'd top shadow instead down_tops = down.topSimples(); // de-rez trace polys because it's not that important for supports down_traces = down_traces.map(p => p.clean(true, undefined, config.clipper / 10)); } } // DEBUG code let SDBG = false; let cks = SDBG ? [] : undefined; let pip = SDBG ? [] : undefined; let pcl = SDBG ? [] : undefined; // check if point is supported by layer below function checkPointSupport(point) { if (SDBG) cks.push(point); // DEBUG // skip points close to other support points for (let i=0; i= maxBridge) { let slope = p1.slopeTo(p2).factor(1/dist), segs = Math.floor(dist / maxBridge) + 1, seglen = dist / segs; while (i < segs) { checkPointSupport(p1.projectOnSlope(slope, i++ * seglen)); } } if (poly) checkPointSupport(p2); } let supports = []; // generate support polys from unsupported points if (slice.down) (function() { // check trace line support needs traces.forEach(function(trace) { trace.forEachSegment(function(p1, p2) { checkLineSupport(p1, p2, true) }); }); // add offset solids to supports (or fill depending) fill.forEachPair(function(p1,p2) { checkLineSupport(p1, p2, false) }); // skip the rest if no points or supports if (!(points.length || supports.length)) return; let pillars = []; // for each point, create a bounding rectangle points.forEach(function(point) { pillars.push(base.newPolygon().centerRectangle(point, size/2, size/2)); }); supports.appendAll(POLY.union(pillars, null, true, { wasm: false })); // merge pillars and replace with convex hull of outer points (aka smoothing) pillars = POLY.union(pillars, null, true, { wasm: false }).forEach(function(pillar) { supports.push(base.newPolygon().createConvexHull(pillar.points)); }); })(); // DEBUG code if (SDBG && down_traces) slice.output() .setLayer('cks', { line: 0xee5533, check: 0xee5533 }) .addPolys(cks.map(p => base.newPolygon().centerRectangle(p, 0.25, 0.25))) .setLayer('pip', { line: 0xdd4422, check: 0xdd4422 }) .addPolys(pip.map(p => base.newPolygon().centerRectangle(p, 0.4, 0.4))) .setLayer('pcl', { line: 0xcc3311, check: 0xcc3311 }) .addPolys(pcl.map(p => base.newPolygon().centerRectangle(p, 0.3, 0.3))) .setLayer('pts', { line: 0xdd33dd, check: 0xdd33dd }) .addPolys(points.map(p => base.newPolygon().centerRectangle(p, 0.8, 0.8))) .setLayer('dtr', { line: 0x0, check: 0x0 }) .addPolys(POLY.setZ(down_traces.clone(true),slice.z)); ; if (supports.length === 0) { return; } // then union supports if (supports.length > 10) { supports = await kiri.minions.union(supports); } else { supports = POLY.union(supports, null, true, { wasm: false }); } // clip to top polys supports = POLY.trimTo(supports, shadow); let depth = 0; while (down && supports.length > 0) { down.supports = down.supports || []; let trimmed = [], culled = []; // culled = supports; // clip supports to shell offsets POLY.subtract(supports, down.topSimples(), trimmed, null, slice.z, minArea, { wasm: false }); // set depth hint on support polys for infill density trimmed.forEach(function(trim) { if (trim.area() < minArea) return; culled.push(trim.setZ(down.z)); }); // exit when no more support polys exist if (culled.length === 0) break; // new bridge polys for next pass (skip first layer below) if (depth >= gap) { down.supports.appendAll(culled); } supports = culled; down = down.down; depth++; } } function doSupportFill(args) { const { promises, slice, lineWidth, density, minArea, isBelt, angle, outline } = args; let supports = slice.supports, nsB = [], nsC = [], min = minArea || 0.1; if (!supports) return; // union supports supports = POLY.setZ(POLY.union(supports, undefined, true, { wasm: false }), slice.z); // clip supports to slice clip offset (or shell if none) POLY.subtract(supports, slice.clips, nsB, null, slice.z, min, { wasm: false }); supports = nsB; // also trim to lower offsets, if they exist if (slice.down && slice.down.clips) { POLY.subtract(nsB, slice.down.clips, nsC, null, slice.z, min, { wasm: false }); supports = nsC; } if (supports) { fillSupportPolys({ promises, polys: supports, lineWidth, density, z: slice.z, isBelt, angle, outline }); } // re-assign new supports back to slice slice.supportOutline = supports; slice.supports = supports; }; function fillSupportPolys(args) { const { promises, polys, lineWidth, density, z, isBelt, angle, outline } = args; // calculate fill density let spacing = lineWidth * (1 / density); polys.forEach(function (poly) { // calculate angle based on width/height ratio let auto = isBelt || (poly.bounds.width() / poly.bounds.height() > 1) ? 1090 : 1000; // inset support poly for fill lines 33% of nozzle width let inset = POLY.offset([poly], -lineWidth/3, {flat: true, z, wasm: true}); // do the fill if (inset && inset.length > 0) { doFillArea(promises, inset, angle || auto, spacing, poly.fill = []); if (!outline && poly.fill.length) { poly.fill = connect_lines(poly.fill, spacing * 2); } } return true; }); } /** * * @param {Slice} slice * @param {Polygon[]} polys * @param {number} count * @param {boolean} up * @param {boolean} first * @returns {*} */ function projectSolid(slice, polys, count, up, first) { if (!slice || count <= 0) { return; } let clones = polys.clone(true); if (first) { clones.forEach(function(p) { p.hintFillAngle(); }); } addSolidFills(slice, clones); if (count > 0) { if (up) projectSolid(slice.up, polys, count-1, true, false); else projectSolid(slice.down, polys, count-1, false, false); } } FDM.supports = function(settings, widget) { let isBelt = settings.device.bedBelt; let process = settings.process; let size = process.sliceSupportSize; let s9 = size / 9; let s4 = size / 4; let s2 = size * 0.45; let min = 0.01; let geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3)); let rad = (Math.PI / 180); let deg = (180 / Math.PI); let angle = rad * settings.process.sliceSupportAngle; let thresh = -Math.sin(angle); let dir = new THREE.Vector3(0,0,-1) let add = []; let mat = new THREE.MeshBasicMaterial(); let mesh = new THREE.Mesh(geo, mat); let platform = new THREE.Mesh( new THREE.PlaneGeometry(10000,10000,1), mat ); const now = Date.now(); // test point function tp(point) { if (point.added) { return; } // omit pillars close to existing pillars for (let added of add) { let p2 = new THREE.Vector2(point.x, point.y); let pm = new THREE.Vector2(added.mid.x, added.mid.y); if (Math.abs(point.z - added.from.z) < s2 && p2.distanceTo(pm) < s4) { return; } } let ray = new THREE.Raycaster(point, dir); let int = ray.intersectObjects([ mesh, platform ], false); if (int && int.length && int[0].distance > 0.5) { let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2); add.push({from: point, to: int[0].point, mid}); point.added = true; } } function tf(a, b, c) { let dab = a.distanceTo(b); let dbc = b.distanceTo(c); let dca = c.distanceTo(a); let max = Math.max(dab, dbc, dca); if (max < size) { let min = Math.min(dab, dbc, dca); if (min < s9 && Math.random() < 0.5) { return; } // test midpoint of tri face return tp(new THREE.Vector3().add(a).add(b).add(c).divideScalar(3)); } if (dab === max) { let mp = new THREE.Vector3().add(a).add(b).divideScalar(2); tf(mp, b, c); tf(a, mp, c); } else if (dbc === max) { let mp = new THREE.Vector3().add(b).add(c).divideScalar(2); tf(a, mp, c); tf(a, b, mp); } else { let mp = new THREE.Vector3().add(c).add(a).divideScalar(2); tf(a, b, mp); tf(mp, b, c); } } let filter = isBelt ? (norm) => { return norm.z <= thresh && norm.y < 0; } : (norm) => { return norm.z < thresh; }; let { position } = geo.attributes; let { itemSize, count, array } = position; for (let i = 0; i 0; }; class Vector3Cache { constructor() { this.cache = {}; } get(x, y, z) { let key = [x.round(4),y.round(4),z.round(4)].join(','); let val = this.cache[key]; if (!val) { val = new THREE.Vector3(x, y, z); this.cache[key] = val; } return val; } } class Coplanars { constructor() { this.cache = {}; } put(a, b, c, norm) { let key = norm.round(7).toString(); let arr = this.cache[key]; if (!arr) { arr = []; this.cache[key] = arr; } arr.push([a,b,c]); } group(union) { let out = {}; for (let norm in this.cache) { let arr = this.cache[norm]; let groups = []; for (let face of arr) { let match = undefined; // see if face matches vertices in any group outer: for (let group of groups) { for (let el of group) { if ( el.indexOf(face[0]) >= 0 || el.indexOf(face[1]) >= 0 || el.indexOf(face[2]) >= 0 ) { match = group; break outer; } } } if (match) { match.push(face); } else { groups.push([face]); } } if (union) { // convert groups of faces to contiguous polygon groups groups = groups.map(group => { let parr = group.map(arr => { return base.newPolygon() .add(arr[0].x, arr[0].y, arr[0].z) .add(arr[1].x, arr[1].y, arr[1].z) .add(arr[2].x, arr[2].y, arr[2].z); }); let union = parr.length === 1 ? parr : POLY.union(parr, 0, true, {wasm:false}); union.merged = parr.length; union.face = group[0]; return union; }); } out[norm] = groups; } // console.log(out); return out; } } });