From d087aaae734583bf54ec42b0f0c4d55ed97c9c4e Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Thu, 13 Jun 2024 19:00:04 -0400 Subject: [PATCH] refactor tile packing, cut tools file pathing --- src/geo/polygon.js | 4 +- src/kiri-mode/laser/driver.js | 392 ++++++++++++++++++---------------- src/kiri/pack.js | 100 +++++++-- src/kiri/platform.js | 4 +- 4 files changed, 283 insertions(+), 217 deletions(-) diff --git a/src/geo/polygon.js b/src/geo/polygon.js index 11536a02..89645d55 100644 --- a/src/geo/polygon.js +++ b/src/geo/polygon.js @@ -717,10 +717,10 @@ class Polygon { /** * move all poly points by some offset */ - move(offset) { + move(offset, skipinner) { this._bounds = undefined; this.points = this.points.map(point => point.move(offset)); - if (this.inner) { + if (!skipinner && this.inner) { for (let inner of this.inner) { inner.move(offset); } diff --git a/src/kiri-mode/laser/driver.js b/src/kiri-mode/laser/driver.js index 5ec529e9..7f5f85d8 100644 --- a/src/kiri-mode/laser/driver.js +++ b/src/kiri-mode/laser/driver.js @@ -40,6 +40,139 @@ function init(kiri, api, current) { }); } +function polyLabel(poly, label) { + console.log('label', label); +} + +function sliceEmitObjects(print, slice, groups, opt = { }) { + let process = print.settings.process; + let stacked = process.ctOutStack; + let grouped = stacked || process.ctOutGroup; + let label = false && process.outputLaserLabel; + let simple = opt.simple || false; + let emit = { in: [], out: [], mark: [] }; + let lastEmit = opt.lastEmit; + let zcolor = print.settings.process.ctOutZColor; + + function polyOut(poly, group, type, indexed) { + if (!poly) { + return; + } + if (Array.isArray(poly)) { + poly.forEach((pi, index) => { + polyOut(pi, group, type, indexed ? index : undefined); + }); + } else { + let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1, rate: 1}; + if (type === "mark") { + pathOpt.rate = 0.001; + pathOpt.extrude = 2; + } else if (label && type === "out") { + let lbl = index !== undefined ? `${slice.index}-${index}` : `${slice.index}`; + polyLabel(poly, lbl); + } + if (simple) pathOpt.simple = true; + if (poly.open) pathOpt.open = true; + emit[type].push(poly); + print.PPP(poly, group, pathOpt); + // when stacking (top down widget slices), if the last layer is fully contained + // by the current layer, then it is "marked" onto the current layer in a different color + if (stacked && type === "out" && lastEmit) { + for (let out of lastEmit.out) { + if (out.isInside(poly)) { + polyOut(out, group, "mark"); + } + } + } + } + } + + if (grouped) { + let group = []; + group.thick = slice.thick; + let outer = slice.offset; + let inner = outer.map(poly => poly.inner || []).flat(); + // cut inside before outside + polyOut(inner, group, "in", outer.length > 1); + polyOut(outer, group, "out", outer.length > 1); + groups.push(group); + } else { + for (let top of slice.offset) { + let group = []; + group.thick = slice.thick; + polyOut([ top ], group, "in"); + polyOut(top.inner || [], group, "out"); + groups.push(group); + } + } + + return emit; +}; + +// convert arc into line segments +function arc(center, rad, s1, s2, out) { + let a1 = s1.angle; + let step = 5; + let diff = s1.angleDiff(s2, true); + let ticks = Math.abs(Math.floor(diff / step)); + let dir = Math.sign(diff); + let off = (diff % step) / 2; + if (off == 0) { + ticks++; + } else { + out.push( center.projectOnSlope(s1, rad) ); + } + while (ticks-- > 0) { + out.push( center.projectOnSlope(base.newSlopeFromAngle(a1 + off), rad) ); + a1 += step * dir; + } + out.push( center.projectOnSlope(s2, rad) ); +} + +// start to the "left" of the first point +function addKnifeRadii(poly, tipoff) { + poly.setClockwise(); + let oldpts = poly.points.slice(); + // find leftpoint and make that the first point + let start = oldpts[0]; + let startI = 0; + for (let i=1; i start.y) { + start = pt; + startI = i; + } + } + if (startI > 0) { + oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); + } + + let lastpt = oldpts[0].clone().move({x:-tipoff,y:0,z:0}); + let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); + let newpts = [ lastpt, lastpt = oldpts[0].clone() ]; + let tmp; + for (let i=1; i= 10) { + if (lastpt.distTo2D(nextpt) >= tipoff) { + arc(lastpt, tipoff, lastsl, nextsl, newpts); + } else { + // todo handle short segments + // newpts.push(lastpt.projectOnSlope(lastsl, tipoff) ); + // newpts.push( lastpt.projectOnSlope(nextsl, tipoff) ); + } + } + newpts.push(nextpt); + lastsl = nextsl; + lastpt = nextpt; + } + newpts.push( tmp = lastpt.projectOnSlope(lastsl, tipoff) ); + newpts.push( tmp.clone().move({x:tipoff, y:0, z: 0}) ); + poly.open = true; + poly.points = newpts; +} + /** * DRIVER SLICE CONTRACT * @@ -98,90 +231,26 @@ function slice(settings, widget, onupdate, ondone) { onupdate(v); } }).then(output => { + // ensure slice order is high Z to low Z for stacking widget.slices = output.slices.map(data => { let { z, lines, groups } = data; let tops = POLY.nest(groups); + // for WireEDM, we commonly omit inner polys because we can't + // navigate to cut them separately unlike laser, water, drag if (ctOmitInner) tops.forEach(top => delete top.inner); let slice = newSlice(z).addTops(tops); slice.index = indices.indexOf(z); + // create top offsets (even if 0) let offsets = slice.offset = offset ? POLY.offset(tops, offset, {z: slice.z, miter: 2 / offset}) : tops; - slice.output().setLayer("layer", { line: 0x888800 }).addPolys(tops); + slice.output().setLayer("object", { line: 0x888800 }).addPolys(tops); slice.output().setLayer("cut", { line: color }).addPolys(offsets); return slice; - }); + }).sort((a,b) => b.z - a.z); ondone(); }); }; -function polyLabel(poly, label) { - console.log('label', label); -} - -function sliceEmitObjects(print, slice, groups, opt = { }) { - let start = newPoint(0,0,0); - let process = print.settings.process; - let stacked = process.ctOutStack; - let grouped = stacked || process.ctOutGroup; - let label = false && process.outputLaserLabel; - let simple = opt.simple || false; - let emit = { in: [], out: [], mark: [] }; - let lastEmit = opt.lastEmit; - let zcolor = print.settings.process.ctOutZColor; - - function polyOut(poly, group, type, indexed) { - if (!poly) { - return; - } - if (Array.isArray(poly)) { - poly.forEach((pi, index) => { - polyOut(pi, group, type, indexed ? index : undefined); - }); - } else { - let pathOpt = zcolor ? {extrude: slice.z, rate: slice.z} : {extrude: 1, rate: 1}; - if (type === "mark") { - pathOpt.rate = 0.001; - pathOpt.extrude = 2; - } else if (label && type === "out") { - let lbl = index !== undefined ? `${slice.index}-${index}` : `${slice.index}`; - polyLabel(poly, lbl); - } - if (simple) pathOpt.simple = true; - if (poly.open) pathOpt.open = true; - emit[type].push(poly); - print.polyPrintPath(poly, start, group, pathOpt); - if (stacked && type === "out" && lastEmit) { - for (let out of lastEmit.out) { - if (out.isInside(poly)) { - polyOut(out, group, "mark"); - } - } - } - } - } - - if (grouped) { - let group = []; - group.thick = slice.thick; - let offset = slice.offset; - let inner = offset.map(poly => poly.inner || []).flat(); - // cut inside before outside - polyOut(inner, group, "in", offset.length > 1); - polyOut(offset, group, "out", offset.length > 1); - groups.push(group); - } else { - for (let top of slice.offset) { - let group = []; - group.thick = slice.thick; - polyOut([ top ], group, "in"); - polyOut(top.inner || [], group, "out"); - groups.push(group); - } - } - - return emit; -}; - /** * DRIVER PRINT CONTRACT * @@ -197,75 +266,28 @@ async function prepare(widgets, settings, update) { knifeTipOff = process.ctOutKnifeTip, output = print.output = [], totalSlices = 0, - slices = 0; + slices = 0, + { bedWidth, bedDepth } = device, + { ctOriginCenter, ctOriginBounds } = process, + { ctOriginOffX, ctOriginOffY } = process; + + // shim to adapt older code -- should be refactored + let pppo = []; + let pppg = []; + let tile = []; + let tiles = []; + print.PPP = function(poly, group, options) { + if (!pppg.contains(group)){ + pppg.push(group); + tiles.push(tile = []); + } + pppo.push({ poly, group, options }); + tile.push({ poly, group, options }); + }; // filter ignored widgets widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); - // convert arc into line segments - function arc(center, s1, s2, out) { - let a1 = s1.angle; - let step = 5; - let diff = s1.angleDiff(s2, true); - let ticks = Math.abs(Math.floor(diff / step)); - let dir = Math.sign(diff); - let off = (diff % step) / 2; - if (off == 0) { - ticks++; - } else { - out.push( center.projectOnSlope(s1, knifeTipOff) ); - } - while (ticks-- > 0) { - out.push( center.projectOnSlope(base.newSlopeFromAngle(a1 + off), knifeTipOff) ); - a1 += step * dir; - } - out.push( center.projectOnSlope(s2, knifeTipOff) ); - } - - // start to the "left" of the first point - function addKnifeRadii(poly) { - poly.setClockwise(); - let oldpts = poly.points.slice(); - // find leftpoint and make that the first point - let start = oldpts[0]; - let startI = 0; - for (let i=1; i start.y) { - start = pt; - startI = i; - } - } - if (startI > 0) { - oldpts = oldpts.slice(startI,oldpts.length).appendAll(oldpts.slice(0,startI)); - } - - let lastpt = oldpts[0].clone().move({x:-knifeTipOff,y:0,z:0}); - let lastsl = lastpt.slopeTo(oldpts[0]).toUnit(); - let newpts = [ lastpt, lastpt = oldpts[0] ]; - let tmp; - for (let i=1; i= 10) { - if (lastpt.distTo2D(nextpt) >= knifeTipOff) { - arc(lastpt, lastsl, nextsl, newpts); - } else { - // todo handle short segments - // newpts.push(lastpt.projectOnSlope(lastsl, knifeTipOff) ); - // newpts.push( lastpt.projectOnSlope(nextsl, knifeTipOff) ); - } - } - newpts.push(nextpt); - lastsl = nextsl; - lastpt = nextpt; - } - newpts.push( tmp = lastpt.projectOnSlope(lastsl, knifeTipOff) ); - newpts.push( tmp.clone().move({x:knifeTipOff, y:0, z: 0}) ); - poly.open = true; - poly.points = newpts; - } - // find max layers (for updates) for (let widget of widgets) { totalSlices += widget.slices.length; @@ -273,11 +295,11 @@ async function prepare(widgets, settings, update) { // emit objects from each slice into output array for (let widget of widgets) { - // slice stack merging - // there is not layout in this mode - // there are no inner vs outer polys - // merged/same polys "increase" color/weight if (process.ctOutMerged) { + // slice stack merging + // there is not layout in this mode + // there are no inner vs outer polys + // merged/same polys "increase" color/weight let merged = []; for (let slice of widget.slices) { let polys = []; @@ -299,30 +321,29 @@ async function prepare(widgets, settings, update) { }); update((slices++ / totalSlices) * 0.5, "prepare"); } - let start = newPoint(0,0,0); let gather = []; for (let poly of merged) { if (knifeOn) { - addKnifeRadii(poly); + addKnifeRadii(poly, knifeTipOff); } - print.polyPrintPath(poly, start, gather, { + print.PPP(poly, gather, { extrude: poly.depth, rate: poly.depth * 10 - }); + }) } output.push(gather); } else { + // output a layer for each slice if (knifeOn) { for (let slice of widget.slices) { slice.offset.forEach(poly => { - addKnifeRadii(poly); + addKnifeRadii(poly, knifeTipOff); if (poly.inner) poly.inner.forEach(ip => { - addKnifeRadii(ip); + addKnifeRadii(ip, knifeTipOff); }); }); } } - // TODO packing / layout SHOULD be done here instead let lastEmit; for (let slice of widget.slices.reverse()) { lastEmit = sliceEmitObjects(print, slice, output, {simple: knifeOn, lastEmit}); @@ -331,59 +352,50 @@ async function prepare(widgets, settings, update) { } } - // compute tile width / height - for (let layer of output) { - let min = {w:Infinity, h:Infinity}, max = {w:-Infinity, h:-Infinity}, p; - // compute bounding box for each layer - for (let out of layer) { - p = out.point; - out.point = p.clone(); // b/c first/last point are often shared - min.w = Math.min(min.w, p.x); - max.w = Math.max(max.w, p.x); - min.h = Math.min(min.h, p.y); - max.h = Math.max(max.h, p.y); - } - layer.w = max.w - min.w; - layer.h = max.h - min.h; - // shift objects to top/left of w/h box bounds - for (let out of layer) { - p = out.point; - p.x -= min.w; - p.y -= min.h; - } - } - - // do object layout packing + // for tile layout packing let dw = device.bedWidth / 2, dh = device.bedDepth / 2, sort = !process.ctOutLayer, - spacing = process.ctOutTileSpacing, - // sort objects by size when not using laser layer ordering - c = sort ? output.sort() : output, - p = new kiri.Pack(dw, dh, spacing, {invy:isWire}).fit(c, !sort); + spacing = process.ctOutTileSpacing; - // test different ratios until packed - while (!p.packed) { - dw *= 1.1; - dh *= 1.1; - p = new kiri.Pack(dw, dh, spacing, {invy:isWire}).fit(c ,!sort); + // compute tile bounds + for (let tile of tiles) { + let bounds = base.newBounds(); + tile.forEach(rec => bounds.merge(rec.poly.bounds)); + tile.w = bounds.width(); + tile.h = bounds.height(); + tile.bounds = bounds; } - // update packed tile with new location - let pad = c.length > 1 ? p.pad : 0; - for (let m of c) { - m.fit.x += m.w + pad; - m.fit.y += m.h + pad; - for (let o of m) { - // because first point emitted twice (begin/end) - let e = o.point; - e.x += p.max.w / 2 - m.fit.x; - e.y += p.max.h / 2 - m.fit.y; - e.z = 0; + // pack tiles + let tp = new kiri.Pack(dw, dh, spacing, { invy:isWire, simple: !sort }).pack(tiles, packer => { + return packer.rescale(1.1, 1.1); + }); + + let currentPos = + ctOriginCenter ? newPoint(0,0,0) : + ctOriginBounds ? newPoint(- tp.max.w/2 - ctOriginOffX, - tp.max.h/2 - ctOriginOffY, 0) : + newPoint(-dw, -dh, 0); + + // output tiles + for (let tile of tiles) { + let { fit, bounds } = tile; + for (let { poly, group, options } of tile) { + poly.setZ(0); + poly.move({ + x: fit.x - (tp.max.w / 2) - bounds.minx , + y: fit.y - (tp.max.h / 2) - bounds.miny, + z: 0}, true); + currentPos = print.polyPrintPath(poly, currentPos, group, options); } } - console.log({ output }); + // clone output points to prevent double offsetting during export + for (let layer of output) { + for (let rec of layer) { + rec.point = rec.point.clone(); + } + } return kiri.render.path(output, (progress, layer) => { update(0.5 + progress * 0.5, "render", layer); diff --git a/src/kiri/pack.js b/src/kiri/pack.js index a685ebd0..9d1a1121 100644 --- a/src/kiri/pack.js +++ b/src/kiri/pack.js @@ -13,6 +13,15 @@ const { kiri } = root; class Packer { constructor(w, h, spacing, opt = {}) { + this.w = w; + this.h = h; + this.spacing = spacing; + this.opt = opt; + this.init(); + } + + init() { + const { w, h, spacing, opt } = this; this.root = { x: 0, y: 0, w: w, h: h }; this.max = { w: 0, h: 0 }; this.packed = false; @@ -21,23 +30,61 @@ class Packer { this.pad = this.spacing / 2; } + get size() { + return { w: this.w, h: this. h }; + } + + resize(w, h) { + this.w = w; + this.h = h; + return this; + } + + rescale(wm, hm) { + this.w *= wm; + this.h *= hm; + return this; + } + + pack(blocks, onretry) { + for (;;) { + let simple = this.opt.simple; + this.fit(blocks, simple); + if (!this.packed && onretry && onretry(this)) { + this.init(); + continue; + } + return this; + } + } + // array of blocks/tiles with {w,h} properties - simple(blocks) { - let n = 0, block, x = 0, y = 0, mh = 0, maxx = 0, maxy = 0; - let spacing = blocks.length > 1 ? this.spacing : 0; - while (n < blocks.length) { - block = blocks[n++]; + fit(blocks, simple) { + if (simple) { + return this.#fit_simple(blocks); + } else { + return this.#fit_split(blocks); + } + } + + // array of blocks/tiles with {w,h} properties + #fit_simple(blocks) { + let x = 0, y = 0, mh = 0, maxx = 0, maxy = 0; + let spacing = this.spacing; + for (let block of blocks) { block.fit = { x, y }; this.max.w = Math.max(this.max.w, x + block.w); this.max.h = Math.max(this.max.h, y + block.h); maxx = Math.max(maxx, x); maxy = Math.max(maxy, y); mh = Math.max(mh, block.h); - x += block.w + spacing; + x += block.w; if (x > this.root.w) { x = 0; y = y + mh + spacing; mh = 0; + } else { + x += spacing; } if (y > this.root.h) { return this; @@ -57,18 +104,14 @@ class Packer { return this; } - fit(blocks, simple) { - if (simple) { - return this.simple(blocks); - } - let n = 0, node, block, w, h; - let spacing = blocks.length > 1 ? this.spacing : 0; - while (n < blocks.length) { - block = blocks[n++]; - w = block.w + spacing; - h = block.h + spacing; - if (node = this.findNode(this.root, w, h)) { - block.fit = this.splitNode(node, w, h); + // array of blocks/tiles with {w,h} properties + #fit_split(blocks) { + let node, w, h; + for (let block of blocks) { + w = block.w; + h = block.h; + if (node = this.#findNode(this.root, w, h)) { + block.fit = this.#splitNode(node, w, h); } else { return this; } @@ -77,9 +120,9 @@ class Packer { return this; } - findNode(root, w, h) { + #findNode(root, w, h) { if (root.used) { - return this.findNode(root.right, w, h) || this.findNode(root.down, w, h); + return this.#findNode(root.right, w, h) || this.#findNode(root.down, w, h); } else if (w <= root.w && h <= root.h) { return root; } else { @@ -87,10 +130,21 @@ class Packer { } } - splitNode(node, w, h) { + #splitNode(node, w, h) { + let spacing = this.spacing; node.used = true; - node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h }; - node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h }; + node.down = { + x: node.x, + y: node.y + h + spacing, + w: node.w, + h: node.h - h + }; + node.right = { + x: node.x + w + spacing, + y: node.y, + w: node.w - w, + h: h + }; this.max.w = Math.max(this.max.w, node.x + w); this.max.h = Math.max(this.max.h, node.y + h); return node; diff --git a/src/kiri/platform.js b/src/kiri/platform.js index 5488c014..97b73613 100644 --- a/src/kiri/platform.js +++ b/src/kiri/platform.js @@ -735,8 +735,8 @@ function platformLayout() { for (i = 0; i < c.length; i++) { m = c[i]; - m.fit.x += m.w / 2 + p.pad; - m.fit.y += m.h / 2 + p.pad; + m.fit.x += m.w / 2; + m.fit.y += m.h / 2; m.move(p.max.w / 2 - m.fit.x, p.max.h / 2 - m.fit.y, 0, true); } }