From 14ee94311dc9846bf8092bab22aa6077870509c6 Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Wed, 23 Jun 2021 11:13:45 -0400 Subject: [PATCH] improve efficiency of wasm poly diff --- src/geo/polygons.js | 43 +++++++++++++---------------- src/geo/wasm.js | 61 +++++++++++++++++++++++------------------- src/mode/fdm/slice.js | 2 +- wasm/kiri-geo.cpp | 8 +++++- wasm/kiri-geo.wasm | Bin 65053 -> 66303 bytes 5 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/geo/polygons.js b/src/geo/polygons.js index 9cc5efaa..5d4e17a1 100644 --- a/src/geo/polygons.js +++ b/src/geo/polygons.js @@ -309,45 +309,40 @@ return to; } - // expensive but worth it? - clip.StrictlySimple = true; - const JS = true; - if (outA) { - if (JS) { + if (JS) { + // expensive but worth it? + clip.StrictlySimple = true; + if (outA) { clip.AddPaths(sp1, ptyp.ptSubject, true); clip.AddPaths(sp2, ptyp.ptClip, true); - if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { cleanClipperTree(ctre); filter(fromClipperTree(ctre, z, null, null, min), outA); } - } else { - let woutA = filter(geo.poly.diff(setA, setB, z)); - outA.appendAll(woutA); - // console.log({outA, woutA}); } - } - - if (outB) { - if (outA) { - ctre.Clear(); - clip.Clear(); - } - - if (JS) { + if (outB) { + if (outA) { + ctre.Clear(); + clip.Clear(); + } clip.AddPaths(sp2, ptyp.ptSubject, true); clip.AddPaths(sp1, ptyp.ptClip, true); - if (clip.Execute(ctyp.ctDifference, ctre, cfil.pftEvenOdd, cfil.pftEvenOdd)) { cleanClipperTree(ctre); filter(fromClipperTree(ctre, z, null, null, min), outB); } - } else { - let woutB = filter(geo.poly.diff(setB, setA, z)); - outB.appendAll(woutB); - // console.log({outB, woutB}); + } + } else { + let oA = outA ? [] : undefined; + let oB = outB ? [] : undefined; + geo.wasm.js.diff(setA, setB, z, oA, oB); + if (oA) { + outA.appendAll(filter(oA)); + } + if (oB) { + outB.appendAll(filter(oB)); } } diff --git a/src/geo/wasm.js b/src/geo/wasm.js index 243c9f6e..8d2cc03e 100644 --- a/src/geo/wasm.js +++ b/src/geo/wasm.js @@ -11,12 +11,6 @@ if (!self.window) (function() { const geo = self.geo; const debug = false; - geo.poly = { - diff: polyDiff, - union: polyUnion, - offset: polyOffset - }; - function log() { console.log(...arguments); } @@ -76,34 +70,36 @@ if (!self.window) (function() { function polyOffset(polys, offset, z) { let wasm = geo.wasm, - memat = wasm.malloc(1024 * 128), - pcount = writePolys(new DataWriter(wasm.heap, memat), polys), - resat = wasm.offset(memat, pcount, offset * factor), + buffer = geo.wasm.shared, + pcount = writePolys(new DataWriter(wasm.heap, buffer), polys), + resat = wasm.fn.offset(buffer, pcount, offset * factor), out = readPolys(new DataReader(wasm.heap, resat), z); - wasm.free(memat); return polyNest(out); } function polyUnion(polys, z) { let wasm = geo.wasm, - memat = wasm.malloc(1024 * 128), - pcount = writePolys(new DataWriter(wasm.heap, memat), polys), - resat = wasm.union(memat, pcount), + buffer = geo.wasm.shared, + pcount = writePolys(new DataWriter(wasm.heap, buffer), polys), + resat = wasm.fn.union(buffer, pcount), out = readPolys(new DataReader(wasm.heap, resat), z); - wasm.free(memat); return polyNest(out); } - function polyDiff(polysA, polysB, z) { + function polyDiff(polysA, polysB, z, AB, BA) { let wasm = geo.wasm, - memat = wasm.malloc(1024 * 128), - writer = new DataWriter(wasm.heap, memat), + buffer = geo.wasm.shared, + writer = new DataWriter(wasm.heap, buffer), pcountA = writePolys(writer, polysA), pcountB = writePolys(writer, polysB), - resat = wasm.diff(memat, pcountA, pcountB, 1, 0), - out = readPolys(new DataReader(wasm.heap, resat), z); - wasm.free(memat); - return polyNest(out); + resat = wasm.fn.diff(buffer, pcountA, pcountB, AB?1:0, BA?1:0, base.config.clipperClean), + reader = new DataReader(wasm.heap, resat); + if (AB) { + AB.appendAll(polyNest(readPolys(reader, z))); + } + if (BA) { + BA.appendAll(polyNest(readPolys(reader, z))); + } } // nest closed polygons without existing parent / child relationships @@ -138,10 +134,11 @@ if (!self.window) (function() { let newPolygon = self.base.newPolygon; let p1 = newPolygon().add(0,0).add(4,0).add(4,4).add(0,4); let p2 = p1.clone().setZ(0).move({x:2, y:2}); - let d1 = geo.poly.diff([p1], [p2], 0); - let d2 = geo.poly.diff([p2], [p1], 0); + let d1 = []; + let d2 = []; + geo.wasm.js.diff([p1], [p2], 0, d1, d2); console.log('diff',{p1:p1.points, p2:p2.points, d1:d1[0].points, d2:d2[0].points}); - let o1 = geo.poly.offset([p1], 1, 0); + let o1 = geo.wasm.js.offset([p1], 1, 0); console.log('offs',{o1:o1[0].points}); } @@ -162,8 +159,8 @@ if (!self.window) (function() { } })) .then(results => { - let {module, instance} = results; - let {exports} = instance; + let { module, instance } = results; + let { exports } = instance; let heap = new DataView(exports.memory.buffer); let wasm = geo.wasm = { heap, @@ -172,10 +169,18 @@ if (!self.window) (function() { memmax: exports.memory.buffer.byteLength, malloc: exports.mem_get, free: exports.mem_clr, + set_debug: exports.set_debug + }; + wasm.shared = wasm.malloc(1024 * 1024 * 10), + wasm.fn = { diff: exports.poly_diff, union: exports.poly_union, - offset: exports.poly_offset, - set_debug: exports.set_debug + offset: exports.poly_offset + }; + wasm.js = { + diff: polyDiff, + union: polyUnion, + offset: polyOffset }; if (debug) { wasm.set_debug(1); diff --git a/src/mode/fdm/slice.js b/src/mode/fdm/slice.js index de6e9abb..cbee0860 100644 --- a/src/mode/fdm/slice.js +++ b/src/mode/fdm/slice.js @@ -1171,7 +1171,7 @@ // create inner clip offset from tops (unless pre-computed) if (!slice.offsets) { // POLY.expand(tops, offset, slice.z, slice.offsets = []); - slice.offsets = geo.poly.offset(tops, offset, slice.z); + slice.offsets = geo.wasm.js.offset(tops, offset, slice.z); } let traces = POLY.flatten(slice.topShells().clone(true)), diff --git a/wasm/kiri-geo.cpp b/wasm/kiri-geo.cpp index 1b9d412d..f47dc2c3 100644 --- a/wasm/kiri-geo.cpp +++ b/wasm/kiri-geo.cpp @@ -135,7 +135,7 @@ Uint32 poly_union(Uint32 memat, Uint32 polys, float offset) { } __attribute__ ((export_name("poly_diff"))) -Uint32 poly_diff(Uint32 memat, Uint32 polysA, Uint32 polysB, Uint8 AB, Uint8 BA) { +Uint32 poly_diff(Uint32 memat, Uint32 polysA, Uint32 polysB, Uint8 AB, Uint8 BA, Uint32 clean) { Paths inA(polysA); Paths inB(polysB); @@ -152,6 +152,9 @@ Uint32 poly_diff(Uint32 memat, Uint32 polysA, Uint32 polysB, Uint8 AB, Uint8 BA) clip.AddPaths(inA, ptSubject, true); clip.AddPaths(inB, ptClip, true); clip.Execute(ctDifference, outs, pftEvenOdd, pftEvenOdd); + if (clean > 0) { + CleanPolygons(outs, (double)clean); + } pos = writePolys(outs, pos); clip.Clear(); } @@ -162,6 +165,9 @@ Uint32 poly_diff(Uint32 memat, Uint32 polysA, Uint32 polysB, Uint8 AB, Uint8 BA) clip.AddPaths(inB, ptSubject, true); clip.AddPaths(inA, ptClip, true); clip.Execute(ctDifference, outs, pftEvenOdd, pftEvenOdd); + if (clean > 0) { + CleanPolygons(outs, (double)clean); + } pos = writePolys(outs, pos); clip.Clear(); } diff --git a/wasm/kiri-geo.wasm b/wasm/kiri-geo.wasm index 7c4a688c717dc98d175e4a11976ce8f0e4f38bdf..69cbb00d2ac2405bfbc2c9b404e44ad931c5effb 100755 GIT binary patch delta 10986 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 SNeS|?`3?10?e4P^lKu^kM7Ns& delta 9760 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 on1XV+ygnDZ**gTvK#{KC